mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-13 03:02:38 +00:00
## About The Pull Request The fish analyzer now uses ui_data to fetch hunger, health, size, weight and breeding cooldown rather than ui_static_data, meaning these values are updated in real time on the UI. It also calls update_static_data_for_all_viewers() whenever the user scans a new fish or aquarium or if fishes are added/removed to/from the aquarium, and closes the UI if the scanned object is out of normal view or further than 7 tiles away. I've also reduced the breeding cooldown for newly spawned fish from two times the standard cooldown (usually 2 minutes, hence 4 minutes) to 60% of it. Offsprings still retain the usual 200% cooldown however. This should make it a bit easier for people who want to use the aquarium with fish acquired through means other than fish farming itself. Lastly, this PR introduces a small unit test to make sure that the stable population of most fish is higher than 1. This was the problem with #93043, where you couldn't breed a slimefish with a lavaloop because the stable population of the latter wasn't set. I'm sure that's a problem with other fishes as well, and that explains some of the confusion with the feature (that and its opacity as a whole I guess). ## Why It's Good For The Game This will close #93043, improve the fish analyzer UI updates, make fish farming etc. less problematic. ## Changelog 🆑 qol: The fish analyzer UI should update more reliably. fix: Fixed some of the fishes being unable to reproduce. balance: Fish acquired through means other than fish farming itself takes less time to be able to reproduce. /🆑
744 lines
30 KiB
Plaintext
744 lines
30 KiB
Plaintext
///Defines that clamp the beauty of the aquarium, to prevent it from making most areas great or horrid all by itself.
|
|
#define MIN_AQUARIUM_BEAUTY -3500
|
|
#define MAX_AQUARIUM_BEAUTY 6000
|
|
|
|
/**
|
|
* The component that manages the aquariums UI, fluid, temperature, the current fish inside the parent object, as well as beauty,
|
|
* and a few other common aquarium features.
|
|
*/
|
|
/datum/component/aquarium
|
|
dupe_mode = COMPONENT_DUPE_UNIQUE
|
|
can_transfer = TRUE
|
|
/// list of fishes inside the parent object, sorted by type - does not include things with aquarium visuals that are not fish
|
|
var/list/tracked_fish_by_type
|
|
|
|
///The current type of fluid of the aquarium
|
|
var/fluid_type = AQUARIUM_FLUID_FRESHWATER
|
|
///The current temperature of the fluid of the aquarium
|
|
var/fluid_temp = DEFAULT_AQUARIUM_TEMP
|
|
|
|
///A lazy list of key instances and assoc vals representing how much beauty they contribute to the aquarium
|
|
var/list/beauty_by_content
|
|
|
|
///The default beauty of the aquarium when empty.
|
|
var/default_beauty
|
|
|
|
///A list of layers that are currently being used for the various overlays of the aquarium (from aquarium_content comp)
|
|
var/list/used_layers = list()
|
|
|
|
///The minimum pixel x of the area where vis overlays should be displayed
|
|
var/aquarium_zone_min_pw
|
|
///The maximum pixel x of the area where vis overlays should be displayed
|
|
var/aquarium_zone_max_pw
|
|
///The minimum pixel y of the area where vis overlays should be displayed
|
|
var/aquarium_zone_min_pz
|
|
///The maximum pixel y of the area where vis overlays should be displayed
|
|
var/aquarium_zone_max_pz
|
|
|
|
///While the feed (reagent) storage is not empty, this is the interval which the fish are fed.
|
|
var/feeding_interval = 3 MINUTES
|
|
///The last time fishes were fed by the acquarium itsef.
|
|
var/last_feeding
|
|
|
|
///The minimum fluid temperature that can be reached by this aquarium
|
|
var/min_fluid_temp = MIN_AQUARIUM_TEMP
|
|
///The maximum fluid temperature that can be reached by this aquarium
|
|
var/max_fluid_temp = MAX_AQUARIUM_TEMP
|
|
|
|
///static list of available fluid types.
|
|
var/static/list/fluid_types = list(
|
|
AQUARIUM_FLUID_SALTWATER,
|
|
AQUARIUM_FLUID_FRESHWATER,
|
|
AQUARIUM_FLUID_SULPHWATEVER,
|
|
AQUARIUM_FLUID_AIR,
|
|
)
|
|
///static list of available aquarium modes
|
|
var/static/list/aquarium_modes = list(
|
|
AQUARIUM_MODE_MANUAL = "Take full control of fluid and temperature settings. There's no stasis option here.",
|
|
AQUARIUM_MODE_AUTO = "Let an internal processor regulate aquarium settings based on its fish population. Can temporarily fall back to stasis.",
|
|
AQUARIUM_MODE_SAFE = "Prevent fish from dying from wrong fluid/temperature settings and hunger, but also stops growth and reproduction",
|
|
)
|
|
|
|
///The size of the reagents holder which will store fish feed.
|
|
var/reagents_size
|
|
|
|
///The current aquarium mode, one of either AQUARIUM_MODE_MANUAL, AQUARIUM_MODE_AUTO or AQUARIUM_MODE_SAFE.
|
|
var/current_mode
|
|
|
|
/datum/component/aquarium/Initialize(
|
|
min_px,
|
|
max_px,
|
|
min_py,
|
|
max_py,
|
|
default_beauty = 0,
|
|
reagents_size = 6,
|
|
min_fluid_temp = MIN_AQUARIUM_TEMP,
|
|
max_fluid_temp = MAX_AQUARIUM_TEMP,
|
|
init_mode = AQUARIUM_MODE_AUTO,
|
|
)
|
|
|
|
if(!ismovable(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
src.default_beauty = default_beauty
|
|
src.reagents_size = reagents_size
|
|
|
|
aquarium_zone_min_pw = min_px
|
|
aquarium_zone_max_pw = max_px
|
|
aquarium_zone_min_pz = min_py
|
|
aquarium_zone_max_pz = max_py
|
|
|
|
src.min_fluid_temp = min_fluid_temp
|
|
src.max_fluid_temp = max_fluid_temp
|
|
fluid_temp = clamp(fluid_temp, min_fluid_temp, max_fluid_temp)
|
|
|
|
set_aquarium_mode(init_mode)
|
|
|
|
/datum/component/aquarium/RegisterWithParent()
|
|
if(default_beauty)
|
|
update_aquarium_beauty(0)
|
|
|
|
RegisterSignals(parent, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(on_entered))
|
|
RegisterSignal(parent, COMSIG_ATOM_EXITED, PROC_REF(on_exited))
|
|
|
|
RegisterSignal(parent, COMSIG_AQUARIUM_GET_REPRODUCTION_CANDIDATES, PROC_REF(get_candidates))
|
|
RegisterSignal(parent, COMSIG_AQUARIUM_CHECK_EVOLUTION_CONDITIONS, PROC_REF(check_evolution))
|
|
|
|
RegisterSignal(parent, COMSIG_AQUARIUM_SET_VISUAL, PROC_REF(set_visual))
|
|
RegisterSignal(parent, COMSIG_AQUARIUM_REMOVE_VISUAL, PROC_REF(remove_visual))
|
|
|
|
var/atom/movable/movable = parent
|
|
|
|
ADD_KEEP_TOGETHER(movable, AQUARIUM_TRAIT) //render the fish on the same layer of the aquarium.
|
|
|
|
if(reagents_size > 0)
|
|
if(!movable.reagents)
|
|
movable.create_reagents(reagents_size, SEALED_CONTAINER)
|
|
if(movable.reagents.total_volume)
|
|
start_autofeed(movable.reagents)
|
|
else
|
|
RegisterSignal(movable.reagents, COMSIG_REAGENTS_HOLDER_UPDATED, PROC_REF(start_autofeed))
|
|
RegisterSignal(movable, COMSIG_PLUNGER_ACT, PROC_REF(on_plunger_act))
|
|
|
|
RegisterSignal(movable, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_interaction))
|
|
RegisterSignal(movable, COMSIG_CLICK_ALT, PROC_REF(on_click_alt))
|
|
RegisterSignal(movable, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
|
|
|
|
if(isitem(movable))
|
|
RegisterSignal(movable, COMSIG_ITEM_ATTACK_SELF, PROC_REF(interact))
|
|
RegisterSignal(movable, COMSIG_ITEM_ATTACK_SELF_SECONDARY, PROC_REF(secondary_interact))
|
|
RegisterSignals(movable, list(COMSIG_ATOM_ATTACK_ROBOT_SECONDARY, COMSIG_ATOM_ATTACK_HAND_SECONDARY), PROC_REF(on_secondary_attack_hand))
|
|
else
|
|
RegisterSignal(movable, COMSIG_ATOM_UI_INTERACT, PROC_REF(interact))
|
|
|
|
movable.AddElement(/datum/element/relay_attackers)
|
|
movable.AddComponent(/datum/component/fishing_spot, /datum/fish_source/aquarium)
|
|
|
|
|
|
movable.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
|
|
RegisterSignal(movable, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
|
|
|
|
for(var/atom/movable/content as anything in movable.contents)
|
|
if(content.flags_1 & INITIALIZED_1)
|
|
on_entered(movable, content)
|
|
|
|
movable.add_traits(list(TRAIT_IS_AQUARIUM, TRAIT_STOP_FISH_FLOPPING), AQUARIUM_TRAIT)
|
|
|
|
/datum/component/aquarium/UnregisterFromParent()
|
|
var/atom/movable/movable = parent
|
|
UnregisterSignal(movable, list(
|
|
COMSIG_ATOM_ENTERED,
|
|
COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON,
|
|
COMSIG_ATOM_EXITED,
|
|
COMSIG_AQUARIUM_GET_REPRODUCTION_CANDIDATES,
|
|
COMSIG_AQUARIUM_CHECK_EVOLUTION_CONDITIONS,
|
|
COMSIG_AQUARIUM_SET_VISUAL,
|
|
COMSIG_AQUARIUM_REMOVE_VISUAL,
|
|
COMSIG_PLUNGER_ACT,
|
|
COMSIG_ATOM_ITEM_INTERACTION,
|
|
COMSIG_CLICK_ALT,
|
|
COMSIG_ATOM_EXAMINE,
|
|
COMSIG_ITEM_ATTACK_SELF,
|
|
COMSIG_ATOM_ATTACK_ROBOT_SECONDARY,
|
|
COMSIG_ATOM_ATTACK_HAND_SECONDARY,
|
|
COMSIG_ATOM_UI_INTERACT,
|
|
COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM,
|
|
))
|
|
if(movable.reagents)
|
|
UnregisterSignal(movable, COMSIG_REAGENTS_HOLDER_UPDATED)
|
|
STOP_PROCESSING(SSobj, src)
|
|
beauty_by_content = null
|
|
tracked_fish_by_type = null
|
|
movable.remove_traits(list(TRAIT_IS_AQUARIUM, TRAIT_AQUARIUM_PANEL_OPEN, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, TRAIT_STOP_FISH_FLOPPING), AQUARIUM_TRAIT)
|
|
qdel(movable.GetComponent(/datum/component/fishing_spot))
|
|
REMOVE_KEEP_TOGETHER(movable, AQUARIUM_TRAIT)
|
|
|
|
/datum/component/aquarium/PreTransfer(atom/movable/new_parent)
|
|
if(!istype(new_parent))
|
|
return
|
|
if(HAS_TRAIT(parent, TRAIT_AQUARIUM_PANEL_OPEN))
|
|
ADD_TRAIT(new_parent, TRAIT_AQUARIUM_PANEL_OPEN, AQUARIUM_TRAIT)
|
|
if(HAS_TRAIT_FROM(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT))
|
|
ADD_TRAIT(new_parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
var/atom/movable/movable = parent
|
|
var/old_mode = current_mode //Make sure we don't keep updating temp/fluid on AQUARIUM_MODE_AUTO for this.
|
|
current_mode = NONE
|
|
for(var/atom/movable/moving as anything in movable.contents)
|
|
if(HAS_TRAIT(moving, TRAIT_AQUARIUM_CONTENT))
|
|
moving.forceMove(new_parent)
|
|
current_mode = old_mode
|
|
if(reagents_size)
|
|
if(!new_parent.reagents)
|
|
new_parent.create_reagents(reagents_size, SEALED_CONTAINER)
|
|
movable.reagents.trans_to(new_parent, movable.reagents.total_volume)
|
|
|
|
/datum/component/aquarium/PostTransfer(datum/new_parent)
|
|
if(!ismovable(new_parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
/datum/component/aquarium/InheritComponent(datum/component/aquarium/new_comp, i_am_original)
|
|
fluid_temp = clamp(new_comp.fluid_temp, min_fluid_temp, max_fluid_temp)
|
|
set_fluid_type(new_comp.fluid_type)
|
|
feeding_interval = new_comp.feeding_interval
|
|
last_feeding = new_comp.last_feeding
|
|
var/atom/movable/movable = parent
|
|
movable.update_appearance()
|
|
|
|
/datum/component/aquarium/proc/on_click_alt(atom/movable/source, mob/living/user)
|
|
SIGNAL_HANDLER
|
|
if(!user.can_perform_action(source))
|
|
return
|
|
var/closing = HAS_TRAIT(parent, TRAIT_AQUARIUM_PANEL_OPEN)
|
|
if(closing)
|
|
REMOVE_TRAIT(parent, TRAIT_AQUARIUM_PANEL_OPEN, AQUARIUM_TRAIT)
|
|
source.reagents.flags &= ~(TRANSPARENT|REFILLABLE)
|
|
SStgui.close_uis(src)
|
|
else
|
|
ADD_TRAIT(parent, TRAIT_AQUARIUM_PANEL_OPEN, AQUARIUM_TRAIT)
|
|
source.reagents.flags |= TRANSPARENT|REFILLABLE
|
|
|
|
source.balloon_alert(user, "panel [closing ? "closed" : "open"]")
|
|
source.update_appearance()
|
|
return CLICK_ACTION_SUCCESS
|
|
|
|
///This proc handles feeding the aquarium and inserting aquarium content.
|
|
/datum/component/aquarium/proc/on_item_interaction(atom/movable/source, mob/living/user, obj/item/item, modifiers)
|
|
SIGNAL_HANDLER
|
|
|
|
if(istype(item, /obj/item/reagent_containers/cup/fish_feed))
|
|
if(source.reagents && HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN))
|
|
return //don't block, we'll be transferring reagents to the feed storage.
|
|
if(!item.reagents.total_volume)
|
|
source.balloon_alert(user, "[item] is empty!")
|
|
return ITEM_INTERACT_BLOCKING
|
|
var/list/fishes = get_fishes()
|
|
if(!length(fishes))
|
|
source.balloon_alert(user, "no fish to feed!")
|
|
return ITEM_INTERACT_BLOCKING
|
|
feed_fishes(item, fishes)
|
|
source.balloon_alert(user, "fed the fish")
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
if(!HAS_TRAIT(item, TRAIT_AQUARIUM_CONTENT) || (!isitem(parent) && user.combat_mode))
|
|
return //proceed with normal interactions
|
|
|
|
var/broken = source.get_integrity_percentage() <= source.integrity_failure
|
|
if(!can_insert(source, item, user))
|
|
return ITEM_INTERACT_BLOCKING
|
|
if(broken)
|
|
source.balloon_alert(user, "aquarium is broken!")
|
|
return ITEM_INTERACT_BLOCKING
|
|
if(!user.transferItemToLoc(item, source))
|
|
user.balloon_alert(user, "stuck to your hand!")
|
|
return ITEM_INTERACT_BLOCKING
|
|
source.balloon_alert(user, "added to aquarium")
|
|
source.update_appearance()
|
|
return ITEM_INTERACT_SUCCESS
|
|
|
|
///Called when the feed storage is no longer empty.
|
|
/datum/component/aquarium/proc/start_autofeed(datum/reagents/source)
|
|
SIGNAL_HANDLER
|
|
UnregisterSignal(source, COMSIG_REAGENTS_HOLDER_UPDATED)
|
|
START_PROCESSING(SSobj, src)
|
|
|
|
///Feed the fish at defined intervals until the feed storage is empty.
|
|
/datum/component/aquarium/process(seconds_per_tick)
|
|
//safe mode, no need to feed the fishes
|
|
if(HAS_TRAIT_FROM(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT))
|
|
last_feeding += seconds_per_tick SECONDS
|
|
return
|
|
var/atom/movable/movable = parent
|
|
if(!movable.reagents?.total_volume)
|
|
if(movable.reagents)
|
|
RegisterSignal(movable.reagents, COMSIG_REAGENTS_HOLDER_UPDATED, PROC_REF(start_autofeed))
|
|
return PROCESS_KILL
|
|
if(world.time < last_feeding + feeding_interval)
|
|
return
|
|
last_feeding = world.time
|
|
feed_fishes(movable)
|
|
|
|
/datum/component/aquarium/proc/feed_fishes(atom/movable/movable, list/fishes = get_fishes())
|
|
for(var/obj/item/fish/fish as anything in fishes)
|
|
fish.feed(movable.reagents)
|
|
if(current_mode == AQUARIUM_MODE_AUTO && is_fish_population_safe())
|
|
REMOVE_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
|
|
/datum/component/aquarium/proc/on_plunger_act(atom/movable/source, obj/item/plunger/plunger, mob/living/user, reinforced)
|
|
SIGNAL_HANDLER
|
|
if(!HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN))
|
|
source.balloon_alert(user, "open panel first!")
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(do_plunging), source, user)
|
|
return COMPONENT_NO_AFTERATTACK
|
|
|
|
/datum/component/aquarium/proc/do_plunging(atom/movable/source, mob/living/user)
|
|
user.balloon_alert_to_viewers("plunging...")
|
|
if(do_after(user, 3 SECONDS, target = source))
|
|
user.balloon_alert_to_viewers("finished plunging")
|
|
source.reagents.expose(get_turf(source), TOUCH) //splash on the floor
|
|
source.reagents.clear_reagents()
|
|
|
|
/datum/component/aquarium/proc/on_examine(atom/movable/source, mob/user, list/examine_list)
|
|
SIGNAL_HANDLER
|
|
examine_list += span_notice("Its temperature and fluid are currently set to [EXAMINE_HINT("[fluid_temp] K")] and [EXAMINE_HINT(fluid_type)].")
|
|
var/panel_open = HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN)
|
|
examine_list += span_notice("[EXAMINE_HINT("Alt-click")] to [panel_open ? "close" : "open"] the control and feed panel.")
|
|
if(panel_open && source.reagents.total_volume)
|
|
examine_list += span_notice("You can use a plunger to empty the feed storage.")
|
|
|
|
///Check if an item can be inserted into the aquarium
|
|
/datum/component/aquarium/proc/can_insert(atom/movable/source, obj/item/item, mob/living/user)
|
|
var/return_value = SEND_SIGNAL(src, COMSIG_AQUARIUM_CAN_INSERT, item, user)
|
|
if(return_value & COMSIG_CANNOT_INSERT_IN_AQUARIUM)
|
|
return FALSE
|
|
if(return_value & COMSIG_CAN_INSERT_IN_AQUARIUM)
|
|
return TRUE
|
|
|
|
if(HAS_TRAIT(item, TRAIT_UNIQUE_AQUARIUM_CONTENT))
|
|
for(var/atom/movable/content as anything in source)
|
|
if(content == item)
|
|
continue
|
|
if(content.type == item.type)
|
|
source.balloon_alert(user, "cannot add to aquarium!")
|
|
return FALSE
|
|
return TRUE
|
|
|
|
///Handles aquarium content insertion
|
|
/datum/component/aquarium/proc/on_entered(atom/movable/source, atom/movable/entered)
|
|
SIGNAL_HANDLER
|
|
get_content_beauty(entered)
|
|
if(!isfish(entered))
|
|
return
|
|
var/obj/item/fish/fish = entered
|
|
LAZYADDASSOCLIST(tracked_fish_by_type, entered.type, entered)
|
|
if(fish.stable_population < length(tracked_fish_by_type[fish.type]))
|
|
for(var/obj/item/fish/anyfin as anything in tracked_fish_by_type[entered.type])
|
|
anyfin.fish_flags |= FISH_FLAG_OVERPOPULATED
|
|
|
|
if(current_mode == AQUARIUM_MODE_AUTO && fish.status != FISH_DEAD)
|
|
get_optimal_aquarium_settings()
|
|
else
|
|
check_fluid_and_temperature(fish)
|
|
RegisterSignal(fish, COMSIG_FISH_STATUS_CHANGED, PROC_REF(on_fish_status_changed))
|
|
|
|
///update the beauty_by_content of a 'beauty_by_content' key and then recalculate the beauty.
|
|
/datum/component/aquarium/proc/get_content_beauty(atom/movable/content)
|
|
var/list/beauty_holder = list()
|
|
SEND_SIGNAL(content, COMSIG_MOVABLE_GET_AQUARIUM_BEAUTY, beauty_holder)
|
|
var/beauty = beauty_holder[1]
|
|
if(!beauty)
|
|
return
|
|
var/old_beauty = default_beauty
|
|
for(var/key in beauty_by_content)
|
|
old_beauty += beauty_by_content[key]
|
|
LAZYSET(beauty_by_content, content, beauty)
|
|
update_aquarium_beauty(old_beauty)
|
|
|
|
///Handles aquarium content removal.
|
|
/datum/component/aquarium/proc/on_exited(atom/movable/source, atom/movable/gone)
|
|
SIGNAL_HANDLER
|
|
var/beauty = beauty_by_content?[gone]
|
|
if(beauty)
|
|
var/old_beauty = default_beauty
|
|
for(var/key in beauty_by_content)
|
|
old_beauty += beauty_by_content[key]
|
|
LAZYREMOVE(beauty_by_content, gone)
|
|
update_aquarium_beauty(old_beauty)
|
|
if(!isfish(gone))
|
|
return
|
|
var/obj/item/fish/fish = gone
|
|
if(fish.stable_population == length(tracked_fish_by_type[fish.type]))
|
|
for(var/obj/item/fish/anyfin as anything in tracked_fish_by_type[fish.type])
|
|
anyfin.fish_flags &= ~FISH_FLAG_OVERPOPULATED
|
|
LAZYREMOVEASSOC(tracked_fish_by_type, fish.type, fish)
|
|
fish.fish_flags &= ~(FISH_FLAG_SAFE_TEMPERATURE|FISH_FLAG_SAFE_FLUID)
|
|
UnregisterSignal(gone, COMSIG_FISH_STATUS_CHANGED, PROC_REF(on_fish_status_changed))
|
|
if(current_mode == AQUARIUM_MODE_AUTO && fish.status != FISH_DEAD)
|
|
get_optimal_aquarium_settings()
|
|
|
|
///Return a list of fish which our fishie can reproduce with (including itself if self-reproducing)
|
|
/datum/component/aquarium/proc/get_candidates(atom/movable/source, obj/item/fish/fish, list/candidates)
|
|
SIGNAL_HANDLER
|
|
var/list/types_to_mate_with = tracked_fish_by_type
|
|
if(!HAS_TRAIT(fish, TRAIT_FISH_CROSSBREEDER))
|
|
var/list/types_to_check = list(fish.type)
|
|
if(fish.compatible_types)
|
|
types_to_check |= fish.compatible_types
|
|
types_to_mate_with = types_to_mate_with & types_to_check
|
|
|
|
for(var/obj/item/fish/fish_type as anything in types_to_mate_with)
|
|
var/list/type_fishes = tracked_fish_by_type[fish_type]
|
|
if(length(type_fishes) >= fish_type::stable_population)
|
|
continue
|
|
candidates += type_fishes
|
|
|
|
///Check if an offspring of two fish (or one if self-reproducing) can evolve.
|
|
/datum/component/aquarium/proc/check_evolution(atom/movable/source, obj/item/fish/fish, obj/item/fish/mate, datum/fish_evolution/evolution)
|
|
SIGNAL_HANDLER
|
|
//chances are halved if only one parent has this evolution.
|
|
var/real_probability = (mate && (evolution.type in mate.evolution_types)) ? evolution.probability : evolution.probability * 0.5
|
|
if(HAS_TRAIT(fish, TRAIT_FISH_MUTAGENIC) || (mate && HAS_TRAIT(mate, TRAIT_FISH_MUTAGENIC)))
|
|
real_probability *= 3
|
|
if(!prob(real_probability))
|
|
return NONE
|
|
if(!ISINRANGE(fluid_temp, evolution.required_temperature_min, evolution.required_temperature_max))
|
|
return NONE
|
|
return COMPONENT_ALLOW_EVOLUTION
|
|
|
|
/**
|
|
* Toggles a couple flags that determine if the fish is in safe waters so that we won't have to use signals or
|
|
* access this comp in multiple places just to confirm that.
|
|
*/
|
|
/datum/component/aquarium/proc/check_fluid_and_temperature(obj/item/fish/fish)
|
|
if((fluid_type in GLOB.fish_compatible_fluid_types[fish.required_fluid_type]) || (fluid_type == AQUARIUM_FLUID_AIR && HAS_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS)))
|
|
fish.fish_flags |= FISH_FLAG_SAFE_FLUID
|
|
else
|
|
fish.fish_flags &= ~FISH_FLAG_SAFE_FLUID
|
|
if(ISINRANGE(fluid_temp, fish.required_temperature_min, fish.required_temperature_max))
|
|
fish.fish_flags |= FISH_FLAG_SAFE_TEMPERATURE
|
|
else
|
|
fish.fish_flags &= ~FISH_FLAG_SAFE_TEMPERATURE
|
|
|
|
///Fish beauty changes when they're dead, so we need to update the beauty of the aquarium too.
|
|
/datum/component/aquarium/proc/on_fish_status_changed(obj/item/fish/fish)
|
|
get_content_beauty(fish)
|
|
if(current_mode == AQUARIUM_MODE_AUTO)
|
|
get_optimal_aquarium_settings()
|
|
|
|
/datum/component/aquarium/proc/update_aquarium_beauty(old_beauty)
|
|
if(QDELETED(parent))
|
|
return
|
|
old_beauty = clamp(old_beauty, MIN_AQUARIUM_BEAUTY, MAX_AQUARIUM_BEAUTY)
|
|
var/new_beauty = 0
|
|
for(var/key in beauty_by_content)
|
|
new_beauty += beauty_by_content[key]
|
|
new_beauty = clamp(new_beauty, MIN_AQUARIUM_BEAUTY, MAX_AQUARIUM_BEAUTY)
|
|
if(new_beauty == old_beauty)
|
|
return
|
|
if(old_beauty)
|
|
parent.RemoveElement(/datum/element/beauty, old_beauty)
|
|
if(new_beauty)
|
|
parent.AddElement(/datum/element/beauty, new_beauty)
|
|
|
|
///Remove a visual overlay from an aquarium_content comp
|
|
/datum/component/aquarium/proc/remove_visual(atom/movable/source, obj/effect/aquarium/visual)
|
|
SIGNAL_HANDLER
|
|
source.vis_contents -= visual
|
|
used_layers -= visual.layer
|
|
|
|
///set values for a visual overlay for an aquarium_content comp
|
|
/datum/component/aquarium/proc/set_visual(atom/movable/source, obj/effect/aquarium/visual)
|
|
SIGNAL_HANDLER
|
|
used_layers -= visual.layer
|
|
visual.layer = request_layer(visual.layer_mode)
|
|
visual.aquarium_zone_min_pw = aquarium_zone_min_pw
|
|
visual.aquarium_zone_max_pw = aquarium_zone_max_pw
|
|
visual.aquarium_zone_min_pz = aquarium_zone_min_pz
|
|
visual.aquarium_zone_max_pz = aquarium_zone_max_pz
|
|
visual.fluid_type = fluid_type
|
|
|
|
/datum/component/aquarium/proc/request_layer(layer_type)
|
|
var/atom/movable/movable = parent
|
|
switch(layer_type)
|
|
if(AQUARIUM_LAYER_MODE_BEHIND_GLASS)
|
|
return movable.layer + AQUARIUM_BELOW_GLASS_LAYER
|
|
if(AQUARIUM_LAYER_MODE_BOTTOM)
|
|
return movable.layer + AQUARIUM_MIN_OFFSET
|
|
if(AQUARIUM_LAYER_MODE_TOP)
|
|
return movable.layer + AQUARIUM_MAX_OFFSET
|
|
if(AQUARIUM_LAYER_MODE_AUTO)
|
|
var/chosen_layer = AQUARIUM_MIN_OFFSET + AQUARIUM_LAYER_STEP
|
|
while((chosen_layer in used_layers) && (chosen_layer <= AQUARIUM_MAX_OFFSET))
|
|
chosen_layer += AQUARIUM_LAYER_STEP
|
|
used_layers += chosen_layer
|
|
return movable.layer + chosen_layer
|
|
|
|
/datum/component/aquarium/proc/get_fishes()
|
|
var/list/fishes = list()
|
|
for(var/key in tracked_fish_by_type)
|
|
fishes += tracked_fish_by_type[key]
|
|
return fishes
|
|
|
|
/datum/component/aquarium/proc/is_fish_population_safe()
|
|
var/list/fishes = get_fishes()
|
|
var/alive = 0
|
|
var/not_safe = 0
|
|
for(var/obj/item/fish/fishie as anything in fishes)
|
|
if(fishie.status == FISH_DEAD)
|
|
continue
|
|
if(!fishie.proper_environment() || fishie.get_hunger() > FISH_STARVING_THRESHOLD * 0.9)
|
|
not_safe++
|
|
alive++
|
|
return not_safe <= (alive / 2)
|
|
|
|
/**
|
|
* Called if the mode is AQUARIUM_MODE_AUTO whenever a fish is added, removed, dies or resurrected.
|
|
*
|
|
* This tries to find what the best combination of fluid and temperature. is to ensure
|
|
* the survival of the highest number of fishes. It also can and will activate stasis if over half of
|
|
* the fish population is at risk of dying. It doesn't care if the fish is big or small, useful
|
|
* or not. All fish are equal before the impartial eye of AQUARIUM_MODE_AUTO.
|
|
*/
|
|
/datum/component/aquarium/proc/get_optimal_aquarium_settings()
|
|
if(!length(tracked_fish_by_type))
|
|
REMOVE_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
return
|
|
|
|
var/list/can_survive_with = list()
|
|
var/list/checked_types = list()
|
|
var/list/sorted_tracked = sortTim(tracked_fish_by_type.Copy(), GLOBAL_PROC_REF(cmp_list_len_dsc), associative = TRUE)
|
|
var/list/fish_alive_by_type = list()
|
|
for(var/obj/item/fish/fish_type as anything in sorted_tracked)
|
|
var/our_min_temp = fish_type::required_temperature_min
|
|
var/our_max_temp = fish_type::required_temperature_max
|
|
var/our_fluid_type = fish_type::required_fluid_type
|
|
|
|
checked_types += fish_type
|
|
LAZYINITLIST(can_survive_with[fish_type])
|
|
|
|
var/fish_amount = 0
|
|
for(var/obj/item/fish/fish_instance as anything in tracked_fish_by_type[fish_type])
|
|
if(fish_instance.status == FISH_ALIVE)
|
|
fish_amount++
|
|
|
|
if(!fish_amount) //all fishes of this type are dead, skip it.
|
|
continue
|
|
|
|
fish_alive_by_type[fish_type] = fish_amount
|
|
|
|
//Check if there's an overlap with the temp/fluid requirements of the other types.
|
|
for(var/obj/item/fish/enemy_type as anything in (tracked_fish_by_type - checked_types))
|
|
var/enem_min_temp = initial(enemy_type.required_temperature_min)
|
|
var/enem_max_temp = initial(enemy_type.required_temperature_max)
|
|
if(our_min_temp > enem_max_temp || our_max_temp < enem_min_temp)
|
|
continue
|
|
var/enem_fluid_type = initial(enemy_type.required_fluid_type)
|
|
if(!length(GLOB.fish_compatible_fluid_types[our_fluid_type] & GLOB.fish_compatible_fluid_types[enem_fluid_type]))
|
|
continue
|
|
can_survive_with[fish_type] |= enemy_type
|
|
LAZYOR(can_survive_with[enemy_type], fish_type)
|
|
|
|
var/list/highest_val_list = list(tracked_fish_by_type[1]) //In case there's only one type of fish in here...
|
|
var/highest_val = -1
|
|
for(var/fish_type in can_survive_with)
|
|
var/list/compatible_types = can_survive_with[fish_type]
|
|
var/fish_amount = fish_alive_by_type[fish_type]
|
|
for(var/obj/item/fish/fish_instance as anything in tracked_fish_by_type[fish_type])
|
|
if(fish_instance.status == FISH_ALIVE)
|
|
fish_amount++
|
|
for(var/ally_type in compatible_types)
|
|
var/val = fish_amount + fish_alive_by_type[ally_type]
|
|
var/list/common_allies = compatible_types & can_survive_with[ally_type]
|
|
for(var/third_type in common_allies)
|
|
val += fish_alive_by_type[third_type]
|
|
if(val <= highest_val)
|
|
continue
|
|
highest_val_list = list(fish_type, ally_type) + common_allies
|
|
highest_val = val
|
|
|
|
var/min_temp = MIN_AQUARIUM_TEMP
|
|
var/max_temp = MAX_AQUARIUM_TEMP
|
|
var/list/possible_fluids = fluid_types.Copy()
|
|
for(var/obj/item/fish/chosen_type as anything in highest_val_list)
|
|
possible_fluids &= GLOB.fish_compatible_fluid_types[chosen_type::required_fluid_type]
|
|
min_temp = max(min_temp, chosen_type::required_temperature_min)
|
|
max_temp = min(max_temp, chosen_type::required_temperature_max)
|
|
|
|
fluid_temp = clamp(min_temp + round((max_temp - min_temp) / 2), min_fluid_temp, max_fluid_temp)
|
|
set_fluid_type(possible_fluids[1])
|
|
|
|
if(!is_fish_population_safe())
|
|
ADD_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
else
|
|
REMOVE_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
|
|
/datum/component/aquarium/proc/interact(atom/movable/source, mob/user)
|
|
SIGNAL_HANDLER
|
|
if(HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN))
|
|
INVOKE_ASYNC(src, PROC_REF(ui_interact), user)
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(admire), source, user)
|
|
|
|
/datum/component/aquarium/proc/secondary_interact(atom/movable/source, mob/user)
|
|
SIGNAL_HANDLER
|
|
if(HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN))
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(admire), source, user)
|
|
|
|
/datum/component/aquarium/proc/on_secondary_attack_hand(obj/item/source, mob/living/user)
|
|
SIGNAL_HANDLER
|
|
INVOKE_ASYNC(src, PROC_REF(admire), source, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
/datum/component/aquarium/ui_interact(mob/user, datum/tgui/ui)
|
|
ui = SStgui.try_update_ui(user, src, ui)
|
|
if(!ui)
|
|
var/atom/movable/movable = parent
|
|
ui = new(user, src, "Aquarium", movable.name)
|
|
ui.open()
|
|
|
|
/datum/component/aquarium/ui_data(mob/user)
|
|
. = ..()
|
|
var/atom/movable/aquarium = parent
|
|
.["fluidType"] = fluid_type
|
|
.["temperature"] = fluid_temp
|
|
.["lockedFluidTemp"] = current_mode == AQUARIUM_MODE_AUTO
|
|
.["currentMode"] = current_mode
|
|
.["currentTooltip"] = aquarium_modes[current_mode]
|
|
.["fishData"] = list()
|
|
.["feedingInterval"] = feeding_interval / (1 MINUTES)
|
|
.["propData"] = list()
|
|
for(var/atom/movable/item as anything in aquarium.contents)
|
|
if(isfish(item))
|
|
var/obj/item/fish/fish = item
|
|
.["fishData"] += list(list(
|
|
"fish_ref" = REF(fish),
|
|
"fish_name" = uppertext(fish.name),
|
|
"fish_happiness" = fish.get_happiness_value(),
|
|
"fish_icon" = fish::icon,
|
|
"fish_icon_state" = fish::icon_state,
|
|
"fish_alive" = fish.status == FISH_ALIVE,
|
|
))
|
|
continue
|
|
.["propData"] += list(list(
|
|
"prop_ref" = REF(item),
|
|
"prop_name" = item.name,
|
|
"prop_icon" = item::icon,
|
|
"prop_icon_state" = item::icon_state,
|
|
))
|
|
|
|
/datum/component/aquarium/ui_static_data(mob/user)
|
|
. = ..()
|
|
//I guess these should depend on the fluid so lava critters can get high or stuff below water freezing point but let's keep it simple for now.
|
|
.["minTemperature"] = min_fluid_temp
|
|
.["maxTemperature"] = max_fluid_temp
|
|
.["fluidTypes"] = fluid_types
|
|
.["heartIcon"] = 'icons/effects/effects.dmi'
|
|
var/list/modes_no_assoc = list() //the UI dropdown won't work with assoc lists here
|
|
for(var/mode in aquarium_modes)
|
|
modes_no_assoc += mode
|
|
.["aquariumModes"] = modes_no_assoc
|
|
|
|
/datum/component/aquarium/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
|
. = ..()
|
|
if(.)
|
|
return
|
|
var/mob/user = usr
|
|
var/atom/movable/movable = parent
|
|
switch(action)
|
|
if("temperature")
|
|
var/temperature = params["temperature"]
|
|
if(isnum(temperature) && temperature != fluid_temp)
|
|
fluid_temp = clamp(temperature, min_fluid_temp, max_fluid_temp)
|
|
for(var/obj/item/fish/fish as anything in get_fishes())
|
|
check_fluid_and_temperature(fish)
|
|
. = TRUE
|
|
if("fluid")
|
|
if(params["fluid"] != fluid_type && (params["fluid"] in fluid_types))
|
|
set_fluid_type(params["fluid"])
|
|
. = TRUE
|
|
if("change_mode")
|
|
var/new_mode = params["new_mode"]
|
|
set_aquarium_mode(new_mode)
|
|
. = TRUE
|
|
if("feeding_interval")
|
|
feeding_interval = params["feeding_interval"] MINUTES
|
|
. = TRUE
|
|
if("pet_fish")
|
|
var/obj/item/fish/fish = locate(params["fish_reference"]) in movable.contents
|
|
fish?.pet_fish(user)
|
|
if("remove_item")
|
|
var/atom/movable/item = locate(params["item_reference"]) in movable.contents
|
|
item?.forceMove(movable.drop_location())
|
|
to_chat(user, span_notice("You take out [item] from [movable]."))
|
|
if("rename_fish")
|
|
var/new_name = sanitize_name(params["chosen_name"])
|
|
var/atom/movable/fish = locate(params["fish_reference"]) in movable.contents
|
|
if(!fish || !new_name || new_name == fish.name)
|
|
return
|
|
fish.AddComponent(/datum/component/rename, new_name, fish.desc)
|
|
|
|
/datum/component/aquarium/proc/set_aquarium_mode(new_mode)
|
|
if(new_mode == current_mode)
|
|
return
|
|
REMOVE_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
current_mode = new_mode
|
|
switch(current_mode)
|
|
if(AQUARIUM_MODE_SAFE)
|
|
ADD_TRAIT(parent, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)
|
|
if(AQUARIUM_MODE_AUTO)
|
|
get_optimal_aquarium_settings()
|
|
|
|
/datum/component/aquarium/proc/set_fluid_type(new_fluid_type)
|
|
var/atom/movable/movable = parent
|
|
fluid_type = new_fluid_type
|
|
SEND_SIGNAL(movable, COMSIG_AQUARIUM_FLUID_CHANGED, fluid_type)
|
|
for(var/obj/item/fish/fish as anything in get_fishes())
|
|
check_fluid_and_temperature(fish)
|
|
|
|
/datum/component/aquarium/proc/admire(atom/movable/source, mob/living/user)
|
|
source.balloon_alert(user, "admiring aquarium...")
|
|
if(!do_after(user, 5 SECONDS, target = source))
|
|
return
|
|
var/alive_fish = 0
|
|
var/dead_fish = 0
|
|
for(var/obj/item/fish/fish as anything in get_fishes())
|
|
if(fish.status == FISH_ALIVE)
|
|
alive_fish++
|
|
else
|
|
dead_fish++
|
|
|
|
var/morb = HAS_MIND_TRAIT(user, TRAIT_MORBID)
|
|
//Check if there are live fish - good mood
|
|
//All fish dead - bad mood.
|
|
//No fish - nothing.
|
|
if(alive_fish > 0)
|
|
user.add_mood_event("aquarium", morb ? /datum/mood_event/morbid_aquarium_bad : /datum/mood_event/aquarium_positive)
|
|
else if(dead_fish > 0)
|
|
user.add_mood_event("aquarium", morb ? /datum/mood_event/morbid_aquarium_good : /datum/mood_event/aquarium_negative)
|
|
|
|
/datum/component/aquarium/proc/on_requesting_context_from_item(atom/source, list/context, obj/item/held_item, mob/user)
|
|
SIGNAL_HANDLER
|
|
var/open_panel = HAS_TRAIT(source, TRAIT_AQUARIUM_PANEL_OPEN)
|
|
var/is_held_item = (held_item == source)
|
|
if(!held_item || is_held_item)
|
|
var/isitem = isitem(source)
|
|
if(!isitem || is_held_item)
|
|
context[SCREENTIP_CONTEXT_LMB] = open_panel ? "Adjust settings" : "Admire"
|
|
if(isitem)
|
|
context[SCREENTIP_CONTEXT_RMB] = "Admire"
|
|
context[SCREENTIP_CONTEXT_ALT_LMB] = "[!open_panel ? "Open" : "Close"] settings panel"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
if(istype(held_item, /obj/item/plunger))
|
|
context[SCREENTIP_CONTEXT_LMB] = "Empty feed storage"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
if(istype(held_item, /obj/item/reagent_containers/cup/fish_feed) && (!source.reagents || !open_panel))
|
|
context[SCREENTIP_CONTEXT_LMB] = "Feed fishes"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
if(HAS_TRAIT(held_item, TRAIT_AQUARIUM_CONTENT))
|
|
context[SCREENTIP_CONTEXT_LMB] = "Insert in aquarium"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
#undef MIN_AQUARIUM_BEAUTY
|
|
#undef MAX_AQUARIUM_BEAUTY
|