Files
Bubberstation/code/modules/fishing/fish/_fish.dm
SmArtKar 2f4c4a3e92 Fixes fishes not dying on dry land (#92493)
## About The Pull Request

- Closes #92474

## Changelog
🆑
fix: Fixed fishes not dying on dry land
/🆑
2025-08-11 20:16:16 +02:00

1603 lines
64 KiB
Plaintext

#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"
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 = 1
/// 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 grams. 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 grams
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)
//stops new fish from being able to 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()
/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)
return MANUAL_SUICIDE
/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, "it's 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 it 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("It's [size] cm long.")
. += span_notice("It weighs [weight] g.")
. += get_health_warnings(user, always_deep = FALSE)
if(HAS_TRAIT(src, TRAIT_FISHING_BAIT))
. += span_smallnoticeital("It can be used as a fishing bait.")
if(bites_amount)
. += span_warning("It's 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("It's [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("It's [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)
for(var/reagent_type in grind_results)
grind_results[reagent_type] /= max(FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1), 0.1)
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()
for(var/reagent_type in grind_results)
grind_results[reagent_type] *= max(FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1), 0.1)
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 = GET_MATERIAL_REF(mat_type)
unmodified_weight /= GET_MATERIAL_MODIFIER(material.fish_weight_modifier, 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)
if(material_flags & MATERIAL_EFFECTS && length(custom_materials)) //struck by metal gen or something.
var/multiplier = 1 / length(custom_materials)
if(material_flags & MATERIAL_AFFECT_STATISTICS)
for(var/current_material in custom_materials)
var/datum/material/material = GET_MATERIAL_REF(current_material)
force *= GET_MATERIAL_MODIFIER(material.strength_modifier, multiplier)
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)
throwforce = force
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)
return
material_weight_mult *= GET_MATERIAL_MODIFIER(custom_material.fish_weight_modifier, 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) || material_weight_mult == 1)
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) || material_weight_mult == 1)
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, "it's 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 && stable_population >= 1 && 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)
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
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
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)
/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 + 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)
breeding_wait = world.time + breeding_timeout
return new_fish
#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, elasticity_percent)
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 * elasticity_percent
#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 it, 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 it."))
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