Files
Bubberstation/code/modules/fishing/fish/_fish.dm
T
SkyratBot 901f0b82bc [MIRROR] Miscellaneous fishing code changes. [MDB IGNORE] (#23237)
* Miscellaneous fishing code changes. (#77739)

## About The Pull Request
This PR contains a few changes that I hadn't got to do earlier,
including: different pressure / air mixture thresholds for different
fish (if amphibious), fish being able to be fed directly without the
need of an aquarium, replacing the `available_in_random_cases` variable
with a weight define of value 0, the preset fishing sources global list
so we don't have to manually instantiate lazy fishing spots and assign
them stupid string defines, chasm detritus made into datums, a couple
balloon alerts and removal of unused code.

## Why It's Good For The Game
The fishing portal generator UI is unused, the perfect variable for the
fishing minigame is also unused.
There's no reason for chasm detritus to be an item instead of a datum.
It isn't a map spawner.
Chasm chrabs, if given the amphibious trait, should be able to survive
Lavaland/Icemoon's atmosphere.
I don't even know why I made a snowflake proc to instantiate the
evolutions global list instead of `init_subtypes_w_path_keys`
The shiny lover and wary fish traits were actually making the minigame
slightly easier.
The background icons for the UI had a zero-alpha, one pixel thin stripe
on top that needed to be colored.
Improved `fish_source/proc/dispense_reward`.
Some doc comments and a typo or two.

## Changelog

🆑
add: You can now feed fish with the can of fish feed without having to
put the fish in a aquarium first.
balance: Some fish may survive in different, harsher atmospheres if
given the amphibious trait, like chasm chrabs on lavaland.
qol: aquarium now uses balloon alerts when feeding fish.
fix: The wary and shiny lover no longer incorrectly remove difficulty
from the minigame if conditions aren't met.
/🆑

* Miscellaneous fishing code changes.

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2023-08-21 11:39:47 -04:00

627 lines
23 KiB
Plaintext

// Fish path used for autogenerated fish
/obj/item/fish
name = "generic looking aquarium fish"
desc = "very bland"
icon = 'icons/obj/aquarium.dmi'
icon_state = "bugfish"
lefthand_file = 'icons/mob/inhands/fish_lefthand.dmi'
righthand_file = 'icons/mob/inhands/fish_righthand.dmi'
inhand_icon_state = "fish_normal"
force = 6
attack_verb_continuous = list("slaps", "whacks")
attack_verb_simple = list("slap", "whack")
hitsound = 'sound/weapons/slap.ogg'
///The grind results of the fish. They scale with the weight of the fish.
grind_results = list(/datum/reagent/blood = 20, /datum/reagent/consumable/liquidgibs = 5)
obj_flags = UNIQUE_RENAME
/// Resulting width of aquarium visual icon - default size of "fish_greyscale" state
var/sprite_width = 3
/// Resulting height of aquarium visual icon - default size of "fish_greyscale" state
var/sprite_height = 3
/// Original width of aquarium visual icon - used to calculate scaledown factor
var/source_width = 32
/// Original height of aquarium visual icon - used to calculate scaledown factor
var/source_height = 32
/**
* If present and it also has a dedicated icon state, this icon file will
* be used for in-aquarium visual for the fish instead of its icon
*/
var/dedicated_in_aquarium_icon
/// If present this icon will be used for in-aquarium visual for the fish instead of icon_state
var/dedicated_in_aquarium_icon_state
/// If present aquarium visual will be 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 feedeing
var/last_feeding
/// Fish status
var/status = FISH_ALIVE
///icon used when the fish is dead, ifset.
var/icon_state_dead
///If this fish should do the flopping animation
var/do_flop_animation = TRUE
/// Current fish health. Dies at 0.
var/health = 100
/// The message shown when the fish dies.
var/death_text = "%SRC dies."
/// Should this fish type show in fish catalog
var/show_in_catalog = TRUE
/// How rare this fish is in the random cases
var/random_case_rarity = FISH_RARITY_BASIC
/// Fish autogenerated from this behaviour will be processable into this
var/fillet_type = /obj/item/food/fishmeat
/// number of fillets given by the fish. It scales with its size.
var/num_fillets = 1
/// Won't breed more than this amount in single aquarium.
var/stable_population = 1
/// The time limit before new fish can be created
var/breeding_wait
/// How long it takes to produce new fish
var/breeding_timeout = 2 MINUTES
/// If set, the fish can also breed with these fishes types
var/list/compatible_types
/// A list of possible evolutions. If set, offsprings may be of a different, new fish type if conditions are met.
var/list/evolution_types
/// The species' name(s) of the parents of the fish. Shown by the fish analyzer.
var/progenitors
var/flopping = FALSE
var/in_stasis = FALSE
// 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()
/// Fishing behaviour
var/fish_ai_type = FISH_AI_DUMB
/// 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
/// 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
/// 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
/obj/item/fish/Initialize(mapload, apply_qualities = TRUE)
. = ..()
AddComponent(/datum/component/aquarium_content, PROC_REF(get_aquarium_animation), list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
RegisterSignal(src, COMSIG_ATOM_ON_LAZARUS_INJECTOR, PROC_REF(use_lazarus))
if(do_flop_animation)
RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, PROC_REF(on_temp_animation))
check_environment()
if(status != FISH_DEAD)
START_PROCESSING(SSobj, src)
//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()
progenitors = full_capitalize(name) //default value
register_evolutions()
/obj/item/fish/update_icon_state()
if(status == FISH_DEAD && icon_state_dead)
icon_state = icon_state_dead
else
icon_state = initial(icon_state)
return ..()
/obj/item/fish/attackby(obj/item/item, mob/living/user, params)
if(!istype(item, /obj/item/fish_feed))
return ..()
if(!item.reagents.total_volume)
balloon_alert(user, "[item] is empty!")
return TRUE
if(status == FISH_DEAD)
balloon_alert(user, "[src] is dead!")
return TRUE
feed(item.reagents)
balloon_alert(user, "you feed [src]")
return TRUE
/obj/item/fish/examine(mob/user)
. = ..()
// All spacemen have magic eyes of fish weight perception until fish scale (get it?) is implemented.
. += span_notice("It's [size] cm long.")
. += span_notice("It weighs [weight] g.")
///Randomizes weight and size.
/obj/item/fish/proc/randomize_size_and_weight(avg_size = average_size, avg_weight = average_weight, deviation = 0.2, first_run = FALSE)
var/size_deviation = 0.2 * avg_size
var/new_size = round(max(1,gaussian(avg_size, size_deviation)), 1)
var/weight_deviation = 0.2 * avg_weight
var/new_weight = round(max(1,gaussian(avg_weight, weight_deviation)), 1)
update_size_and_weight(new_size, new_weight, first_run)
///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)
if(size && fillet_type)
RemoveElement(/datum/element/processable, TOOL_KNIFE, fillet_type, num_fillets, 0.5 SECONDS, screentip_verb = "Cut")
size = new_size
switch(size)
if(0 to FISH_SIZE_TINY_MAX)
w_class = WEIGHT_CLASS_TINY
inhand_icon_state = "fish_small"
if(FISH_SIZE_TINY_MAX to FISH_SIZE_SMALL_MAX)
inhand_icon_state = "fish_small"
w_class = WEIGHT_CLASS_SMALL
if(FISH_SIZE_SMALL_MAX to FISH_SIZE_NORMAL_MAX)
inhand_icon_state = "fish_normal"
w_class = WEIGHT_CLASS_NORMAL
if(FISH_SIZE_NORMAL_MAX to FISH_SIZE_BULKY_MAX)
inhand_icon_state = "fish_bulky"
w_class = WEIGHT_CLASS_BULKY
if(FISH_SIZE_BULKY_MAX to INFINITY)
inhand_icon_state = "fish_huge"
w_class = WEIGHT_CLASS_HUGE
if(fillet_type)
var/init_fillets = initial(num_fillets)
var/amount = max(round(init_fillets * size / FISH_FILLET_NUMBER_SIZE_DIVISOR, 1), 1)
num_fillets = amount
AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, num_fillets, 0.5 SECONDS, screentip_verb = "Cut")
if(weight)
for(var/reagent_type in grind_results)
grind_results[reagent_type] /= FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1)
weight = new_weight
for(var/reagent_type in grind_results)
grind_results[reagent_type] *= FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.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 = (x_traits|y_traits)-removed_traits
/**
* Traits that the fish is guaranteed to inherit will be inherited,
* with the assertion that they're compatible anyway.
*/
for(var/trait_type in all_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
if(type in trait.guaranteed_inheritance_types)
fish_traits |= trait_type
all_traits -= trait_type
///Build a list of incompatible traits. Don't let any such trait pass onto the fish.
var/list/incompatible_traits = list()
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(length(fish_traits & trait.incompatible_traits))
continue
if((trait_type in same_traits) ? prob(trait.inheritability) : prob(trait.diff_traits_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/proc/register_evolutions()
for(var/evolution_type in evolution_types)
var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type]
evolution.register_fish(src)
/obj/item/fish/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
check_environment()
/obj/item/fish/proc/enter_stasis()
in_stasis = TRUE
// Stop processing until inserted into aquarium again.
stop_flopping()
STOP_PROCESSING(SSobj, src)
/obj/item/fish/proc/exit_stasis()
in_stasis = FALSE
if(status != FISH_DEAD)
START_PROCESSING(SSobj, src)
///Feed the fishes with the contents of the fish feed
/obj/item/fish/proc/feed(datum/reagents/fed_reagents)
if(status != FISH_ALIVE)
return
var/fed_reagent_type
if(fed_reagents.remove_reagent(food, 0.1))
fed_reagent_type = food
last_feeding = world.time
else
var/datum/reagent/wrong_reagent = pick(fed_reagents.reagent_list)
fed_reagent_type = wrong_reagent.type
fed_reagents.remove_reagent(fed_reagent_type, 0.1)
SEND_SIGNAL(src, COMSIG_FISH_FED, fed_reagents, fed_reagent_type)
/obj/item/fish/proc/check_environment(stasis_check = TRUE)
if(QDELETED(src)) //we don't care anymore
return
if(stasis_check)
// Apply/remove stasis as needed
if(loc && HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE))
enter_stasis()
else if(in_stasis)
exit_stasis()
if(!do_flop_animation)
return
// Do additional stuff
var/in_aquarium = isaquarium(loc)
// Start flopping if outside of fish container
var/should_be_flopping = status == FISH_ALIVE && loc && !HAS_TRAIT(loc,TRAIT_FISH_SAFE_STORAGE) && !in_aquarium
if(should_be_flopping)
start_flopping()
else
stop_flopping()
/obj/item/fish/process(seconds_per_tick)
if(in_stasis || status != FISH_ALIVE)
return
process_health(seconds_per_tick)
if(ready_to_reproduce())
try_to_reproduce()
SEND_SIGNAL(src, COMSIG_FISH_LIFE, seconds_per_tick)
/obj/item/fish/proc/set_status(new_status)
if(status == new_status)
return
switch(new_status)
if(FISH_ALIVE)
status = FISH_ALIVE
health = initial(health) // since the fishe has been revived
last_feeding = world.time //reset hunger
check_environment(FALSE)
START_PROCESSING(SSobj, src)
if(FISH_DEAD)
status = FISH_DEAD
STOP_PROCESSING(SSobj, src)
stop_flopping()
var/message = span_notice(replacetext(death_text, "%SRC", "[src]"))
if(isaquarium(loc))
loc.visible_message(message)
else
visible_message(message)
update_appearance()
SEND_SIGNAL(src, COMSIG_FISH_STATUS_CHANGED)
/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/get_aquarium_animation()
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium) || aquarium.fluid_type == AQUARIUM_FLUID_AIR || status == FISH_DEAD)
return AQUARIUM_ANIMATION_FISH_DEAD
else
return AQUARIUM_ANIMATION_FISH_SWIM
/// Checks if our current environment lets us live.
/obj/item/fish/proc/proper_environment()
var/obj/structure/aquarium/aquarium = loc
if(istype(aquarium))
if(!compatible_fluid_type(required_fluid_type, aquarium.fluid_type))
if(aquarium.fluid_type != AQUARIUM_FLUID_AIR || !HAS_TRAIT(src, TRAIT_FISH_AMPHIBIOUS))
return FALSE
if(!ISINRANGE(aquarium.fluid_temp, required_temperature_min, required_temperature_max))
return FALSE
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 -= 3 //Dying here
if(world.time - last_feeding >= feeding_frequency)
health_change_per_second -= 0.5 //Starving
else
health_change_per_second += 0.5 //Slowly healing
adjust_health(health + health_change_per_second * seconds_per_tick)
/obj/item/fish/proc/adjust_health(amt)
health = clamp(amt, 0, initial(health))
if(health <= 0)
set_status(FISH_DEAD)
//Fish breeding stops if fish count exceeds this.
#define AQUARIUM_MAX_BREEDING_POPULATION 20
/obj/item/fish/proc/ready_to_reproduce(being_targetted = FALSE)
var/obj/structure/aquarium/aquarium = loc
if(!istype(aquarium))
return FALSE
if(being_targetted && HAS_TRAIT(src, TRAIT_FISH_NO_MATING))
return FALSE
if(!being_targetted && length(aquarium.get_fishes()) >= AQUARIUM_MAX_BREEDING_POPULATION)
return FALSE
return aquarium.allow_breeding && health >= initial(health) * 0.8 && stable_population > 1 && world.time >= breeding_wait
#undef AQUARIUM_MAX_BREEDING_POPULATION
/obj/item/fish/proc/try_to_reproduce()
var/obj/structure/aquarium/aquarium = loc
if(!istype(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.
* Also mating takes priority over that.
*/
if(!HAS_TRAIT(src, TRAIT_FISH_NO_MATING))
var/list/available_fishes = list()
var/types_to_mate_with = aquarium.tracked_fish_by_type
if(!HAS_TRAIT(src, TRAIT_FISH_CROSSBREEDER))
var/list/types_to_check = list(src)
if(compatible_types)
types_to_check |= compatible_types
types_to_mate_with = types_to_mate_with & types_to_check
for(var/obj/item/fish/fish_type as anything in types_to_mate_with)
var/list/type_fishes = types_to_mate_with[fish_type]
if(length(type_fishes) >= initial(fish_type.stable_population))
continue
available_fishes += type_fishes
available_fishes -= src //no self-mating.
if(length(available_fishes))
for(var/obj/item/fish/other_fish as anything in shuffle(available_fishes))
if(other_fish.ready_to_reproduce(TRUE))
second_fish = other_fish
break
if(!second_fish && !HAS_TRAIT(src, TRAIT_FISH_SELF_REPRODUCE))
return FALSE
var/chosen_type
var/datum/fish_evolution/chosen_evolution
if(PERFORM_ALL_TESTS(fish_breeding) && second_fish && !length(evolution_types))
chosen_type = second_fish.type
else
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, aquarium))
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, aquarium))
possible_evolutions += evolution
if(length(possible_evolutions))
chosen_evolution = pick(possible_evolutions)
chosen_type = chosen_evolution.new_fish_type
else if(second_fish)
if(length(aquarium.tracked_fish_by_type[type]) >= stable_population)
chosen_type = second_fish.type
else
chosen_type = pick(second_fish.type, type)
else
chosen_type = type
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(partner)
var/mean_size = (size + partner.size)/2
var/mean_weight = (weight + partner.weight)/2
new_fish.randomize_size_and_weight(mean_size, mean_weight, 0.3, TRUE)
partner.breeding_wait = world.time + breeding_timeout
else //Make a close of this fish.
new_fish.update_size_and_weight(size, weight, TRUE)
new_fish.progenitors = initial(name)
if(partner && type != partner.type)
var/string = "[initial(name)] - [initial(partner.name)]"
new_fish.progenitors = full_capitalize(string)
else
new_fish.progenitors = full_capitalize(initial(name))
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 animation should be applied to actual parent atom instead of vc_object.
/proc/flop_animation(atom/movable/animation_target)
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
animate(animation_target, 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(flopping) //Requires update_transform/animate_wrappers to be less restrictive.
return
flopping = TRUE
flop_animation(src)
/// Stops flopping animation
/obj/item/fish/proc/stop_flopping()
if(flopping)
flopping = FALSE
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(flopping)
flop_animation(src)
/// 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(!compatible_fluid_type(init_fish_fluid_type, required_fluid))
continue
chance_table[fish] = initial(fish.random_case_rarity)
probability_table[argkey] = chance_table
return pick_weight(probability_table[argkey])
/proc/compatible_fluid_type(fish_fluid_type, fluid_type)
switch(fish_fluid_type)
if(AQUARIUM_FLUID_ANY_WATER)
return fluid_type != AQUARIUM_FLUID_AIR
if(AQUARIUM_FLUID_ANADROMOUS)
return fluid_type == AQUARIUM_FLUID_SALTWATER || fluid_type == AQUARIUM_FLUID_FRESHWATER
else
return fish_fluid_type == fluid_type