mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-25 16:45:42 +00:00
* Fishing, Version 1 (#67691) Adds fishing and fishing minigame. You use fishing rod to fish. Equipping specific bait/hook/reels will affect your success chances. You can fish out fish,items and other things. Fishing Equipment Fishing rods have three slots: Bait, Reel and Hook. Any food can be used as bait but dedicated bait makes fishing easier. You can buy hook and line sets New bait types: Worms : Buy can of them at cargo (alternative acquirement method pending) Doughballs : Use knife on flat piece of dough to get five of them. Fishing rod types: Basic : Print these at the lathe, nothing fancy here. Tech: Experimental tech. Provides infinite bait Fishing rods can also hook and reel normal items. Equipment screen and reeling video Fishing spots Keep in mind this PR is meant to add the basic systems and i intend to fill these with more fish in future PR's so wait with suggestions until then. Lavaland lava (no fish here right now, just other stuff), requires reinforced line to fish in. Maintenance moisture traps. Beach away mission water. Fishing portal available for purchase from cargo - This is stopgap until we fill more spots. Difficulty depends on fishing spot, fish type, and the fish traits and rod setup combinations. All fish types can have specific traits, most common ones being favourite and disliked bait types/categories. Other Fishing catalog now lists fishing related info New admin debug verb, fishing calculator that show probabilities with different setups so it's easier to balance this. Fish now have average weight and size. Make sure to boast if you catch a big one. Adds tgui mouse passthrough Screens Sprites: Fishing portal sprite by @ ArcaneMusic Other sprites by @ Mey-Ha-Zah Bad ones by me. (Could still use better fishing minigame backgrounds) Sounds: https://freesound.org/people/soundscalpel.com/sounds/110393/ https://freesound.org/people/soundslikewillem/sounds/343748/ * Fishing, Version 1 Co-authored-by: AnturK <AnturK@users.noreply.github.com>
371 lines
12 KiB
Plaintext
371 lines
12 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"
|
|
|
|
w_class = WEIGHT_CLASS_TINY
|
|
|
|
/// 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 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/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
|
|
|
|
/// Current fish health. Dies at 0.
|
|
var/health = 100
|
|
|
|
/// Should this fish type show in fish catalog
|
|
var/show_in_catalog = TRUE
|
|
/// Should this fish spawn in random fish cases
|
|
var/available_in_random_cases = 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
|
|
|
|
/// Won't breed more than this amount in single aquarium.
|
|
var/stable_population = 1
|
|
/// Last time new fish was created
|
|
var/last_breeding
|
|
/// How long it takes to produce new fish
|
|
var/breeding_timeout = 2 MINUTES
|
|
|
|
var/flopping = FALSE
|
|
|
|
var/in_stasis = FALSE
|
|
|
|
// Fishing related properties
|
|
|
|
/// List of fishing trait types, these modify probabilty/difficulty depending on rod/user properties
|
|
var/list/fishing_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
|
|
var/size = 50
|
|
/// 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
|
|
var/weight = 1000
|
|
/// Average weight for this fish type in grams
|
|
var/average_weight = 1000
|
|
|
|
|
|
|
|
/obj/item/fish/Initialize(mapload)
|
|
. = ..()
|
|
if(fillet_type)
|
|
AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, 1, 5)
|
|
AddComponent(/datum/component/aquarium_content, .proc/get_aquarium_animation, list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
|
|
RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, .proc/on_temp_animation)
|
|
|
|
check_environment_after_movement()
|
|
if(status != FISH_DEAD)
|
|
START_PROCESSING(SSobj, src)
|
|
|
|
size = average_size
|
|
weight = average_weight
|
|
|
|
/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.")
|
|
|
|
/obj/item/fish/proc/randomize_weight_and_size(modifier = 0)
|
|
var/size_deviation = 0.2 * average_size
|
|
var/size_mod = modifier * average_size
|
|
size = max(1,gaussian(average_size + size_mod, size_deviation))
|
|
|
|
var/weight_deviation = 0.2 * average_weight
|
|
var/weight_mod = modifier * average_weight
|
|
weight = max(1,gaussian(average_weight + weight_mod, weight_deviation))
|
|
|
|
/obj/item/fish/Moved(atom/OldLoc, Dir)
|
|
. = ..()
|
|
check_environment_after_movement()
|
|
|
|
/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)
|
|
|
|
/obj/item/fish/proc/on_aquarium_insertion(obj/structure/aquarium)
|
|
if(isnull(last_feeding)) //Fish start fed.
|
|
last_feeding = world.time
|
|
RegisterSignal(aquarium, COMSIG_ATOM_EXITED, .proc/aquarium_exited)
|
|
RegisterSignal(aquarium, COMSIG_PARENT_ATTACKBY, .proc/attack_reaction)
|
|
|
|
/obj/item/fish/proc/aquarium_exited(datum/source, atom/movable/gone, direction)
|
|
SIGNAL_HANDLER
|
|
if(src != gone)
|
|
return
|
|
UnregisterSignal(source,list(COMSIG_ATOM_EXITED,COMSIG_PARENT_ATTACKBY))
|
|
|
|
/// Our aquarium is hit with stuff
|
|
/obj/item/fish/proc/attack_reaction(datum/source, obj/item/thing, mob/user, params)
|
|
SIGNAL_HANDLER
|
|
if(is_food(thing))
|
|
on_feeding(thing.reagents)
|
|
return COMPONENT_NO_AFTERATTACK
|
|
else
|
|
//stirred effect
|
|
SEND_SIGNAL(src, COMSIG_FISH_STIRRED)
|
|
|
|
/obj/item/fish/proc/is_food(obj/item/thing)
|
|
return istype(thing, /obj/item/fish_feed)
|
|
|
|
/obj/item/fish/proc/on_feeding(datum/reagents/feed_reagents)
|
|
if(feed_reagents.has_reagent(food))
|
|
last_feeding = world.time
|
|
|
|
/obj/item/fish/proc/check_environment_after_movement()
|
|
if(QDELETED(src)) //we don't care anymore
|
|
return
|
|
// Apply/remove stasis as needed
|
|
if(loc && HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE))
|
|
enter_stasis()
|
|
else if(in_stasis)
|
|
exit_stasis()
|
|
|
|
// Do additional stuff
|
|
var/in_aquarium = istype(loc,/obj/structure/aquarium)
|
|
if(in_aquarium)
|
|
on_aquarium_insertion(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(delta_time)
|
|
if(in_stasis || status != FISH_ALIVE)
|
|
return
|
|
|
|
process_health(delta_time)
|
|
if(ready_to_reproduce())
|
|
try_to_reproduce()
|
|
|
|
/obj/item/fish/proc/set_status(new_status)
|
|
switch(new_status)
|
|
if(FISH_ALIVE)
|
|
status = FISH_ALIVE
|
|
health = initial(health) // this is admin option anyway
|
|
START_PROCESSING(SSobj, src)
|
|
if(FISH_DEAD)
|
|
status = FISH_DEAD
|
|
STOP_PROCESSING(SSobj, src)
|
|
stop_flopping()
|
|
var/message = span_notice("\The [name] dies.")
|
|
if(istype(loc,/obj/structure/aquarium))
|
|
loc.visible_message(message)
|
|
else
|
|
visible_message(message)
|
|
SEND_SIGNAL(src, COMSIG_FISH_STATUS_CHANGED)
|
|
|
|
/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))
|
|
return FALSE
|
|
|
|
if(required_fluid_type != AQUARIUM_FLUID_ANADROMOUS)
|
|
if(aquarium.fluid_type != required_fluid_type)
|
|
return FALSE
|
|
else
|
|
if(aquarium.fluid_type != AQUARIUM_FLUID_SALTWATER && aquarium.fluid_type != AQUARIUM_FLUID_FRESHWATER)
|
|
return FALSE
|
|
if(aquarium.fluid_temp < required_temperature_min || aquarium.fluid_temp > required_temperature_max)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/obj/item/fish/proc/process_health(delta_time)
|
|
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 * delta_time)
|
|
|
|
/obj/item/fish/proc/adjust_health(amt)
|
|
health = clamp(amt, 0, initial(health))
|
|
if(health <= 0)
|
|
set_status(FISH_DEAD)
|
|
|
|
|
|
/obj/item/fish/proc/ready_to_reproduce()
|
|
var/obj/structure/aquarium/aquarium = loc
|
|
if(!istype(aquarium))
|
|
return FALSE
|
|
return aquarium.allow_breeding && health == initial(health) && stable_population > 1 && world.time - last_breeding >= breeding_timeout
|
|
|
|
//Fish breeding stops if fish count exceeds this.
|
|
#define AQUARIUM_MAX_BREEDING_POPULATION 20
|
|
/obj/item/fish/proc/try_to_reproduce()
|
|
var/obj/structure/aquarium/aquarium = loc
|
|
if(!istype(aquarium))
|
|
return
|
|
if(length(aquarium.tracked_fish) >= AQUARIUM_MAX_BREEDING_POPULATION) //so aquariums full of fish don't need to do these expensive checks
|
|
return
|
|
var/list/other_fish_of_same_type = list()
|
|
for(var/obj/item/fish/fish_in_aquarium in aquarium)
|
|
if(fish_in_aquarium == src || fish_in_aquarium.type != type)
|
|
continue
|
|
other_fish_of_same_type += fish_in_aquarium
|
|
if(length(other_fish_of_same_type) >= stable_population)
|
|
return
|
|
var/obj/item/fish/second_fish
|
|
for(var/obj/item/fish/other_fish in other_fish_of_same_type)
|
|
if(other_fish.ready_to_reproduce())
|
|
second_fish = other_fish
|
|
break
|
|
if(second_fish)
|
|
new type(loc) //could use child_type var
|
|
last_breeding = world.time
|
|
second_fish.last_breeding = world.time
|
|
#undef AQUARIUM_MAX_BREEDING_POPULATION
|
|
|
|
#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.
|
|
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/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(case_fish_only=TRUE, required_fluid)
|
|
var/static/probability_table
|
|
var/argkey = "fish_[required_fluid]_[case_fish_only]" //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
|
|
if(required_fluid && initial(fish.required_fluid_type) != required_fluid)
|
|
continue
|
|
if(initial(fish.available_in_random_cases) || !case_fish_only)
|
|
chance_table[fish] = initial(fish.random_case_rarity)
|
|
probability_table[argkey] = chance_table
|
|
return pick_weight(probability_table[argkey])
|
|
|
|
|