Files
Bubberstation/code/modules/fishing/fish/_fish.dm
Ghom 1f3894e793 Crafting refactor, implementing materials (#89465)
My original plan was to just implement materials into crafting so that
items would inherit the materials of their components, allowing for some
interesting stuff if the material flags of the item allow it. However to
my dismay crafting is a pile of old tech debt, starting from the old
`del_reqs` and `CheckParts` which still contain lines about old janky
bandaids that are no longer in use nor reachable, up to the
`customizable_reagent_holder` component which has some harddel issues
when your custom food is sliced, and items used in food recipes not
being deleted and instead stored inside the result with no purpose as
well as other inconsistencies like stack recipes that transfer materials
having counterparts in the UI that don't do that.

EDIT: Several things have come up while working on this, so I apologise
that it ended up changing over 100+ files. I managed to atomize some of
the changes, but it's a bit tedious.

EDIT: TLDR because I was told this section is too vague and there's too
much going on. This PR:
- Improves the dated crafting code (not the UI).
- replaced `atom/CheckParts` and `crafting_recipe/on_craft_completion`
with `atom/on_craft_completion`.
- Reqs used in food recipes are now deleted by default and not stored
inside the result (they did nothing).
- Renames the customizable_reagent_holder comp and improves it (No
harddels/ref issues).
- Adds a unit test that tries to craft all recipes to see what's wrong
(it skips some of the much more specific reqs for now).
- In the unit test is also the code to make sure materials of the
crafted item and a non-crafted item of the same type are roughly the
same, so far only applied to food.
- Some mild material/food refactoring around the fact that food item
code has been changed to support materials.

Improving the backbone of the crafting system. Also materials and food
code.

🆑
refactor: Refactored crafting backend. Report possible pesky bugs.
balance: the MEAT backpack (from the MEAT cargo pack) may be a smidge
different because of code standardization.
/🆑
2025-06-05 20:05:13 -04:00

1591 lines
63 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
/// 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
/// Current fish health. Dies at 0.
var/health = 100
/// 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 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)) //Composting a food that is not real food wouldn't work anyway.
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_TRAIT(src, 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)
adjust_health(0)
//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))
adjust_health(0)
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
adjust_health(health - (initial(health) / 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, 0.4, include_source_subtypes = TRUE)
reagents.convert_reagent(/datum/reagent/consumable/nutriment/protein, /datum/reagent/blood, 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 = health / initial(health)
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)
bare_wound_bonus = initial(bare_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
health = initial(health) // since the fishe has been revived
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, health))
if(!isnum(var_value))
return FALSE
adjust_health(health)
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"
visual.color = aquarium_vc_color
/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_per_second = 0
if(!proper_environment())
health_change_per_second -= 2.5 //Dying here
var/starvation_mult = get_starvation_mult()
if(starvation_mult)
health_change_per_second -= 0.25 * starvation_mult //Starving
else
health_change_per_second += 0.5 //Slowly healing
if(HAS_TRAIT(src, TRAIT_FISH_ON_TESLIUM))
health_change_per_second -= 0.65
adjust_health(health + health_change_per_second * seconds_per_tick)
/obj/item/fish/proc/adjust_health(amount)
if(status == FISH_DEAD || amount == health)
return
var/pre_health = health
var/initial_health = initial(health)
health = clamp(amount, 0, initial_health)
if(health <= 0)
set_status(FISH_DEAD)
return
if(amount < pre_health || !bites_amount)
return
var/health_to_pre_health_diff = amount - pre_health
var/init_health_to_pre_diff = initial_health - pre_health
var/bites_to_recover = bites_amount * (health_to_pre_health_diff / init_health_to_pre_diff)
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 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) && health >= initial(health) * 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(health < initial(health) * 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
/obj/item/fish/update_atom_colour()
. = ..()
aquarium_vc_color = color || initial(aquarium_vc_color)
///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