mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 07:46:20 +00:00
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. /🆑
1591 lines
63 KiB
Plaintext
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
|