#define FISH_SAD 0 #define FISH_VERY_HAPPY 4 #define GET_FISH_ELECTROGENESIS(fish) (fish.electrogenesis_power * fish.size * 0.1) #define FISH_SUBMERGING_THRESHOLD 100 SECONDS #define STARVING_FISH_SUBMERGING_THRESHOLD 20 SECONDS GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( AQUARIUM_FLUID_ANY_WATER = list(AQUARIUM_FLUID_SALTWATER, AQUARIUM_FLUID_FRESHWATER, AQUARIUM_FLUID_SULPHWATEVER), AQUARIUM_FLUID_ANADROMOUS = list(AQUARIUM_FLUID_SALTWATER, AQUARIUM_FLUID_FRESHWATER), AQUARIUM_FLUID_SALTWATER = list(AQUARIUM_FLUID_SALTWATER), AQUARIUM_FLUID_FRESHWATER = list(AQUARIUM_FLUID_FRESHWATER), AQUARIUM_FLUID_SULPHWATEVER = list(AQUARIUM_FLUID_SULPHWATEVER), AQUARIUM_FLUID_AIR = list(AQUARIUM_FLUID_AIR), )) // Fish path used for autogenerated fish /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' icon_angle = 180 force = 6 throwforce = 6 throw_range = 8 attack_verb_continuous = list("slaps", "whacks") attack_verb_simple = list("slap", "whack") hitsound = SFX_DEFAULT_FISH_SLAP drop_sound = 'sound/mobs/non-humanoids/fish/fish_drop1.ogg' pickup_sound = SFX_FISH_PICKUP sound_vary = TRUE obj_flags = UNIQUE_RENAME item_flags = SLOWS_WHILE_IN_HAND //we handle slowdowns internally, and the fish weight modifier from materials already contributes to it. material_flags = MATERIAL_EFFECTS|MATERIAL_AFFECT_STATISTICS|MATERIAL_COLOR|MATERIAL_ADD_PREFIX|MATERIAL_NO_SLOWDOWN|MATERIAL_NO_EDIBILITY max_integrity = 200 integrity_failure = 0.5 /// Flags for fish variables that would otherwise be TRUE/FALSE var/fish_flags = FISH_FLAG_SHOW_IN_CATALOG|FISH_DO_FLOP_ANIM|FISH_FLAG_EXPERIMENT_SCANNABLE /// width of aquarium visual icon var/sprite_width /// height of aquarium visual icon var/sprite_height ///this icon file will be used for in-aquarium visual for the fish var/dedicated_in_aquarium_icon = 'icons/obj/aquarium/fish.dmi' /** * The icon_state that will be used for in-aquarium visual for the fish * If not set, "[initial(icon_state)]_small" will be used instead */ var/dedicated_in_aquarium_icon_state /// If present aquarium visual will be of this color var/aquarium_vc_color /// Required fluid type for this fish to live. var/required_fluid_type = AQUARIUM_FLUID_FRESHWATER /// Required minimum temperature for the fish to live. var/required_temperature_min = MIN_AQUARIUM_TEMP /// Maximum possible temperature for the fish to live. var/required_temperature_max = MAX_AQUARIUM_TEMP /// What type of reagent this fish needs to be fed. var/datum/reagent/food = /datum/reagent/consumable/nutriment /// How often the fish needs to be fed var/feeding_frequency = 5 MINUTES /// Time of last the fish was fed var/last_feeding /// Fish status var/status = FISH_ALIVE ///icon used when the fish is dead, ifset. var/icon_state_dead /// The message shown when the fish dies. var/death_text = "%SRC dies." /// How rare this fish is in the random cases var/random_case_rarity = FISH_RARITY_BASIC /// Fish autogenerated from this behaviour will be processable into this var/fillet_type = /obj/item/food/fishmeat /// number of fillets given by the fish. It scales with its size. var/num_fillets = 2 //BUBBERSTATION CHANGE: BASE 1 TO 2. /// Won't breed more than this amount in single aquarium. var/stable_population = 1 /// The time limit before new fish can be created var/breeding_wait /// How long it takes to produce new fish var/breeding_timeout = 2 MINUTES /// If set, the fish can also breed with these fishes types var/list/compatible_types /// If set, when procreating these are the types of fish that will be generate instead of 'type' var/list/spawn_types /// A list of possible evolutions. If set, offsprings may be of a different, new fish type if conditions are met. var/list/evolution_types // Fishing related properties /** * List of fish trait types, these may modify probabilty/difficulty depending on rod/user properties * or dictate how the fish behaves or some of its qualities. */ var/list/fish_traits = list() /// path to datums that dictate how the fish moves during the fishing minigame var/fish_movement_type = /datum/fish_movement /// Base additive modifier to fishing difficulty var/fishing_difficulty_modifier = 0 /** * Bait identifiers that make catching this fish easier and more likely * Bait identifiers: Path | Trait | list("Type"="Foodtype","Value"= Food Type Flag like [MEAT]) */ var/list/favorite_bait = list() /** * Bait identifiers that make catching this fish harder and less likely * Bait identifiers: Path | Trait | list("Type"="Foodtype","Value"= Food Type Flag like [MEAT]) */ var/list/disliked_bait = list() /// Size in centimeters. Null until update_size_and_weight is called. Number of fillets and w_class scale with it. var/size /// Average size for this fish type in centimeters. Will be used as gaussian distribution with 20% deviation for fishing, bought fish are always standard size var/average_size = 50 /// Temporarily stores the new size of the fish from randomize_size_and_weight() to be used by update_size_weight() later, so that it can be deferred. var/temp_size /// The maximum size this fish can reach, calculated the first time update_size_and_weight() is called. var/maximum_size /// Weight in "kiloclam". Null until update_size_and_weight is called. Grind results scale with it. Don't think too hard how a trout could fit in a blender. var/weight /// Average weight for this fish type in "kiloclam" var/average_weight = 1000 /// Temporarily stores the new weight of the fish from randomize_size_and_weight() to be used by update_size_weight() later, so that it can be deferred. var/temp_weight /// The maximum weight this fish can reach, calculated the first time update_size_and_weight() is called. var/maximum_weight /// Stores the current total weight modifier from materials. var/material_weight_mult = 1 ///The general deviation from the average weight and size this fish has in the wild var/weight_size_deviation = 0.2 /// When outside of an aquarium, these gases that are checked (as well as pressure and temp) to assert if the environment is safe or not. var/list/safe_air_limits = list( /datum/gas/oxygen = list(12, 100), /datum/gas/nitrogen, /datum/gas/carbon_dioxide = list(0, 10), /datum/gas/water_vapor, ) /// Outside of an aquarium, the pressure needs to be within these two variables for the environment to be safe. var/min_pressure = WARNING_LOW_PRESSURE var/max_pressure = HAZARD_HIGH_PRESSURE /// cooldown on creating tesla zaps COOLDOWN_DECLARE(electrogenesis_cooldown) /// power of the tesla zap created by the fish in a bioelectric generator. Scales with size. var/electrogenesis_power = 2 MEGA JOULES /// The beauty this fish provides to the aquarium or mount it's inserted in. var/beauty = FISH_BEAUTY_GENERIC /// Set and used by trophy mounts, this one is for the name of who mounted it (might actually not be the catcher but w/e) var/catcher_name /// Set and used by trophy mounts, this is for the day of when it was first mounted var/catch_date /** * If you wonder why this isn't being tracked by the edible component instead: * We reset the this value when revived, and slowly chip it away as we heal. * Of course, it would be daunting to get this to be handled by the edible component * given its complexity. */ var/bites_amount = 0 /** * An identifier for this fish used to track progress for fish caught between rounds in * a way that's resilient to repathing (and removing paths). Only catchable fish need it. * Once set, the value shouldn't be changed, so don't make typos. */ var/fish_id ///Used to redirect to another fish path so that catching this fish unlocks its entry instead. var/obj/item/fish/fish_id_redirect_path /// only used in the suicide for comedic value var/suicide_slap_text = "*SLAP!*" var/time_passed_on_safe_turf = 0 /obj/item/fish/Initialize(mapload, apply_qualities = TRUE) . = ..() base_icon_state = icon_state //It's important that we register the signals before the component is attached. RegisterSignal(src, COMSIG_AQUARIUM_CONTENT_DO_ANIMATION, PROC_REF(update_aquarium_animation)) RegisterSignal(src, COMSIG_AQUARIUM_CONTENT_RANDOMIZE_POSITION, PROC_REF(randomize_aquarium_position)) RegisterSignal(src, COMSIG_AQUARIUM_CONTENT_GENERATE_APPEARANCE, PROC_REF(update_aquarium_appearance)) AddComponent(/datum/component/aquarium_content, list(COMSIG_ATOM_WAS_ATTACKED)) RegisterSignal(src, COMSIG_MOVABLE_GET_AQUARIUM_BEAUTY, PROC_REF(get_aquarium_beauty)) RegisterSignal(src, COMSIG_ATOM_ON_LAZARUS_INJECTOR, PROC_REF(use_lazarus)) if(fish_flags & FISH_DO_FLOP_ANIM) RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, PROC_REF(on_temp_animation)) check_flopping() if(status != FISH_DEAD) ADD_TRAIT(src, TRAIT_UNCOMPOSTABLE, REF(src)) //Compost fish only when it's dead. START_PROCESSING(SSobj, src) RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_FISH_STASIS), PROC_REF(enter_stasis)) RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_FISH_STASIS), PROC_REF(exit_stasis)) //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) //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) if(apply_qualities) apply_traits() //Make sure traits are applied before size and weight. update_size_and_weight() register_context() register_item_context() if(!apply_qualities || !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/grind_results() SHOULD_NOT_OVERRIDE(TRUE) var/list/grind_results = fish_grind_results() for(var/reagent_type in grind_results) grind_results[reagent_type] *= max(FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1), 0.1) return grind_results /obj/item/fish/proc/fish_grind_results() RETURN_TYPE(/list/datum/reagent) return list() /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!")) return SHAME user.visible_message(span_suicide("[user] starts rapidly slapping [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!")) user.set_combat_mode(TRUE) ADD_TRAIT(user, TRAIT_COMBAT_MODE_LOCK, REF(src)) slapperoni(user, iteration = 1) REMOVE_TRAIT(user, TRAIT_COMBAT_MODE_LOCK, REF(src)) if (user.stat == DEAD) return MANUAL_SUICIDE user.visible_message(span_suicide("[user] slaps [user.p_them()]self with [src], but fails to go through with it!")) return SHAME /obj/item/fish/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE) if(attack_type == OVERWHELMING_ATTACK) return FALSE return ..() /obj/item/fish/proc/slapperoni(mob/living/user, iteration) stoplag(0.1 SECONDS) user.visible_message(span_bolddanger(suicide_slap_text)) user.attackby(src, user) if(user.stat > SOFT_CRIT || (iteration > 100)) REMOVE_TRAIT(user, TRAIT_COMBAT_MODE_LOCK, REF(src)) user.gib(DROP_ORGANS|DROP_BODYPARTS|DROP_ITEMS) return slapperoni(user, iteration++) /obj/item/fish/add_item_context(atom/source, list/context, obj/item/held_item, mob/user) if(HAS_TRAIT(source, TRAIT_CATCH_AND_RELEASE)) context[SCREENTIP_CONTEXT_RMB] = "Release" return CONTEXTUAL_SCREENTIP_SET return NONE /obj/item/fish/add_context(atom/source, list/context, obj/item/held_item, mob/user) if(src == held_item) context[SCREENTIP_CONTEXT_LMB] = "Pet" return CONTEXTUAL_SCREENTIP_SET if(istype(held_item, /obj/item/reagent_containers/cup/fish_feed)) context[SCREENTIP_CONTEXT_LMB] = "Feed" return CONTEXTUAL_SCREENTIP_SET if(istype(held_item, /obj/item/fish_analyzer)) context[SCREENTIP_CONTEXT_LMB] = "Scan" return CONTEXTUAL_SCREENTIP_SET if(istype(held_item, /obj/item/clothing/neck/stethoscope)) context[SCREENTIP_CONTEXT_LMB] = "Check Pulse" return CONTEXTUAL_SCREENTIP_SET return NONE /obj/item/fish/item_interaction(mob/living/user, obj/item/tool, list/modifiers) if(!istype(tool, /obj/item/clothing/neck/stethoscope)) return NONE user.balloon_alert_to_viewers("checking pulse") if(!do_after(user, 2.5 SECONDS, src)) return ITEM_INTERACT_FAILURE // Sir... I'm afraid your fish is dying. user.visible_message(span_notice("[user] checks the pulse of [src] with [tool]."), span_notice("You check the pulse of [src] with [tool].")) var/warns = get_health_warnings(user, always_deep = TRUE) if(!warns) to_chat(user, span_notice("[src] appears to be perfectly healthy!")) return ITEM_INTERACT_SUCCESS to_chat(user, warns) return ITEM_INTERACT_SUCCESS /obj/item/fish/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers) if(!HAS_TRAIT(interacting_with, TRAIT_CATCH_AND_RELEASE)) return NONE if(HAS_TRAIT(src, TRAIT_NODROP)) balloon_alert(user, "[p_theyre()] stuck to your hand!") return ITEM_INTERACT_BLOCKING balloon_alert(user, "releasing fish...") if(!do_after(user, 3 SECONDS, interacting_with)) return ITEM_INTERACT_BLOCKING balloon_alert(user, "fish released") var/goodbye_text = "Bye bye [name]." if(status == FISH_DEAD && !HAS_MIND_TRAIT(user, TRAIT_NAIVE)) goodbye_text = "May [p_they()] rest in peace..." user.visible_message(span_notice("[user] releases [src] into [interacting_with]"), \ span_notice("You release [src] into [interacting_with]. [goodbye_text]"), \ span_notice("You hear a splash.")) released(interacting_with, user) return ITEM_INTERACT_SUCCESS /obj/item/fish/proc/released(atom/location, mob/living/user) playsound(location, 'sound/effects/splash.ogg', 50) SEND_SIGNAL(location, COMSIG_FISH_RELEASED_INTO, src, user) qdel(src) ///Main proc that makes the fish edible. /obj/item/fish/proc/make_edible() var/foodtypes = get_food_types() if(foodtypes & RAW) AddComponent(/datum/component/infective, GLOB.floor_diseases.Copy(), weak = TRUE, weak_infection_chance = PERFORM_ALL_TESTS(edible_fish) ? 100 : 15) else AddComponent(/datum/component/germ_sensitive) var/bites_to_finish = weight / FISH_WEIGHT_BITE_DIVISOR create_reagents(INFINITY) //We'll set this to the total volume of the reagents right after generate_fish_reagents() is over generate_fish_reagents(bites_to_finish) reagents.maximum_volume = round(reagents.total_volume * 1.25) //make some meager space for condiments. AddComponentFrom( SOURCE_EDIBLE_INNATE, \ /datum/component/edible, \ food_flags = FOOD_NO_EXAMINE|FOOD_NO_BITECOUNT, \ foodtypes = foodtypes, \ volume = reagents.total_volume, \ eat_time = 1.5 SECONDS, \ bite_consumption = reagents.total_volume / bites_to_finish, \ after_eat = CALLBACK(src, PROC_REF(after_eat)), \ check_liked = CALLBACK(src, PROC_REF(check_liked)), \ reagent_purity = 1, \ ) RegisterSignals(src, list(COMSIG_ITEM_FRIED, COMSIG_ITEM_BARBEQUE_GRILLED), PROC_REF(on_fish_cooked)) ///A proc that returns the food types the edible component has when initialized. /obj/item/fish/proc/get_food_types() return SEAFOOD|MEAT|RAW|GORE ///Kill the fish, remove the raw and gore food types, and the infectiveness too if not under-cooked. /obj/item/fish/proc/on_fish_cooked(datum/source, cooking_time) SIGNAL_HANDLER SHOULD_NOT_OVERRIDE(TRUE) damage_fish(max_integrity) //Remove the blood from the reagents holder and reward the player with some extra nutriment added to the fish. var/datum/reagent/consumable/nutriment/protein/protein = reagents.has_reagent(/datum/reagent/consumable/nutriment/protein, check_subtypes = TRUE) var/datum/reagent/blood/blood = reagents.has_reagent(/datum/reagent/blood) var/old_blood_volume = blood ? blood.volume : 0 //we can't use the ?. operator since the above proc doesn't return null but 0 reagents.del_reagent(/datum/reagent/blood) ///Make space for the additional nutriment if(blood || protein) var/volume_mult = 1 var/protein_volume = protein ? protein.volume : 0 if(bites_amount) var/initial_bites_left = weight / FISH_WEIGHT_BITE_DIVISOR var/bites_left = initial_bites_left - bites_amount volume_mult = initial_bites_left / bites_left adjust_reagents_capacity((protein_volume - old_blood_volume) * volume_mult) ///Add the extra nutriment if(protein) reagents.multiply(2, /datum/reagent/consumable/nutriment/protein) //Remove the raw and gore foodtypes from the edible component AddComponentFrom(SOURCE_EDIBLE_INNATE, /datum/component/edible, foodtypes = get_food_types() & ~(RAW|GORE)) if(cooking_time >= FISH_SAFE_COOKING_DURATION) well_cooked() ///override the signals so they don't mess with blood and proteins again. RegisterSignals(src, list(COMSIG_ITEM_FRIED, COMSIG_ITEM_BARBEQUE_GRILLED), PROC_REF(on_fish_cooked_again), TRUE) ///Just kill the fish, again, and perhaps remove the infective comp. /obj/item/fish/proc/on_fish_cooked_again(datum/source, cooking_time) SIGNAL_HANDLER if(!HAS_TRAIT(src, TRAIT_FISH_SURVIVE_COOKING)) damage_fish(max_integrity) if(cooking_time >= FISH_SAFE_COOKING_DURATION) well_cooked() ///The fish is well cooked. Change how the fish tastes, remove the infective comp and add the relative trait. /obj/item/fish/proc/well_cooked() qdel(GetComponent(/datum/component/infective)) AddComponent(/datum/component/germ_sensitive) ADD_TRAIT(src, TRAIT_FISH_WELL_COOKED, INNATE_TRAIT) var/datum/reagent/consumable/nutriment/protein/protein = reagents.has_reagent(/datum/reagent/consumable/nutriment/protein, check_subtypes = TRUE) if(protein) protein.data = get_fish_taste_cooked() ///Checks if the fish is liked or not when eaten by a human. /obj/item/fish/proc/check_liked(mob/living/eater) if(HAS_TRAIT(eater, TRAIT_PACIFISM) && (status == FISH_ALIVE ||HAS_MIND_TRAIT(eater, TRAIT_NAIVE))) eater.add_mood_event("eating_fish", /datum/mood_event/pacifist_eating_fish_item) return FOOD_TOXIC if(HAS_TRAIT(eater, TRAIT_AGEUSIA)) return if(HAS_TRAIT(eater, TRAIT_FISH_EATER) && !HAS_TRAIT(eater, TRAIT_VEGETARIAN)) return FOOD_LIKED /** * Fish is not a reagent holder yet it's edible, so it doen't behave like most other snacks * which means it has its own way of handling being bitten, which is defined here. */ /obj/item/fish/proc/after_eat(mob/living/eater, mob/living/feeder) SHOULD_CALL_PARENT(TRUE) if(!reagents.total_volume) return bites_amount++ var/bites_to_finish = weight / FISH_WEIGHT_BITE_DIVISOR damage_fish((max_integrity / bites_to_finish) * 3) flinch_on_eat(eater, feeder) /obj/item/fish/proc/flinch_on_eat(mob/living/eater, mob/living/feeder) if(status == FISH_ALIVE && prob(50) && feeder.is_holding(src) && feeder.dropItemToGround(src)) to_chat(feeder, span_warning("[src] slips out of your hands in pain!")) var/turf/target_turf = get_ranged_target_turf(get_turf(src), pick(GLOB.alldirs), 2) throw_at(target_turf) ///A proc that returns a static reagent holder with a set reagents that you'd get when eating this fish. /obj/item/fish/proc/generate_fish_reagents(multiplier = 1) SHOULD_NOT_OVERRIDE(TRUE) var/list/reagents_to_add = get_base_edible_reagents_to_add() SEND_SIGNAL(src, COMSIG_GENERATE_REAGENTS_TO_ADD, reagents_to_add) if(multiplier != 1) for(var/reagent in reagents_to_add) reagents_to_add[reagent] *= multiplier reagents.add_reagent_list(reagents_to_add, added_purity = 1) var/datum/reagent/consumable/nutriment/protein/protein = reagents.has_reagent(/datum/reagent/consumable/nutriment/protein, check_subtypes = TRUE) if(protein) protein.data = HAS_TRAIT(src, TRAIT_FISH_WELL_COOKED) ? get_fish_taste_cooked() : get_fish_taste() /obj/item/fish/proc/get_fish_taste() return list("raw fish" = 2.5, "scales" = 1) /obj/item/fish/proc/get_fish_taste_cooked() return list("cooked fish" = 2) ///The proc that adds in the main reagents this fish has when eaten (without accounting for traits) /obj/item/fish/proc/get_base_edible_reagents_to_add() var/return_list = list( /datum/reagent/consumable/nutriment/protein = 2, /datum/reagent/blood = 1, ) //It has been at the very least under-cooked. if(HAS_TRAIT(src, TRAIT_FOOD_FRIED) || HAS_TRAIT(src, TRAIT_FOOD_BBQ_GRILLED)) return_list[/datum/reagent/consumable/nutriment/protein] *= 2 return_list -= /datum/reagent/blood if(required_fluid_type == AQUARIUM_FLUID_SALTWATER) return_list[/datum/reagent/consumable/salt] = 0.4 return return_list ///adjusts the maximum volume of the fish reagents holder and update the amount of food to bite /obj/item/fish/proc/adjust_reagents_capacity(amount_to_add) if(!reagents) return reagents.maximum_volume += amount_to_add var/bites_to_finish = weight / FISH_WEIGHT_BITE_DIVISOR ///updates how many units of reagent one bite takes if edible. if(IS_EDIBLE(src)) AddComponentFrom(SOURCE_EDIBLE_INNATE, /datum/component/edible, bite_consumption = reagents.maximum_volume / bites_to_finish) ///Grinding a fish replaces some the protein it has with blood and gibs. You ain't getting a clean smoothie out of it. /obj/item/fish/on_grind() . = ..() if(!reagents) return reagents.convert_reagent(/datum/reagent/consumable/nutriment/protein, /datum/reagent/consumable/liquidgibs, multiplier = 0.4, include_source_subtypes = TRUE) reagents.convert_reagent(/datum/reagent/consumable/nutriment/protein, /datum/reagent/blood, multiplier = 0.2, include_source_subtypes = TRUE) ///When processed, the reagents inside this fish will be passed to the created atoms. /obj/item/fish/UsedforProcessing(mob/living/user, obj/item/used_item, list/chosen_option, list/created_atoms) var/created_len = length(created_atoms) for(var/atom/movable/created as anything in created_atoms) if(!created.reagents) continue for(var/datum/reagent/reagent as anything in reagents.reagent_list) var/transfer_vol = reagent.volume / created_len var/datum/reagent/result_reagent = created.reagents.has_reagent(reagent.type) if(!result_reagent) created.reagents.add_reagent(reagent.type, transfer_vol, reagents.copy_data(reagent), reagents.chem_temp, reagent.purity, reagent.ph, no_react = TRUE) continue created.reagents.multiply(transfer_vol / result_reagent.volume, reagent.type) return ..() /obj/item/fish/update_icon_state() if((status == FISH_DEAD || HAS_TRAIT(src, TRAIT_FISH_STASIS)) && icon_state_dead) icon_state = icon_state_dead else icon_state = base_icon_state return ..() /obj/item/fish/attackby(obj/item/item, mob/living/user, list/modifiers, list/attack_modifiers) if(!istype(item, /obj/item/reagent_containers/cup/fish_feed)) return ..() if(!item.reagents.total_volume) balloon_alert(user, "[item.name] is empty!") return TRUE if(status == FISH_DEAD) balloon_alert(user, "[name] [HAS_MIND_TRAIT(user, TRAIT_NAIVE) ? "isn't hungry" : "is dead!"]") return TRUE feed(item.reagents) balloon_alert(user, "fed [name]") return TRUE /obj/item/fish/examine(mob/user) . = ..() if(catcher_name && catch_date) . += span_boldnicegreen("Caught by [catcher_name] on [catch_date].") if(HAS_MIND_TRAIT(user, TRAIT_EXAMINE_FISH) || HAS_TRAIT(loc, TRAIT_EXAMINE_FISH)) . += span_notice("[p_theyre(TRUE)] [size] cm long.") . += span_notice("[p_they(TRUE)] weighs [weight] [span_tooltip("the standard unit of measurement for space age fish", "kiloclam")].") if(HAS_TRAIT(src, TRAIT_FISH_GENEGUNNED)) . += span_warning("[p_theyve(TRUE)] been edited by a fish genegun. [p_they(TRUE)]'ll die if edited again.") . += get_health_warnings(user, always_deep = FALSE) if(HAS_TRAIT(src, TRAIT_FISHING_BAIT)) . += span_smallnoticeital("[p_they(TRUE)] can be used as a fishing bait.") if(bites_amount) . += span_warning("[p_theyve(TRUE)] been bitten by someone.") /obj/item/fish/proc/get_health_warnings(mob/user, always_deep = FALSE) if(!HAS_MIND_TRAIT(user, TRAIT_EXAMINE_DEEPER_FISH) && !always_deep) return if(status == FISH_DEAD) return span_deadsay("[p_theyre(TRUE)] [HAS_MIND_TRAIT(user, TRAIT_NAIVE) ? "taking the big snooze" : "dead"].") var/list/warnings = list() if(get_starvation_mult()) warnings += "starving" if(!HAS_TRAIT(src, TRAIT_FISH_STASIS) && !proper_environment()) warnings += "drowning" var/health_ratio = get_health_percentage() switch(health_ratio) if(0 to 0.25) warnings += "dying" if(0.25 to 0.5) warnings += "very unhealthy" if(0.5 to 0.75) warnings += "unhealthy" if(0.75 to 0.9) warnings += "mostly healthy" if(length(warnings)) . += span_warning("[p_theyre(TRUE)] [english_list(warnings)].") return . /** * This proc takes a base size, base weight and deviation arguments to generate new size and weight through a gaussian distribution (bell curve) * Mainly used to determinate the size and weight of caught fish. */ /obj/item/fish/proc/randomize_size_and_weight(base_size = average_size, base_weight = average_weight, deviation = weight_size_deviation, update = TRUE) var/size_deviation = 0.2 * base_size temp_size = round(clamp(gaussian(base_size, size_deviation), average_size * 1/MAX_FISH_DEVIATION_COEFF, average_size * MAX_FISH_DEVIATION_COEFF)) var/weight_deviation = 0.2 * base_weight temp_weight = round(clamp(gaussian(base_weight, weight_deviation), average_weight * 1/MAX_FISH_DEVIATION_COEFF, average_weight * MAX_FISH_DEVIATION_COEFF)) set_max_size_and_weight(temp_size, temp_weight) if(update) update_size_and_weight(temp_size, temp_weight) ///Set the maximum size and weight a fish can reach from base size and weight args if they have't been set already. /obj/item/fish/proc/set_max_size_and_weight(base_size, base_weight) if(!maximum_size) maximum_size = min(base_size * 2, average_size * MAX_FISH_DEVIATION_COEFF) if(!maximum_weight) maximum_weight = min(base_weight * 2, average_weight * MAX_FISH_DEVIATION_COEFF) ///Updates weight and size, along with weight class, number of fillets you can get and grind results. /obj/item/fish/proc/update_size_and_weight(new_size = average_size, new_weight = average_weight, update_materials = TRUE) fish_flags |= FISH_FLAG_UPDATING_SIZE_AND_WEIGHT SEND_SIGNAL(src, COMSIG_FISH_UPDATE_SIZE_AND_WEIGHT, new_size, new_weight) var/is_mount = istype(loc, /obj/structure/fish_mount) //used to prevent fish from getting butchered inside mounts if(size) if(!is_mount) remove_fillet_type() if(size > FISH_SIZE_TWO_HANDS_REQUIRED) qdel(GetComponent(/datum/component/two_handed)) else set_max_size_and_weight(new_size, new_weight) size = new_size var/init_icon_state = initial(inhand_icon_state) switch(size) if(0 to FISH_SIZE_TINY_MAX) update_weight_class(WEIGHT_CLASS_TINY) if(!init_icon_state) inhand_icon_state = "fish_small" if(FISH_SIZE_TINY_MAX to FISH_SIZE_SMALL_MAX) if(!init_icon_state) inhand_icon_state = "fish_small" update_weight_class(WEIGHT_CLASS_SMALL) if(FISH_SIZE_SMALL_MAX to FISH_SIZE_NORMAL_MAX) if(!init_icon_state) inhand_icon_state = "fish_normal" update_weight_class(WEIGHT_CLASS_NORMAL) if(FISH_SIZE_NORMAL_MAX to FISH_SIZE_BULKY_MAX) if(!init_icon_state) inhand_icon_state = "fish_bulky" update_weight_class(WEIGHT_CLASS_BULKY) if(FISH_SIZE_BULKY_MAX to FISH_SIZE_HUGE_MAX) if(!init_icon_state) inhand_icon_state = "fish_huge" update_weight_class(WEIGHT_CLASS_HUGE) if(FISH_SIZE_HUGE_MAX to INFINITY) if(!init_icon_state) inhand_icon_state = "fish_huge" update_weight_class(WEIGHT_CLASS_GIGANTIC) if(size > FISH_SIZE_TWO_HANDS_REQUIRED || (HAS_TRAIT(src, TRAIT_FISH_SHOULD_TWOHANDED) && w_class >= WEIGHT_CLASS_BULKY)) inhand_icon_state = "[inhand_icon_state]_wielded" AddComponent(/datum/component/two_handed, require_twohands = TRUE) if(!is_mount) add_fillet_type() var/make_edible = !weight if(weight) if(reagents) //This fish has reagents. Adjust the maximum volume of the reagent holder and do some math to adjut the reagents too. var/new_weight_ratio = new_weight / weight var/volume_diff = reagents.maximum_volume * new_weight_ratio - reagents.maximum_volume if(new_weight_ratio > weight) adjust_reagents_capacity(volume_diff) ///As always, we want to maintain proportions here, so we need to get the ratio of bites left and initial bites left. var/weight_diff = new_weight - weight var/multiplier = weight_diff / FISH_WEIGHT_BITE_DIVISOR var/initial_bites_left = weight / FISH_WEIGHT_BITE_DIVISOR var/bites_left = initial_bites_left - bites_amount var/amount_to_gen = bites_left / initial_bites_left * multiplier generate_fish_reagents(amount_to_gen) else reagents.multiply(new_weight_ratio) adjust_reagents_capacity(volume_diff) weight = new_weight if(make_edible) make_edible() if(weight >= FISH_WEIGHT_SLOWDOWN && !HAS_TRAIT(src, TRAIT_SPEED_POTIONED)) slowdown = GET_FISH_SLOWDOWN(weight) drag_slowdown = round(slowdown * 0.5, 1) else slowdown = 0 drag_slowdown = 0 if(ismob(loc)) var/mob/mob = loc mob.update_equipment_speed_mods() var/mats_len = length(custom_materials) if(update_materials && mats_len) var/list/new_mats_list = custom_materials.Copy() var/multiplier = 1 / mats_len var/unmodified_weight = weight for(var/mat_type in custom_materials) var/datum/material/material = SSmaterials.get_material(mat_type) unmodified_weight /= GET_MATERIAL_MODIFIER(1 + (material.get_property(MATERIAL_DENSITY) - 4) * 0.1, multiplier) multiplier = unmodified_weight / weight for(var/mat_type in new_mats_list) new_mats_list[mat_type] *= multiplier set_custom_materials(new_mats_list) // apply_material_effects() will call update_fish_force for us update_fish_force() fish_flags &= ~FISH_FLAG_UPDATING_SIZE_AND_WEIGHT /obj/item/fish/proc/remove_fillet_type() if(!fillet_type) return var/amount = max(round(num_fillets * size / FISH_FILLET_NUMBER_SIZE_DIVISOR, 1), 1) var/time = PERFORM_ALL_TESTS(fish_size_weight) ? 0 : 0.5 SECONDS * amount RemoveElement(/datum/element/processable, TOOL_KNIFE, fillet_type, amount, time, screentip_verb = "Cut") /obj/item/fish/proc/add_fillet_type() if(!fillet_type) return var/amount = max(round(num_fillets * size / FISH_FILLET_NUMBER_SIZE_DIVISOR, 1), 1) var/time = PERFORM_ALL_TESTS(fish_size_weight) ? 0 : 0.5 SECONDS * amount AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, amount, time, screentip_verb = "Cut") return amount //checked by a unit test ///Reset weapon-related variables of this items and recalculates those values based on the fish weight and size. /obj/item/fish/proc/update_fish_force() if(force >= 15 && hitsound == SFX_ALT_FISH_SLAP) hitsound = SFX_DEFAULT_FISH_SLAP force = initial(force) throwforce = initial(throwforce) throw_range = initial(throw_range) demolition_mod = initial(demolition_mod) attack_verb_continuous = initial(attack_verb_continuous) attack_verb_simple = initial(attack_verb_simple) hitsound = initial(hitsound) damtype = initial(damtype) attack_speed = initial(attack_speed) block_chance = initial(block_chance) armour_penetration = initial(armour_penetration) wound_bonus = initial(wound_bonus) exposed_wound_bonus = initial(exposed_wound_bonus) toolspeed = initial(toolspeed) var/weight_rank = GET_FISH_WEIGHT_RANK(weight) throw_range -= weight_rank get_force_rank() var/bonus_malus = weight_rank - w_class if(bonus_malus) calculate_fish_force_bonus(bonus_malus) throwforce = force if(material_flags & MATERIAL_EFFECTS && length(custom_materials)) //struck by metal gen or something. if(material_flags & MATERIAL_AFFECT_STATISTICS) var/index = 1 for(var/current_material in custom_materials) var/datum/material/material = SSmaterials.get_material(current_material) change_material_strength(material, custom_materials[material], get_material_multiplier(material, custom_materials, index)) index += 1 var/datum/material/master = get_master_material() if(master?.item_sound_override) hitsound = master.item_sound_override usesound = master.item_sound_override mob_throw_hit_sound = master.item_sound_override equip_sound = master.item_sound_override pickup_sound = master.item_sound_override drop_sound = master.item_sound_override SEND_SIGNAL(src, COMSIG_FISH_FORCE_UPDATED, weight_rank, bonus_malus) if(force >= 15 && hitsound == SFX_DEFAULT_FISH_SLAP) // don't override special attack sounds hitsound = SFX_ALT_FISH_SLAP // do more damage - do heavier slap sound ///A proc that makes the fish slightly stronger or weaker if there's a noticeable discrepancy between size and weight. /obj/item/fish/proc/calculate_fish_force_bonus(bonus_malus) demolition_mod += bonus_malus * 0.1 attack_speed += bonus_malus * 0.1 force = round(force * (1 + bonus_malus * 0.1), 0.1) /obj/item/fish/proc/get_force_rank() switch(w_class) if(WEIGHT_CLASS_TINY) force -= 3 attack_speed -= 0.1 SECONDS if(WEIGHT_CLASS_NORMAL) force += 2 if(WEIGHT_CLASS_BULKY) force += 5 attack_speed += 0.1 SECONDS if(WEIGHT_CLASS_HUGE) force += 9 attack_speed += 0.2 SECONDS demolition_mod += 0.2 if(WEIGHT_CLASS_GIGANTIC) force += 13 attack_speed += 0.4 SECONDS demolition_mod += 0.4 /obj/item/fish/apply_single_mat_effect(datum/material/custom_material, amount, multiplier) . = ..() //The materials are being increased/decreased along with the weight. if(!(fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT)) material_weight_mult *= GET_MATERIAL_MODIFIER(1 + (custom_material.get_property(MATERIAL_DENSITY) - 4) * 0.1, multiplier) /obj/item/fish/apply_material_effects() . = ..() //Either effects aren't applied of he materials are simply being increased/decreased along with the weight. Avoids recursion. if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) ) return maximum_weight *= material_weight_mult update_size_and_weight(size, (temp_weight || weight) * material_weight_mult, update_materials = FALSE) /obj/item/fish/remove_material_effects(replace_mats = TRUE) . = ..() if(replace_mats || !(material_flags & MATERIAL_EFFECTS) ) return maximum_weight /= material_weight_mult update_size_and_weight(size, weight / material_weight_mult) material_weight_mult = 1 /** * This proc has fish_traits list populated with fish_traits paths from three different lists: * traits from x_traits and y_traits are compared, and inserted if conditions are met; * traits from fixed_traits are inserted unconditionally. * traits from removed_traits will be removed from the for loop. * * This proc should only be called if the fish was spawned with the apply_qualities arg set to FALSE * and hasn't had inherited traits already. */ /obj/item/fish/proc/inherit_traits(list/x_traits, list/y_traits, list/fixed_traits, list/removed_traits) fish_traits = fixed_traits?.Copy() || list() var/list/same_traits = x_traits & y_traits var/list/all_traits = (y_traits ? (x_traits|y_traits) : x_traits) - removed_traits /// a list of incompatible traits that'll be filled as it goes on. Don't let any such trait pass onto the fish. var/list/incompatible_traits = list() ///some traits can spontaneously manifest for some fishes. These have higher priorities than other traits var/list/potential_spontaneous_traits = GLOB.spontaneous_fish_traits[type] for(var/trait_type in potential_spontaneous_traits) if(!prob(potential_spontaneous_traits[trait_type])) continue var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] if(length(fish_traits & trait.incompatible_traits)) continue fish_traits |= trait_type incompatible_traits |= trait.incompatible_traits for(var/trait_type in fish_traits) var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] incompatible_traits |= trait.incompatible_traits /** * shuffle the traits, so, in the case of incompatible traits, we don't have to choose which to discard. * Instead we let the random numbers do it for us in a first come, first served basis. */ for(var/trait_type in shuffle(all_traits)) if(trait_type in fish_traits) continue //likely a fixed trait if(trait_type in incompatible_traits) continue var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] if(isnull(trait)) stack_trace("Couldn't find trait [trait_type || "null"] in the global fish traits list") continue if(!isnull(trait.fish_whitelist) && !(type in trait.fish_whitelist)) continue if(length(fish_traits & trait.incompatible_traits)) continue // If there's no partner, we've been reated through parthenogenesis or growth, therefore, traits are copied // Otherwise, we check if both have the trait or perform a probability check. if(!y_traits || (trait_type in same_traits) || prob(trait.inheritability)) fish_traits |= trait_type incompatible_traits |= trait.incompatible_traits apply_traits() /obj/item/fish/proc/apply_traits() for(var/fish_trait_type in fish_traits) var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait_type] trait.apply_to_fish(src) /obj/item/fish/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) . = ..() check_flopping() /// Stop processing once the stasis trait is added /obj/item/fish/proc/enter_stasis(datum/source) SIGNAL_HANDLER stop_flopping() update_appearance() STOP_PROCESSING(SSobj, src) /// Start processing again when the stasis trait is removed /obj/item/fish/proc/exit_stasis(datum/source) SIGNAL_HANDLER if(status == FISH_DEAD) return START_PROCESSING(SSobj, src) check_flopping() ///Returns the value for hunger ranging from 0 to the cap (by default 1) /obj/item/fish/proc/get_hunger(cap = FISH_STARVING_THRESHOLD) . = clamp((world.time - last_feeding) / feeding_frequency, 0, cap) if(HAS_TRAIT(src, TRAIT_FISH_NO_HUNGER)) return min(., FISH_STARVING_THRESHOLD * 0.2) /obj/item/fish/proc/get_starvation_mult() var/hunger = get_hunger(cap = FISH_STARVING_THRESHOLD * 2) return hunger >= FISH_STARVING_THRESHOLD ? hunger : 0 ///Feed the fishes with the contents of the fish feed /obj/item/fish/proc/feed(datum/reagents/fed_reagents) if(status != FISH_ALIVE) return ///If one of the reagent with fish effects is also our food reagent this is set to TRUE var/already_fed = FALSE var/was_hungry = get_hunger() for(var/datum/reagent/reagent as anything in fed_reagents.reagent_list) if(!fed_reagents.has_reagent(reagent.type, 0.1) || !reagent.used_on_fish(src)) continue fed_reagents.remove_reagent(reagent.type, 0.1) if(reagent.type == food) already_fed = TRUE if(was_hungry && !get_hunger()) //one of the other reagents already sated its hunger. return if(already_fed) sate_hunger() return if(fed_reagents.remove_reagent(food, 0.1)) sate_hunger() return var/datum/reagent/wrong_reagent = pick(fed_reagents.reagent_list) if(!wrong_reagent) return fed_reagents.remove_reagent(wrong_reagent.type, 0.1) ///Proc that should be called when the fish is fed. By default, it grows the fish depending on various variables. /obj/item/fish/proc/sate_hunger() if(HAS_TRAIT(loc, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH)) last_feeding = world.time return var/hunger = get_hunger() last_feeding = world.time if(hunger < 0.05) //don't bother growing for very small amounts. return var/new_size = size var/new_weight = weight var/hunger_mult if(hunger <= FISH_GROWTH_PEAK) hunger_mult = hunger / FISH_GROWTH_PEAK else hunger_mult = 1 - (hunger - FISH_GROWTH_PEAK) * 4 if(hunger_mult <= 0) return var/base_mult = FISH_GROWTH_MULT if(HAS_TRAIT(src, TRAIT_FISH_QUICK_GROWTH)) base_mult *= 2.5 if(size < maximum_size) new_size += CEILING((maximum_size - size) * base_mult / (w_class * FISH_SIZE_WEIGHT_GROWTH_MALUS) * hunger_mult, 1) new_size = min(new_size, maximum_size) if(weight < maximum_weight) new_weight += CEILING((maximum_weight - weight) * base_mult / (GET_FISH_WEIGHT_RANK(weight) * FISH_SIZE_WEIGHT_GROWTH_MALUS) * hunger_mult, 1) new_weight = min(new_weight, maximum_weight) if(new_size != size || new_weight != weight) update_size_and_weight(new_size, new_weight) /obj/item/fish/proc/check_flopping() if(QDELETED(src)) //we don't care anymore return if(!(fish_flags & FISH_DO_FLOP_ANIM)) return // Do additional stuff // Start flopping if outside of fish container var/should_be_flopping = status == FISH_ALIVE && (loc && !HAS_TRAIT(loc, TRAIT_STOP_FISH_FLOPPING)) if(should_be_flopping) start_flopping() else stop_flopping() /obj/item/fish/process(seconds_per_tick) if(HAS_TRAIT(src, TRAIT_FISH_STASIS) || status != FISH_ALIVE) return do_fish_process(seconds_per_tick) if(status != FISH_ALIVE || !is_type_in_typecache(loc, SSfishing.fish_safe_turfs_by_type[type])) time_passed_on_safe_turf = 0 SECONDS return time_passed_on_safe_turf += seconds_per_tick SECONDS if(time_passed_on_safe_turf >= (get_starvation_mult() ? STARVING_FISH_SUBMERGING_THRESHOLD : FISH_SUBMERGING_THRESHOLD)) visible_message(span_notice("[src] disperses into \the [loc]"), span_notice("You hear a splash.")) released(loc) /obj/item/fish/proc/do_fish_process(seconds_per_tick) //safe mode, don't do much except a few things that don't involve growing or reproducing. if(loc && HAS_TRAIT_FROM(loc, TRAIT_STOP_FISH_REPRODUCTION_AND_GROWTH, AQUARIUM_TRAIT)) last_feeding += seconds_per_tick SECONDS breeding_wait += seconds_per_tick SECONDS else process_health(seconds_per_tick) if(ready_to_reproduce()) try_to_reproduce() if(HAS_TRAIT(src, TRAIT_FISH_ELECTROGENESIS) && COOLDOWN_FINISHED(src, electrogenesis_cooldown)) try_electrogenesis() SEND_SIGNAL(src, COMSIG_FISH_LIFE, seconds_per_tick) /obj/item/fish/proc/set_status(new_status, silent = FALSE) if(status == new_status) return switch(new_status) if(FISH_ALIVE) status = FISH_ALIVE repair_damage(max_integrity) regenerate_bites(bites_amount) last_feeding = world.time //reset hunger check_flopping() START_PROCESSING(SSobj, src) ADD_TRAIT(src, TRAIT_UNCOMPOSTABLE, INNATE_TRAIT) if(FISH_DEAD) status = FISH_DEAD STOP_PROCESSING(SSobj, src) REMOVE_TRAIT(src, TRAIT_UNCOMPOSTABLE, INNATE_TRAIT) stop_flopping() if(!silent) var/message = span_warning(replacetext(death_text, "%SRC", "[src]")) if(loc && HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) loc.visible_message(message) else visible_message(message) update_appearance() update_fish_force() SEND_SIGNAL(src, COMSIG_FISH_STATUS_CHANGED) /obj/item/fish/vv_edit_var(var_name, var_value) switch(var_name) if(NAMEOF(src, status)) if(var_value != FISH_DEAD && var_value != FISH_ALIVE) var_value = var_value ? FISH_ALIVE : FISH_DEAD set_status(var_value) if(NAMEOF(src, size)) if(!isnum(var_value) || var_value == 0) return FALSE update_size_and_weight(var_value, weight) if(NAMEOF(src, weight)) if(!isnum(var_value) || var_value == 0) return FALSE update_size_and_weight(size, var_value) if(NAMEOF(src, fish_flags)) var/old_fish_flags = fish_flags fish_flags = var_value if((old_fish_flags ^ fish_flags) & FISH_DO_FLOP_ANIM) //the flopping flag wasn't added nor removed return TRUE if(fish_flags & FISH_DO_FLOP_ANIM) RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, PROC_REF(on_temp_animation)) else UnregisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START) check_flopping() if(NAMEOF(src, fillet_type)) if(!ispath(var_value)) return FALSE remove_fillet_type() fillet_type = var_value add_fillet_type() if(NAMEOF(src, num_fillets)) if(!isnum(var_value)) return FALSE remove_fillet_type() num_fillets = var_value add_fillet_type() else return ..() return TRUE /obj/item/fish/expose_reagents(list/reagents, datum/reagents/source, methods = TOUCH, volume_modifier = 1, show_message = TRUE) . = ..() if(. & COMPONENT_NO_EXPOSE_REAGENTS || status != FISH_DEAD) return var/datum/reagent/medicine/strange_reagent/revival = locate() in reagents if(!revival) return if(reagents[revival] >= 2 * w_class && revival.pre_rez_check(src)) set_status(FISH_ALIVE) else balloon_alert_to_viewers("twitches for a moment!") animate(src, pixel_x = 1, time = 0.1 SECONDS, loop = 2, flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) animate(pixel_x = -1, flags = ANIMATION_RELATIVE) /obj/item/fish/proc/use_lazarus(datum/source, obj/item/lazarus_injector/injector, mob/user) SIGNAL_HANDLER if(injector.revive_type != SENTIENCE_ORGANIC) balloon_alert(user, "invalid creature!") return if(status != FISH_DEAD) balloon_alert(user, "[p_theyre(TRUE)] not dead!") return set_status(FISH_ALIVE) injector.expend(src, user) return LAZARUS_INJECTOR_USED /obj/item/fish/proc/update_aquarium_appearance(datum/source, obj/effect/aquarium/visual) SIGNAL_HANDLER visual.icon = dedicated_in_aquarium_icon || icon visual.icon_state = dedicated_in_aquarium_icon_state || "[initial(icon_state)]_small" /obj/item/fish/proc/randomize_aquarium_position(datum/source, atom/movable/current_aquarium, obj/effect/aquarium/visual) SIGNAL_HANDLER var/avg_width = round(sprite_width * 0.5) var/avg_height = round(sprite_height * 0.5) var/pw_min = visual.aquarium_zone_min_pw + avg_width - 16 var/pw_max = visual.aquarium_zone_max_pw - avg_width - 16 var/pz_min = visual.aquarium_zone_min_pz + avg_height - 16 var/pz_max = visual.aquarium_zone_max_pz - avg_height - 16 visual.pixel_w = visual.base_pixel_w = rand(pw_min,pw_max) visual.pixel_z = visual.base_pixel_z = rand(pz_min,pz_max) /obj/item/fish/proc/update_aquarium_animation(datum/source, current_animation, obj/effect/visual, fluid_type) SIGNAL_HANDLER var/animation = get_aquarium_animation(fluid_type) if(animation == current_animation) return switch(animation) if(AQUARIUM_ANIMATION_FISH_SWIM) swim_animation(visual) if(AQUARIUM_ANIMATION_FISH_DEAD) dead_animation(visual) /obj/item/fish/proc/get_aquarium_animation(fluid_type) if(fluid_type == AQUARIUM_FLUID_AIR || status == FISH_DEAD) return AQUARIUM_ANIMATION_FISH_DEAD else return AQUARIUM_ANIMATION_FISH_SWIM /// Create looping random path animation, pixel offsets parameters include offsets already /obj/item/fish/proc/swim_animation(obj/effect/aquarium/visual) var/avg_width = round(sprite_width / 2) var/avg_height = round(sprite_height / 2) var/pw_min = visual.aquarium_zone_min_pw + avg_width - 16 var/pw_max = visual.aquarium_zone_max_pw - avg_width - 16 var/pz_min = visual.aquarium_zone_min_pz + avg_height - 16 var/pz_max = visual.aquarium_zone_max_pz - avg_width - 16 var/origin_w = visual.base_pixel_w var/origin_z = visual.base_pixel_z var/prev_w = origin_w var/prev_z = origin_z animate(visual, pixel_w = origin_w, time = 0, loop = -1) //Just to start the animation var/move_number = rand(3, 5) //maybe unhardcode this for(var/i in 1 to move_number) //If it's last movement, move back to start otherwise move to some random point var/target_w = i == move_number ? origin_w : rand(pw_min,pw_max) //could do with enforcing minimal delta for prettier zigzags var/target_z = i == move_number ? origin_z : rand(pz_min,pz_max) var/dist_w = prev_w - target_w var/dist_z = prev_z - target_z prev_w = target_w prev_z = target_z var/dist = abs(dist_w) + abs(dist_z) var/eyeballed_time = dist * 2 //2ds per px //Face the direction we're going var/matrix/dir_mx = matrix(visual.transform) if(dist_w <= 0) //assuming default sprite is facing left here dir_mx.Scale(-1, 1) animate(transform = dir_mx, time = 0, loop = -1) animate(pixel_w = target_w, pixel_z = target_z, time = eyeballed_time, loop = -1) /obj/item/fish/proc/dead_animation(obj/effect/aquarium/visual) //Set base_pixel_y to lowest possible value var/avg_height = round(sprite_height / 2) var/pz_min = visual.aquarium_zone_min_pz + avg_height - 16 visual.base_pixel_z = pz_min animate(visual, pixel_z = pz_min, time = 1) //flop to bottom and end current animation. ///Malus to the beauty value if the fish content is dead #define DEAD_FISH_BEAUTY -500 ///Prevents more impressive fishes from providing a positive beauty even when dead. #define MAX_DEAD_FISH_BEAUTY -200 ///Some fish are already so ugly, they can't get much worse when dead #define MIN_DEAD_FISH_BEAUTY -600 /obj/item/fish/proc/get_aquarium_beauty(datum/source, list/beauty_holder) SIGNAL_HANDLER var/actual_beauty = beauty if(status == FISH_DEAD) actual_beauty = clamp(beauty + DEAD_FISH_BEAUTY, MIN_DEAD_FISH_BEAUTY, MAX_DEAD_FISH_BEAUTY) beauty_holder += actual_beauty #undef DEAD_FISH_BEAUTY #undef MIN_DEAD_FISH_BEAUTY #undef MAX_DEAD_FISH_BEAUTY /// Checks if our current environment lets us live. /obj/item/fish/proc/proper_environment(temp_range_min = required_temperature_min, temp_range_max = required_temperature_max) if(!loc) return TRUE if(HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) if(!(fish_flags & FISH_FLAG_SAFE_TEMPERATURE) || !(fish_flags & FISH_FLAG_SAFE_FLUID)) return FALSE return TRUE if(is_type_in_typecache(loc, SSfishing.fish_safe_turfs_by_type[type])) return TRUE if(required_fluid_type != AQUARIUM_FLUID_AIR && !HAS_TRAIT(src, TRAIT_FISH_AMPHIBIOUS)) return FALSE var/datum/gas_mixture/mixture = loc.return_air() if(!mixture) return FALSE if(safe_air_limits && !check_gases(mixture.gases, safe_air_limits)) return FALSE if(!ISINRANGE(mixture.temperature, required_temperature_min, required_temperature_max)) return FALSE var/pressure = mixture.return_pressure() if(!ISINRANGE(pressure, min_pressure, max_pressure)) return FALSE return TRUE /obj/item/fish/proc/process_health(seconds_per_tick) var/health_change = 0 if(!proper_environment()) health_change -= 2.5 //Dying here var/starvation_mult = get_starvation_mult() if(starvation_mult) health_change -= 0.25 * starvation_mult //Starving else health_change += 0.5 //Slowly healing if(HAS_TRAIT(src, TRAIT_FISH_ON_TESLIUM)) health_change -= 0.65 if(!health_change) return health_change *= seconds_per_tick if(health_change < 0) damage_fish(-health_change) else repair_damage(health_change) ///Used to damage this fish while it's still alive. Prevents the fish from taking damage beyond the integrity_failure threshold /obj/item/fish/proc/damage_fish(amount) if(status == FISH_DEAD || amount <= 0) return var/current_integrity = get_integrity() take_damage(min(amount, current_integrity - max_integrity * integrity_failure), sound_effect = FALSE, armour_penetration = 100) /// fish dies when its integrity reaches 50% /obj/item/fish/atom_break(damage_flag) . = ..() set_status(FISH_DEAD) /obj/item/fish/repair_damage(amount) . = ..() if(!. || !bites_amount) return var/current_integrity = get_integrity() var/old_integrity = current_integrity - amount var/old_max_integrity_diff = max_integrity - old_integrity var/percent = (max_integrity - current_integrity) / old_max_integrity_diff var/bites_to_recover = bites_amount * percent regenerate_bites(bites_to_recover) /obj/item/fish/proc/regenerate_bites(amount) amount = min(amount, bites_amount) if(amount <= 0) return bites_amount -= amount generate_fish_reagents(amount) /// returns a value between 0 and 1 representing how much integrity the fish has before dying (atom_break) /obj/item/fish/proc/get_health_percentage() var/max_health = max_integrity * (1 - integrity_failure) var/death_thres = max_integrity - max_health return CLAMP01((get_integrity() - death_thres) / max_health) /// Returns tracked_fish_by_type but flattened and without the items in the blacklist, also shuffled if shuffle is TRUE. /obj/item/fish/proc/get_aquarium_fishes(shuffle = FALSE, blacklist) . = list() for(var/obj/item/fish/fish in loc) . += fish . -= blacklist if(shuffle) . = shuffle(.) return . /obj/item/fish/proc/ready_to_reproduce(being_targeted = FALSE) if(!loc || !HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) return FALSE if(being_targeted && HAS_TRAIT(src, TRAIT_FISH_NO_MATING)) 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 && world.time >= breeding_wait /obj/item/fish/proc/try_to_reproduce() if(!loc || !HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) return FALSE var/obj/item/fish/second_fish ///Fishes with this trait cannot mate, but could still reproduce asexually, so don't early return. 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)) 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 //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) var/chosen_type var/datum/fish_evolution/chosen_evolution var/list/possible_evolutions = list() for(var/evolution_type in evolution_types) var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type] if(evolution.check_conditions(src, second_fish, loc)) possible_evolutions += evolution if(second_fish?.evolution_types) var/secondary_evolutions = (second_fish.evolution_types - evolution_types) for(var/evolution_type in secondary_evolutions) var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type] if(evolution.check_conditions(second_fish, src, loc)) possible_evolutions += evolution var/list/types = spawn_types || list(type) if(length(possible_evolutions)) chosen_evolution = pick(possible_evolutions) chosen_type = chosen_evolution.new_fish_type else if(second_fish) var/list/second_fish_types = second_fish.spawn_types || list(second_fish.type) var/recessive = HAS_TRAIT(src, TRAIT_FISH_RECESSIVE) var/recessive_partner = HAS_TRAIT(second_fish, TRAIT_FISH_RECESSIVE) if(fish_flags & FISH_FLAG_OVERPOPULATED) if(recessive_partner && !recessive) return FALSE chosen_type = pick(second_fish_types) else if(recessive && !recessive_partner) chosen_type = pick(second_fish_types) else if(recessive_partner && !recessive) chosen_type = pick(types) else var/list/picks = second_fish_types + types chosen_type = pick(picks) else chosen_type = pick(types) 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 /// A product of fish breeding is spawned, and it's inherited traits are handled here. /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 new_fish.inherit_traits(fish_traits, partner?.fish_traits, evolution?.new_traits, evolution?.removed_traits) ///If set, the offspring will inherit materials from the parent. var/obj/item/fish/chosen_material_giver //We combine two methods for determining the size and weight of the offspring for less extreme results. if(partner) var/ratio_size = new_fish.average_size * (((size / average_size) + (partner.size / partner.average_size)) / 2) var/mean_size = (size + partner.size)/2 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 + partner.breeding_timeout if(length(partner.custom_materials)) if(length(custom_materials)) chosen_material_giver = pick(src, partner) else if(prob(50)) chosen_material_giver = partner else if(length(custom_materials) && prob(50)) chosen_material_giver = src else new_fish.temp_size = size new_fish.temp_weight = weight if(length(custom_materials)) chosen_material_giver = src if(chosen_material_giver) //We need the original weight of the fish to set the correct amount (it scales with weight) of mats for the offspring var/mats_multiplier = new_fish.temp_weight / (chosen_material_giver.weight / material_weight_mult) var/list/new_mats = chosen_material_giver.custom_materials.Copy() for(var/material in new_mats) new_mats[material] *= mats_multiplier new_fish.set_custom_materials(new_mats) // apply_material_effects() will call update_size_and_weight() else new_fish.update_size_and_weight(new_fish.temp_size, new_fish.temp_weight) var/list/fishing_data = list(new_fish.size, new_fish.weight, new_fish.custom_materials) log_fish("[new_fish] has been bred at [new_fish.drop_location()] from [partner].", fishing_data) 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 #define FLOP_DEGREE 20 #define FLOP_SINGLE_MOVE_TIME 1.5 #define JUMP_X_DISTANCE 5 #define JUMP_Y_DISTANCE 6 /// This flopping animation played while the fish is alive. /obj/item/fish/proc/flop_animation() var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync animate(src, time = pause_between, loop = -1) //move nose down and up for(var/_ in 1 to FLOP_COUNT) var/matrix/up_matrix = matrix() up_matrix.Turn(FLOP_DEGREE) var/matrix/down_matrix = matrix() down_matrix.Turn(-FLOP_DEGREE) animate(transform = down_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1) animate(transform = up_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1) animate(transform = matrix(), time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN) animate(time = PAUSE_BETWEEN_FLOPS, loop = -1) //bounce up and down animate(time = pause_between, loop = -1, flags = ANIMATION_PARALLEL) var/jumping_right = FALSE var/up_time = 3 * FLOP_SINGLE_MOVE_TIME / 2 for(var/_ in 1 to FLOP_COUNT) jumping_right = !jumping_right var/x_step = jumping_right ? JUMP_X_DISTANCE/2 : -JUMP_X_DISTANCE/2 animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN) animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT) animate(time = PAUSE_BETWEEN_FLOPS, loop = -1) #undef PAUSE_BETWEEN_PHASES #undef PAUSE_BETWEEN_FLOPS #undef FLOP_COUNT #undef FLOP_DEGREE #undef FLOP_SINGLE_MOVE_TIME #undef JUMP_X_DISTANCE #undef JUMP_Y_DISTANCE /// Starts flopping animation /obj/item/fish/proc/start_flopping() if(HAS_TRAIT(src, TRAIT_FISH_FLOPPING)) //Requires update_transform/animate_wrappers to be less restrictive. return ADD_TRAIT(src, TRAIT_FISH_FLOPPING, TRAIT_GENERIC) flop_animation() /// Stops flopping animation /obj/item/fish/proc/stop_flopping() if(HAS_TRAIT(src, TRAIT_FISH_FLOPPING)) REMOVE_TRAIT(src, TRAIT_FISH_FLOPPING, TRAIT_GENERIC) animate(src, transform = matrix()) //stop animation /// Refreshes flopping animation after temporary animation finishes /obj/item/fish/proc/on_temp_animation(datum/source, animation_duration) if(animation_duration > 0) addtimer(CALLBACK(src, PROC_REF(refresh_flopping)), animation_duration) /obj/item/fish/proc/refresh_flopping() if(HAS_TRAIT(src, TRAIT_FISH_FLOPPING)) flop_animation() /obj/item/fish/proc/try_electrogenesis() if(status == FISH_DEAD || get_starvation_mult()) return COOLDOWN_START(src, electrogenesis_cooldown, ELECTROGENESIS_DURATION + ELECTROGENESIS_VARIANCE) var/fish_zap_range = 1 var/fish_zap_power = 1 KILO JOULES // ~5 damage, just a little friendly "yeeeouch!" var/fish_zap_flags = ZAP_MOB_DAMAGE if(HAS_TRAIT(loc, TRAIT_BIOELECTRIC_GENERATOR)) fish_zap_range = 5 fish_zap_power = GET_FISH_ELECTROGENESIS(src) if(HAS_TRAIT(src, TRAIT_FISH_ON_TESLIUM)) fish_zap_power *= 0.5 fish_zap_flags |= (ZAP_GENERATES_POWER | ZAP_MOB_STUN) tesla_zap(source = get_turf(src), zap_range = fish_zap_range, power = fish_zap_power, cutoff = 1 MEGA JOULES, zap_flags = fish_zap_flags) ///The multiplier of the factor of size and weight of the fish, used to determinate the raw price before exponentation #define FISH_PRICE_MULTIPLIER 0.01 ///This makes each additional unit of fish weight and size yields diminishing marginal returns. #define FISH_PRICE_CURVE_EXPONENT 0.85 /** * past this threshold, the price of fish will plateu even faster. * This stops particularly huge fish from being an overly efficient way to make money * that bypasses price elasticity by selling fewer units. */ #define FISH_PRICE_SOFT_CAP_THRESHOLD 6000 ///The second exponent used for soft-capping the fish price. #define FISH_PRICE_SOFT_CAP_EXPONENT 0.86 ///Returns the price of this fish, for the fish export. /obj/item/fish/proc/get_export_price(price) var/size_weight_exponentation = (size * weight * FISH_PRICE_MULTIPLIER)**FISH_PRICE_CURVE_EXPONENT var/raw_price = price + size_weight_exponentation if(raw_price >= FISH_PRICE_SOFT_CAP_THRESHOLD + 1) var/soft_cap = (raw_price - FISH_PRICE_SOFT_CAP_THRESHOLD)**FISH_PRICE_SOFT_CAP_EXPONENT raw_price = FISH_PRICE_SOFT_CAP_THRESHOLD + soft_cap if(HAS_TRAIT(src, TRAIT_FISH_LOW_PRICE)) //Avoid printing money by simply ordering fish and sending it back. raw_price *= 0.05 return raw_price #undef FISH_PRICE_MULTIPLIER #undef FISH_PRICE_CURVE_EXPONENT #undef FISH_PRICE_SOFT_CAP_THRESHOLD #undef FISH_PRICE_SOFT_CAP_EXPONENT /obj/item/fish/proc/get_happiness_value() var/happiness_value = 0 if(fish_flags & FISH_FLAG_PETTED) happiness_value++ if(get_hunger() < 0.5) happiness_value++ if(loc && HAS_TRAIT(loc, TRAIT_IS_AQUARIUM)) if(fish_flags & FISH_FLAG_SAFE_FLUID) happiness_value++ if(fish_flags & FISH_FLAG_SAFE_TEMPERATURE) happiness_value++ else if(proper_environment()) happiness_value += 2 if(bites_amount) // ouch happiness_value -= 2 if(get_health_percentage() < 0.6) happiness_value -= 1 return clamp(happiness_value, FISH_SAD, FISH_VERY_HAPPY) /obj/item/fish/attack_self(mob/living/user) . = ..() try_pet_fish(user) /obj/item/fish/proc/try_pet_fish(mob/living/user) var/in_aquarium = loc && HAS_TRAIT(loc, TRAIT_IS_AQUARIUM) if(status == FISH_DEAD) to_chat(user, span_warning("You try to pet [src], but [p_theyre()] motionless!")) return FALSE if(!proper_environment()) to_chat(user, span_warning("You try to pet [src], but [p_theyre()] not feeling well!")) return FALSE return pet_fish(user, in_aquarium) /obj/item/fish/proc/pet_fish(mob/living/user, in_aquarium) if(fish_flags & FISH_FLAG_PETTED) if(in_aquarium) to_chat(user, span_warning("[src] runs away from your finger as you dip it into the water!")) else to_chat(user, span_warning("You try to pet [src] but [p_they()] squirms away!")) return FALSE if(HAS_TRAIT(src, TRAIT_FISH_ELECTROGENESIS) && GET_FISH_ELECTROGENESIS(src) > 15 MEGA JOULES) user.electrocute_act(5, src) //was it all worth it? fish_flags |= FISH_FLAG_PETTED new /obj/effect/temp_visual/heart(get_turf(src)) if((/datum/fish_trait/predator in fish_traits) && prob(50)) if(in_aquarium) user.visible_message( span_warning("[src] dances around before biting [user]!"), span_warning("[src] dances around before biting you!"), vision_distance = DEFAULT_MESSAGE_RANGE - 3, ) else user.visible_message( span_warning("[src] bites [user]'s hand!"), span_warning("You pet [src] as you hold [p_they()], only for [p_them()] to happily bite back!"), vision_distance = DEFAULT_MESSAGE_RANGE - 3, ) var/body_zone = pick(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM) user.apply_damage((force * 0.2) + w_class * 2, BRUTE, body_zone, user.run_armor_check(body_zone, MELEE)) playsound(src,'sound/items/weapons/bite.ogg', 45, TRUE, -1) else if(in_aquarium) to_chat(user, span_notice("[src] dances around!")) else to_chat(user, span_notice("You pet [src] as you hold [p_they()].")) user.add_mood_event("petted_fish", /datum/mood_event/fish_petting, src, HAS_MIND_TRAIT(user, TRAIT_MORBID)) playsound(src, 'sound/items/weapons/thudswoosh.ogg', 30, TRUE, -1) addtimer(CALLBACK(src, PROC_REF(undo_petted)), 30 SECONDS) return TRUE /obj/item/fish/proc/undo_petted() fish_flags &= ~FISH_FLAG_PETTED ///Proc called in trophy_fishes.dm, when a fish is mounted on persistent trophy mounts /obj/item/fish/proc/persistence_save(list/data) return ///Proc called in trophy_fishes.dm, when a persistent fishing trophy mount is spawned and the fish instantiated /obj/item/fish/proc/persistence_load(list/data) return /// Returns random fish, using random_case_rarity probabilities. /proc/random_fish_type(required_fluid) var/static/probability_table var/argkey = "fish_[required_fluid]" //If this expands more extract bespoke element arg generation to some common helper. if(!probability_table || !probability_table[argkey]) if(!probability_table) probability_table = list() var/chance_table = list() for(var/_fish_type in subtypesof(/obj/item/fish)) var/obj/item/fish/fish = _fish_type var/rarity = initial(fish.random_case_rarity) if(!rarity) continue if(required_fluid) var/init_fish_fluid_type = initial(fish.required_fluid_type) if(!(required_fluid in GLOB.fish_compatible_fluid_types[init_fish_fluid_type])) continue chance_table[fish] = initial(fish.random_case_rarity) probability_table[argkey] = chance_table return pick_weight(probability_table[argkey]) #undef GET_FISH_ELECTROGENESIS #undef FISH_SAD #undef FISH_VERY_HAPPY #undef FISH_SUBMERGING_THRESHOLD #undef STARVING_FISH_SUBMERGING_THRESHOLD