From b48b717730fdc64fbacc0b0e72cd8c643f57b916 Mon Sep 17 00:00:00 2001 From: Ghom <42542238+Ghommie@users.noreply.github.com> Date: Thu, 23 Oct 2025 02:45:49 +0200 Subject: [PATCH] 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 :cl: 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. /:cl: --- code/__DEFINES/fish.dm | 6 +- code/datums/components/aquarium.dm | 2 +- .../modules/fishing/aquarium/fish_analyzer.dm | 80 ++++++++++++++++--- code/modules/fishing/fish/_fish.dm | 43 +++++++--- code/modules/fishing/fish/fish_traits.dm | 2 +- code/modules/fishing/fish/types/air_space.dm | 1 + code/modules/fishing/fish/types/mining.dm | 1 + code/modules/fishing/fish/types/rift.dm | 2 +- code/modules/fishing/fish/types/ruins.dm | 2 +- code/modules/unit_tests/focus_only_tests.dm | 7 ++ 10 files changed, 116 insertions(+), 30 deletions(-) diff --git a/code/__DEFINES/fish.dm b/code/__DEFINES/fish.dm index 825315e24b8..48a914482fc 100644 --- a/code/__DEFINES/fish.dm +++ b/code/__DEFINES/fish.dm @@ -182,9 +182,9 @@ #define GET_FISH_WEIGHT_RANK(weighty) max(round(1 + log(2, max(weighty/FISH_WEIGHT_FORCE_DIVISOR, 1)), 1), 1) ///The breeding timeout for newly instantiated fish is multiplied by this. -#define NEW_FISH_BREEDING_TIMEOUT_MULT 2 -///The last feeding timestamp of newly instantiated fish is multiplied by this: ergo, they spawn 50% hungry. -#define NEW_FISH_LAST_FEEDING_MULT 0.33 +#define NEW_FISH_BREEDING_TIMEOUT_MULT 0.6 +///The last feeding timestamp of newly instantiated fish is multiplied by this. +#define NEW_FISH_LAST_FEEDING_MULT 0.33 //Their hunger meter starts at 33% ///If get_hunger is above this value, the fish is considered to be starving and will slowly lose health because of it #define FISH_STARVING_THRESHOLD 1 diff --git a/code/datums/components/aquarium.dm b/code/datums/components/aquarium.dm index 314b34bfe13..f9ba8b5164e 100644 --- a/code/datums/components/aquarium.dm +++ b/code/datums/components/aquarium.dm @@ -388,7 +388,7 @@ 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) >= initial(fish_type.stable_population)) + if(length(type_fishes) >= fish_type::stable_population) continue candidates += type_fishes diff --git a/code/modules/fishing/aquarium/fish_analyzer.dm b/code/modules/fishing/aquarium/fish_analyzer.dm index a89ac1456a6..35b01e1edc9 100644 --- a/code/modules/fishing/aquarium/fish_analyzer.dm +++ b/code/modules/fishing/aquarium/fish_analyzer.dm @@ -21,8 +21,8 @@ greyscale_config_worn = /datum/greyscale_config/fish_analyzer_worn ///The color of the case. Used by grayscale configs and update_overlays() var/case_color - ///the item we have scanned - var/datum/weakref/scanned_item + ///the atom (aquarium or fish) we have scanned + var/atom/scanned_object /obj/item/fish_analyzer/Initialize(mapload) case_color = rgb(rand(16, 255), rand(16, 255), rand(16, 255)) @@ -43,6 +43,10 @@ update_appearance() AddComponent(/datum/component/adjust_fishing_difficulty, -3, ITEM_SLOT_HANDS) +/obj/item/fish_analyzer/Destroy() + scanned_object = null + return ..() + /obj/item/fish_analyzer/examine(mob/user) . = ..() . += span_notice("Alt-Click to access the Experiment Configuration UI") @@ -64,38 +68,82 @@ if(!user.can_read(src) || user.is_blind()) return ITEM_INTERACT_BLOCKING - scanned_item = WEAKREF(target) SEND_SIGNAL(src, COMSIG_FISH_ANALYZER_ANALYZE_STATUS, target, user) + if(target != scanned_object) + unregister_scanned() + register_scanned(target) + update_static_data_for_all_viewers() ui_interact(user) return ITEM_INTERACT_SUCCESS +/obj/item/fish_analyzer/proc/register_scanned(atom/target) + scanned_object = target + RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_deleted)) + if(HAS_TRAIT(target, TRAIT_IS_AQUARIUM)) + RegisterSignals(target, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, COMSIG_ATOM_EXITED), PROC_REF(aquarium_update)) + +/obj/item/fish_analyzer/proc/unregister_scanned() + if(!scanned_object) + return + UnregisterSignal(scanned_object, list(COMSIG_QDELETING, COMSIG_ATOM_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, COMSIG_ATOM_EXITED)) + scanned_object = null + +/obj/item/fish_analyzer/proc/on_target_deleted() + SIGNAL_HANDLER + unregister_scanned() + +/obj/item/fish_analyzer/proc/aquarium_update() + SIGNAL_HANDLER + //update static data on the next tick. to give time to offsprings to properly inherit the parents' traits and all. + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, update_static_data_for_all_viewers)), 0) + /obj/item/fish_analyzer/ui_interact(mob/user, datum/tgui/ui) - if(isnull(scanned_item?.resolve())) + if(isnull(scanned_object)) balloon_alert(user, "no specimen data!") return TRUE + if(!(scanned_object in view(7, get_turf(src)))) + balloon_alert(user, "specimen data lost!") + return TRUE ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "FishAnalyzer") ui.open() +/obj/item/fish_analyzer/ui_status(mob/user, datum/ui_state/state) + if(!scanned_object || !(scanned_object in view(7, get_turf(src)))) + balloon_alert(user, "specimen data lost!") + unregister_scanned() + return UI_CLOSE + return ..() + /obj/item/fish_analyzer/ui_static_data(mob/user) var/list/data = list() - var/atom/scanned_object = scanned_item?.resolve() data["fish_list"] = list() data["fish_scanned"] = FALSE if(isfish(scanned_object)) data["fish_scanned"] = TRUE + return extract_static_fish_info(data, scanned_object) + + for(var/obj/item/fish/fishie in scanned_object) + extract_static_fish_info(data, fishie) + + return data + +/obj/item/fish_analyzer/ui_data(mob/user) + var/list/data = list() + data["fish_list"] = list() + + if(isfish(scanned_object)) return extract_fish_info(data, scanned_object) - var/atom/movable/aquarium = scanned_object - for(var/obj/item/fish/fishie in aquarium) + for(var/obj/item/fish/fishie in scanned_object) extract_fish_info(data, fishie) return data -/obj/item/fish_analyzer/proc/extract_fish_info(list/data, obj/item/fish/fishie) +/obj/item/fish_analyzer/proc/extract_static_fish_info(list/data, obj/item/fish/fishie) var/list/fish_traits = list() var/list/fish_evolutions = list() @@ -118,20 +166,26 @@ "fish_name" = fishie.name, "fish_icon" = fishie.icon, "fish_icon_state" = fishie.base_icon_state, - "fish_health" = fishie.status == FISH_DEAD ? 0 : PERCENT(fishie.get_health_percentage()), - "fish_size" = fishie.size, - "fish_weight" = fishie.weight, "fish_food" = fishie.food.name, "fish_food_color" = fishie.food::color, "fish_min_temp" = fishie.required_temperature_min, "fish_max_temp" = fishie.required_temperature_max, - "fish_hunger" = HAS_TRAIT(fishie, TRAIT_FISH_NO_HUNGER) ? 0 : 1 - fishie.get_hunger(), "fish_fluid_compatible" = fishie.fish_flags & FISH_FLAG_SAFE_FLUID, "fish_fluid_type" = fishie.required_fluid_type, - "fish_breed_timer" = round(max(fishie.breeding_wait - world.time, 0) / 10), "fish_traits" = fish_traits, "fish_evolutions" = fish_evolutions, "fish_suitable_temp" = fishie.fish_flags & FISH_FLAG_SAFE_TEMPERATURE, )) return data + +/obj/item/fish_analyzer/proc/extract_fish_info(list/data, obj/item/fish/fishie) + data["fish_list"] += list(list( + "fish_health" = fishie.status == FISH_DEAD ? 0 : PERCENT(fishie.get_health_percentage()), + "fish_size" = fishie.size, + "fish_weight" = fishie.weight, + "fish_hunger" = HAS_TRAIT(fishie, TRAIT_FISH_NO_HUNGER) ? 0 : 1 - fishie.get_hunger(), + "fish_breed_timer" = round(max(fishie.breeding_wait - world.time, 0) / 10), + )) + + return data diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm index beddb14125c..04c7434ce99 100644 --- a/code/modules/fishing/fish/_fish.dm +++ b/code/modules/fishing/fish/_fish.dm @@ -19,6 +19,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( /obj/item/fish name = "fish" desc = "very bland" + abstract_type = /obj/item/fish icon = 'icons/obj/aquarium/fish.dmi' lefthand_file = 'icons/mob/inhands/fish_lefthand.dmi' righthand_file = 'icons/mob/inhands/fish_righthand.dmi' @@ -220,7 +221,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( //Adding this because not all fish have the gore foodtype that makes them automatically eligible for dna infusion. add_traits(list(TRAIT_DUCT_TAPE_UNREPAIRABLE, TRAIT_VALID_DNA_INFUSION), INNATE_TRAIT) - //stops new fish from being able to reproduce right away. + //new fish should be modestly hungry and cannot reproduce right away. breeding_wait = world.time + (breeding_timeout * NEW_FISH_BREEDING_TIMEOUT_MULT) last_feeding = world.time - (feeding_frequency * NEW_FISH_LAST_FEEDING_MULT) @@ -231,6 +232,23 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( register_context() register_item_context() + if(!PERFORM_ALL_TESTS(focus_only/fish_population) || type == abstract_type || stable_population > 1) + return + if(length(compatible_types)) + for(var/obj/item/fish/fishie as anything in compatible_types) + if(fishie::stable_population > 1) + if(!HAS_TRAIT(src, TRAIT_FISH_RECESSIVE)) + stack_trace("[type] has a stable population of [stable_population] but is compatible with fishes with a higher stable population. \ + However, it doesn't have the [/datum/fish_trait/recessive] trait. Either increase its stable population or add the trait to it.") + return + if(!HAS_TRAIT(src, TRAIT_FISH_NO_MATING) && !HAS_TRAIT(src, TRAIT_FISH_CROSSBREEDER)) + var/list/pick_one = list( + /datum/fish_trait/crossbreeder, + /datum/fish_trait/no_mating, + ) + stack_trace("[type] has a stable_population of [stable_population] but has neither of these traits: [english_list(pick_one)]. \ + Either increase its stable population or add one of these traits to it.") + /obj/item/fish/suicide_act(mob/living/user) if(force == 0) user.visible_message(span_suicide("[user] slaps [user.p_them()]self with [src], but nothing happens!")) @@ -1274,7 +1292,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( return FALSE if(!being_targeted && length(get_aquarium_fishes()) >= AQUARIUM_MAX_BREEDING_POPULATION) return FALSE - return !HAS_TRAIT(loc, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH) && get_health_percentage() >= 0.8 && stable_population >= 1 && world.time >= breeding_wait + return !HAS_TRAIT(loc, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH) && get_health_percentage() >= 0.8 && world.time >= breeding_wait /obj/item/fish/proc/try_to_reproduce() if(!loc || !HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) @@ -1286,19 +1304,17 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( if(!HAS_TRAIT(src, TRAIT_FISH_NO_MATING)) var/list/available_fishes = list() SEND_SIGNAL(loc, COMSIG_AQUARIUM_GET_REPRODUCTION_CANDIDATES, src, available_fishes) + available_fishes -= src //self-reproduction goes last if(length(available_fishes)) - //make sure we check if the fish can reproduce with itself last, since that should've lower priority - available_fishes = shuffle(available_fishes) - src - available_fishes += src + available_fishes = shuffle(available_fishes) for(var/obj/item/fish/other_fish as anything in available_fishes) if(other_fish.ready_to_reproduce(TRUE)) second_fish = other_fish break - if(!second_fish || second_fish == src) //check if the fish can self-reproduce in these cases. - if(!HAS_TRAIT(src, TRAIT_FISH_SELF_REPRODUCE)) - return FALSE - second_fish = null //set it to null, since this will make the following operations a bit easier + //check if the fish can self-reproduce if there's no other option + if(!second_fish && !HAS_TRAIT(src, TRAIT_FISH_SELF_REPRODUCE)) + return FALSE if(PERFORM_ALL_TESTS(fish_breeding) && second_fish && !length(evolution_types)) return create_offspring(second_fish.type, second_fish) @@ -1342,6 +1358,9 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( return create_offspring(chosen_type, second_fish, chosen_evolution) +///The timeout multiplier for offspring fish, the ones generated when two compatible fish are coupled +#define OFFSPRING_FISH_BREEDING_TIMEOUT_MULT 2 + /obj/item/fish/proc/create_offspring(chosen_type, obj/item/fish/partner, datum/fish_evolution/evolution) var/obj/item/fish/new_fish = new chosen_type (loc, FALSE) //Try to pass down compatible traits based on inheritability @@ -1356,7 +1375,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( var/ratio_weight = new_fish.average_size * (((weight / average_weight) + (partner.weight / partner.average_weight)) / 2) var/mean_weight = (weight + partner.weight)/2 new_fish.randomize_size_and_weight((mean_size + ratio_size) * 0.5, (mean_weight + ratio_weight) * 0.5, 0.3, update = FALSE) - partner.breeding_wait = world.time + breeding_timeout + partner.breeding_wait = world.time + partner.breeding_timeout if(length(partner.custom_materials)) if(length(custom_materials)) @@ -1383,8 +1402,12 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( breeding_wait = world.time + breeding_timeout + new_fish.breeding_wait = world.time + new_fish.breeding_timeout * OFFSPRING_FISH_BREEDING_TIMEOUT_MULT + return new_fish +#undef OFFSPRING_FISH_BREEDING_TIMEOUT_MULT + #define PAUSE_BETWEEN_PHASES 15 #define PAUSE_BETWEEN_FLOPS 2 #define FLOP_COUNT 2 diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm index e5ea3d97807..40cad34c20a 100644 --- a/code/modules/fishing/fish/fish_traits.dm +++ b/code/modules/fishing/fish/fish_traits.dm @@ -385,7 +385,7 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits()) catalog_description = "If crossbred, offsprings will always be of the mate species, unless it also possess the trait." inheritability = 0 -/datum/fish_trait/no_mating/apply_to_fish(obj/item/fish/fish) +/datum/fish_trait/recessive/apply_to_fish(obj/item/fish/fish) . = ..() ADD_TRAIT(fish, TRAIT_FISH_RECESSIVE, FISH_TRAIT_DATUM) diff --git a/code/modules/fishing/fish/types/air_space.dm b/code/modules/fishing/fish/types/air_space.dm index 82587fca6fc..264d883eddd 100644 --- a/code/modules/fishing/fish/types/air_space.dm +++ b/code/modules/fishing/fish/types/air_space.dm @@ -29,6 +29,7 @@ average_size = 60 average_weight = 1000 weight_size_deviation = 0.1 + stable_population = 5 required_fluid_type = AQUARIUM_FLUID_SALTWATER required_temperature_min = MIN_AQUARIUM_TEMP+20 required_temperature_max = MIN_AQUARIUM_TEMP+40 diff --git a/code/modules/fishing/fish/types/mining.dm b/code/modules/fishing/fish/types/mining.dm index 4a701dd72cc..14e5ce9810b 100644 --- a/code/modules/fishing/fish/types/mining.dm +++ b/code/modules/fishing/fish/types/mining.dm @@ -182,6 +182,7 @@ /datum/fish_trait/carnivore, /datum/fish_trait/heavy, ) + stable_population = 5 compatible_types = list(/obj/item/fish/lavaloop/plasma_river) evolution_types = list(/datum/fish_evolution/plasmaloop) hitsound = null diff --git a/code/modules/fishing/fish/types/rift.dm b/code/modules/fishing/fish/types/rift.dm index 530a32ced23..c73f1fc2cb5 100644 --- a/code/modules/fishing/fish/types/rift.dm +++ b/code/modules/fishing/fish/types/rift.dm @@ -454,7 +454,7 @@ max_pressure = INFINITY safe_air_limits = list() fillet_type = /obj/item/food/badrecipe/moldy/bacteria - stable_population = 0 + stable_population = 2 /obj/item/fish/mossglob/Initialize(mapload, apply_qualities) . = ..() diff --git a/code/modules/fishing/fish/types/ruins.dm b/code/modules/fishing/fish/types/ruins.dm index 83d890ea991..8170fe69062 100644 --- a/code/modules/fishing/fish/types/ruins.dm +++ b/code/modules/fishing/fish/types/ruins.dm @@ -15,7 +15,7 @@ required_fluid_type = AQUARIUM_FLUID_ANY_WATER min_pressure = HAZARD_LOW_PRESSURE max_integrity = 600 - stable_population = 1 //This means they can only crossbreed. + stable_population = 2 grind_results = list(/datum/reagent/bone_dust = 5, /datum/reagent/consumable/liquidgibs = 5) fillet_type = /obj/item/stack/sheet/bone num_fillets = 2 diff --git a/code/modules/unit_tests/focus_only_tests.dm b/code/modules/unit_tests/focus_only_tests.dm index cc3eec42669..40d0845c1a6 100644 --- a/code/modules/unit_tests/focus_only_tests.dm +++ b/code/modules/unit_tests/focus_only_tests.dm @@ -48,6 +48,13 @@ /// Checks that the contents of the fish_counts list are also present in fish_table /datum/unit_test/focus_only/fish_sources_tables +/** + * This will tell us if we forgot to properly set the stable population for any fish. + * Fish with a stable population of one or less need to have either the mateless or crossbreeder trait, or be compatible with + * other fish types with a higher stable population. + */ +/datum/unit_test/focus_only/fish_population + /// Checks that maploaded mobs with either the `atmos_requirements` or `body_temp_sensitive` /datum/unit_test/focus_only/atmos_and_temp_requirements