Files
Bubberstation/code/datums/components/aquarium.dm
Ghom b48b717730 Fish analyzer UI is a bit more reactive, also fixing a couple things with fish reproduction (#93461)
## 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.
/🆑
2025-10-23 13:45:49 +13:00

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