Files
Bubberstation/code/modules/fishing/fishing_minigame.dm
GhomandGitHub 8c534a521e Maintenance PDA themes are added to roundstart PDAs on future rounds as well once installed (#92983)
## About The Pull Request
I'm making the ordeal of finding a maint disk (or buying blackmarket
bootleg disk) with a theme in it a slightly more rewarding experience,
while sticking to the concept that it's something you've to find, unlike
default PDA themes.

This PR also proves to be an opportunity to put the progress tab that I
coded a year ago for the 'Fishdex' to good use.

TODO:
~~Refactor preferences to allow specific choices to be shown/hidden
depending on whether the player meets a defined criteria, otherwise
you'll have to do it manually every round, which is lame~~

- [x] Make some simple ui icons associated with each unlockable theme to
be shown in the cheevo progress tab

- [x] Code to validate deserialized DB values, in the remote case that
any theme is removed in the future, as well as unit test code for any
non-abstract theme without ID

- [x] Add sound cue and chat feedback when unlocking a theme (or when
fishing a new kind of fish for the first time, like, the code's similar)

- [x] Test all of this

## Why It's Good For The Game
These themes are basically an end in itself, and I understand the reason
behind their existence is to make for some cute, little maint loot, but
relegating it to chance of finding a disk somewhere in maintenance,
**every single round** really rots whatever little substance this purely
cosmetic feature already has.

## Changelog

🆑
add: Once installed, special PDA themes from maintenance disks will be
present on your roundstart PDA on future rounds (Sadly I couldn't figure
out a way to add those to the preferences UI yet). You can check which
PDA themes you've unlocked in the Progress tab of the achievements UI.
/🆑
2026-01-16 17:18:03 -05:00

951 lines
36 KiB
Plaintext

// float bobbing
#define WAIT_PHASE 1
// Click now to start tgui part
#define BITING_PHASE 2
// UI minigame phase
#define MINIGAME_PHASE 3
/// The minimum velocity required for the bait to bounce
#define BAIT_MIN_VELOCITY_BOUNCE 150
/// Reduce initial completion rate depending on difficulty
#define MAX_FISH_COMPLETION_MALUS 15
/// The window of time between biting phase and back to baiting phase
#define BITING_TIME_WINDOW 4 SECONDS
/// The multiplier of how much the difficulty negatively impacts the bait height
#define BAIT_HEIGHT_DIFFICULTY_MALUS 1.3
/// Defines to know how the bait is moving on the minigame slider.
#define REELING_STATE_IDLE 0
#define REELING_STATE_UP 1
#define REELING_STATE_DOWN 2
/// The pixel height of the minigame bar
#define MINIGAME_SLIDER_HEIGHT 76
/// The standard pixel height of the bait
#define MINIGAME_BAIT_HEIGHT 27
/// How many pixels bottom and top parts of the bait take up
#define MINIGAME_BAIT_TOP_AND_BOTTOM_HEIGHT 6
/// The standard pixel height of the fish (minus a pixel on each direction for the sake of a better looking sprite)
#define MINIGAME_FISH_HEIGHT 4
/// Pixel height of the completion bar
#define MINIGAME_COMPLETION_BAR_HEIGHT 80
GLOBAL_LIST_EMPTY(fishing_challenges_by_user)
/datum/fishing_challenge
/// When the ui minigame phase started
var/start_time
/// Is it finished (either by win/lose or window closing)
var/completed = FALSE
/// Rule modifiers (eg weighted bait)
var/special_effects = NONE
/// A list of possible active minigame effects. If not empty, one will be picked from time to time.
var/list/active_effects
/// The cooldown between switching active effects
COOLDOWN_DECLARE(active_effect_cd)
/// The current active effect
var/current_active_effect
/// Result path
var/reward_path = FISHING_DUD
/// Minigame difficulty
var/difficulty = FISHING_DEFAULT_DIFFICULTY
/// Current phase
var/phase = WAIT_PHASE
/// Timer for the next phase
var/next_phase_timer
/// The lower and upper bounds of the waiting phase timer
var/list/wait_time_range = list(3 SECONDS, 25 SECONDS)
/// The last time we clicked during the baiting phase
var/last_baiting_click
/// Fishing mob
var/mob/living/user
/// Rod that is used for the challenge
var/obj/item/fishing_rod/used_rod
/// float visual
var/obj/effect/fishing_float/float
///The physical fishing spot our float is hovering
var/atom/location
/// Background icon state from fishing_hud.dmi
var/background = "background_default"
/// Fish icon state from fishing_hud.dmi
var/fish_icon = FISH_ICON_DEF
/// Fishing line visual
var/datum/beam/fishing_line
var/experience_multiplier = 1
/// How much space the fish takes on the minigame slider
var/fish_height = 50
/// How much space the bait takes on the minigame slider
var/bait_height = 360
/// The height in pixels of the bait bar
var/bait_pixel_height = MINIGAME_BAIT_HEIGHT
/// The height in pixels of the fish
var/fish_pixel_height = MINIGAME_FISH_HEIGHT
/// The position of the fish on the minigame slider
var/fish_position = 0
/// The position of the bait on the minigame slider
var/bait_position = 0
/// The current speed the bait is moving at
var/bait_velocity = 0
/// The completion score. If it reaches 100, it's a win. If it reaches 0, it's a loss.
var/completion = 30
/// How much completion is lost per second when the bait area is not intersecting with the fish's
var/completion_loss = 6
/// How much completion is gained per second when the bait area is intersecting with the fish's
var/completion_gain = 5
var/datum/fish_movement/mover
/// Whether the bait is idle or reeling up or down (left and right click)
var/reeling_state = REELING_STATE_IDLE
/// The acceleration of the bait while not reeling
var/gravity_velocity = -800
/// The acceleration of the bait while reeling
var/reeling_velocity = 1200
/// By how much the bait recoils back when hitting the bounds of the slider while idle. Should be never above 1
var/bait_bounce_mult = 0.6
/// The multiplier of deceleration of velocity that happens when the bait switches direction
var/deceleration_mult = 1.8
/// Multiplier of the velocity applied when the bait is overlapping the fish
var/overlap_velocity_mult = 0.6
///The background as shown in the minigame, and the holder of the other visual overlays
var/atom/movable/screen/fishing_hud/fishing_hud
///Keep track of the fish source from which we're pulling the reward
var/datum/fish_source/fish_source
/datum/fishing_challenge/New(datum/component/fishing_spot/comp, obj/item/fishing_rod/rod, mob/living/user)
src.user = user
used_rod = rod
location = comp.parent
float = new(get_turf(location), location)
float.spin_frequency = rod.spin_frequency
RegisterSignal(location, COMSIG_QDELETING, PROC_REF(on_spot_gone))
RegisterSignal(comp, COMSIG_QDELETING, PROC_REF(on_spot_gone))
register_reward_signals(comp.fish_source)
RegisterSignal(fish_source, COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE, PROC_REF(interrupt_challenge))
background = comp.fish_source.background
if(comp.fish_source.wait_time_range)
wait_time_range = comp.fish_source.wait_time_range
if(float.spin_frequency) //Using a fishing lure narrows the range a bit, for better or worse.
wait_time_range = list(wait_time_range[1] + 8 SECONDS, wait_time_range[2] - 8 SECONDS)
SEND_SIGNAL(user, COMSIG_MOB_BEGIN_FISHING, src)
SEND_SIGNAL(rod, COMSIG_ROD_BEGIN_FISHING, src)
GLOB.fishing_challenges_by_user[user] = src
/// Enable special parameters
if(rod.line)
completion_gain += 1 // Any fishing line will provide a small boost by default
if(rod.line.fishing_line_traits & FISHING_LINE_BOUNCY)
completion_loss -= 2
if(rod.line.fishing_line_traits & FISHING_LINE_STIFF)
completion_loss += 1
completion_gain -= 1
if(rod.line.fishing_line_traits & FISHING_LINE_AUTOREEL)
special_effects |= FISHING_MINIGAME_AUTOREEL
if(rod.hook)
if(rod.hook.fishing_hook_traits & FISHING_HOOK_WEIGHTED)
bait_bounce_mult = 0.1
if(rod.hook.fishing_hook_traits & FISHING_HOOK_BIDIRECTIONAL)
special_effects |= FISHING_MINIGAME_RULE_BIDIRECTIONAL
if(rod.hook.fishing_hook_traits & FISHING_HOOK_NO_ESCAPE)
special_effects |= FISHING_MINIGAME_RULE_NO_ESCAPE
if(rod.hook.fishing_hook_traits & FISHING_HOOK_ENSNARE)
completion_loss -= 2
if(rod.hook.fishing_hook_traits & FISHING_HOOK_KILL)
special_effects |= FISHING_MINIGAME_RULE_KILL
//Finish the minigame faster at higher skill. The value modifiers for fishing are negative values btw.
completion_loss += user.mind?.get_skill_modifier(/datum/skill/fishing, SKILL_VALUE_MODIFIER)/5
completion_gain -= user.mind?.get_skill_modifier(/datum/skill/fishing, SKILL_VALUE_MODIFIER)/7.5
reeling_velocity *= rod.bait_speed_mult
completion_gain *= rod.completion_speed_mult
bait_bounce_mult *= rod.bounciness_mult
deceleration_mult *= rod.deceleration_mult
gravity_velocity *= rod.gravity_mult
/**
* The overlap multiplier is lower than 1 by default and exponentiation will make it even lower,
* to offset the harder control however a bait velocity higher (or lower) than normal.
*/
overlap_velocity_mult = overlap_velocity_mult ** rod.bait_speed_mult
/datum/fishing_challenge/Destroy(force)
GLOB.fishing_challenges_by_user -= user
if(!completed)
complete(win = FALSE)
if(fishing_line)
//Stops the line snapped message from appearing everytime the minigame is over.
UnregisterSignal(fishing_line, COMSIG_QDELETING)
QDEL_NULL(fishing_line)
QDEL_NULL(float)
SStgui.close_uis(src)
user = null
used_rod = null
location = null
QDEL_NULL(mover)
return ..()
/**
* Proc responsible for registering the signals for difficulty, possible reward, and challenge completion.
* Call this if you want to override the fish source from which we roll rewards (preferably before the minigame phase).
*/
/datum/fishing_challenge/proc/register_reward_signals(datum/fish_source/new_fish_source)
if(fish_source)
fish_source.UnregisterSignal(src, list(
COMSIG_FISHING_CHALLENGE_ROLL_REWARD,
))
fish_source.UnregisterSignal(user, COMSIG_MOB_COMPLETE_FISHING)
fish_source = new_fish_source
fish_source.RegisterSignal(src, COMSIG_FISHING_CHALLENGE_ROLL_REWARD, TYPE_PROC_REF(/datum/fish_source, roll_reward_minigame))
fish_source.RegisterSignal(user, COMSIG_MOB_COMPLETE_FISHING, TYPE_PROC_REF(/datum/fish_source, on_challenge_completed))
/datum/fishing_challenge/proc/send_alert(message)
location?.balloon_alert(user, message)
/datum/fishing_challenge/proc/on_spot_gone(datum/source)
SIGNAL_HANDLER
send_alert("fishing spot gone!")
interrupt()
/datum/fishing_challenge/proc/interrupt_challenge(datum/source, reason)
SIGNAL_HANDLER
if(reason)
send_alert(reason)
interrupt()
/datum/fishing_challenge/proc/start(mob/living/user)
/// Create fishing line visuals
if(!used_rod.internal)
fishing_line = used_rod.create_fishing_line(float, user, target_py = float.pixel_y + 4)
if(isnull(fishing_line)) //couldn't create a fishing line, probably because we don't have a good line of sight.
qdel(src)
return
RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(on_line_deleted))
else //if the rod doesnt have a fishing line, then it ends when they move away
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_float_or_user_move))
RegisterSignal(float, COMSIG_MOVABLE_MOVED, PROC_REF(on_float_or_user_move))
RegisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), PROC_REF(on_hands_blocked))
RegisterSignal(user, SIGNAL_REMOVETRAIT(TRAIT_PROFOUND_FISHER), PROC_REF(no_longer_fishing))
active_effects = bitfield_to_list(special_effects & FISHING_MINIGAME_ACTIVE_EFFECTS)
// If fishing line breaks los / rod gets dropped / deleted
RegisterSignal(used_rod, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self))
user.add_mood_event("fishing", /datum/mood_event/fishing)
RegisterSignal(user, COMSIG_MOB_CLICKON, PROC_REF(handle_click))
start_baiting_phase()
to_chat(user, span_notice("You start fishing..."))
playsound(location, 'sound/effects/splash.ogg', 100)
///Set the timers for lure that need to be spun at intervals.
/datum/fishing_challenge/proc/set_lure_timers()
float.spin_ready = FALSE
addtimer(CALLBACK(src, PROC_REF(set_lure_ready)), float.spin_frequency[1], TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME)
addtimer(CALLBACK(src, PROC_REF(missed_lure)), float.spin_frequency[2], TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME)
float.update_appearance(UPDATE_OVERLAYS)
/datum/fishing_challenge/proc/set_lure_ready()
if(phase != WAIT_PHASE)
return
float.spin_ready = TRUE
float.update_appearance(UPDATE_OVERLAYS)
if(special_effects & FISHING_MINIGAME_AUTOREEL)
addtimer(CALLBACK(src, PROC_REF(auto_spin)), 0.2 SECONDS)
playsound(float, 'sound/machines/ping.ogg', 20, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
/datum/fishing_challenge/proc/auto_spin()
if(phase != WAIT_PHASE || !float.spin_ready)
return
float.spin_ready = FALSE
float.update_appearance(UPDATE_OVERLAYS)
set_lure_timers()
send_alert("spun")
/datum/fishing_challenge/proc/missed_lure()
if(phase != WAIT_PHASE)
return
send_alert("miss!")
start_baiting_phase(TRUE) //Add in another 3 to 5 seconds for not spinning the lure.
/datum/fishing_challenge/proc/on_line_deleted(datum/source)
SIGNAL_HANDLER
fishing_line = null
///The float may be out of sight if the user has moed around a corner, so the message should be displayed over him instead.
user.balloon_alert(user, user.is_holding(used_rod) ? "line snapped" : "rod dropped")
interrupt()
/datum/fishing_challenge/proc/on_float_or_user_move(datum/source)
SIGNAL_HANDLER
if(!location.IsReachableBy(user))
user.balloon_alert(user, "too far!")
interrupt()
/datum/fishing_challenge/proc/on_hands_blocked(datum/source)
SIGNAL_HANDLER
if(completed) //the rod was dropped and therefore challenge already completed.
return
user.balloon_alert(user, "hands blocked!")
interrupt()
/datum/fishing_challenge/proc/no_longer_fishing(datum/source)
SIGNAL_HANDLER
if(completed) //we already won/lost
return
user.balloon_alert(user, "interrupted!")
interrupt()
/datum/fishing_challenge/proc/handle_click(mob/living/source, atom/target, modifiers)
SIGNAL_HANDLER
if(HAS_TRAIT(source, TRAIT_HANDS_BLOCKED)) //blocked, can't do stuff
return
//Doing other stuff
if(LAZYACCESS(modifiers, SHIFT_CLICK) || LAZYACCESS(modifiers, CTRL_CLICK) || LAZYACCESS(modifiers, ALT_CLICK))
return
//You need to be actively holding on the fishing rod to use it, unless you've the profound_fisher trait.
if(!HAS_TRAIT(source, TRAIT_PROFOUND_FISHER) && source.get_active_held_item() != used_rod)
return
if(phase == WAIT_PHASE)
if(world.time < last_baiting_click + 0.25 SECONDS)
return COMSIG_MOB_CANCEL_CLICKON //Don't punish players if they accidentally double clicked.
if(float.spin_frequency)
if(!float.spin_ready)
send_alert("too early!")
start_baiting_phase(TRUE) //Add in another 3 to 5 seconds for that blunder.
else
send_alert("spun")
last_baiting_click = world.time
float.spin_ready = FALSE
set_lure_timers()
else
send_alert("miss!")
start_baiting_phase(TRUE) //Add in another 3 to 5 seconds for that blunder.
else if(phase == BITING_PHASE)
start_minigame_phase()
return COMSIG_MOB_CANCEL_CLICKON
/// Challenge interrupted by something external
/datum/fishing_challenge/proc/interrupt()
if(!completed)
experience_multiplier *= 0.5
complete(FALSE)
/datum/fishing_challenge/proc/on_attack_self(obj/item/source, mob/living/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(stop_fishing), source, user)
/datum/fishing_challenge/proc/stop_fishing(obj/item/rod, mob/living/user)
if((phase != MINIGAME_PHASE || do_after(user, 3 SECONDS, rod)) && !QDELETED(src) && !completed)
experience_multiplier *= 0.5
send_alert("stopped fishing")
complete(FALSE)
///The multiplier of the fishing experience malus if the user's level is substantially above the difficulty.
#define EXPERIENCE_MALUS_MULT 0.08
/datum/fishing_challenge/proc/complete(win = FALSE)
if(completed)
return
deltimer(next_phase_timer)
completed = TRUE
if(phase == MINIGAME_PHASE)
remove_minigame_hud()
if(!QDELETED(user) && user.mind && start_time && !(special_effects & FISHING_MINIGAME_RULE_NO_EXP))
var/seconds_spent = (world.time - start_time) * 0.1
var/extra_exp_malus = user.mind.get_skill_level(/datum/skill/fishing) - difficulty * 0.1
if(extra_exp_malus > 0)
experience_multiplier /= (1 + extra_exp_malus * EXPERIENCE_MALUS_MULT)
experience_multiplier *= used_rod.experience_multiplier
user.mind.adjust_experience(/datum/skill/fishing, round(seconds_spent * FISHING_SKILL_EXP_PER_SECOND * experience_multiplier))
if(user.mind.get_skill_level(/datum/skill/fishing) >= SKILL_LEVEL_LEGENDARY)
user.client?.give_award(/datum/award/achievement/skill/legendary_fisher, user)
if(!win)
SEND_SIGNAL(user, COMSIG_MOB_COMPLETE_FISHING, src, FALSE)
if(!QDELETED(src))
qdel(src)
return
if(reward_path != FISHING_DUD)
playsound(location, 'sound/effects/bigsplash.ogg', 100)
var/valid_achievement_catch = FALSE
if(ispath(reward_path, /obj/item/fish))
valid_achievement_catch = TRUE
else if(isfish(reward_path))
var/obj/item/fish/fishy_individual = reward_path
if(!HAS_TRAIT(fishy_individual, TRAIT_NO_FISHING_ACHIEVEMENT) && fishy_individual.status == FISH_ALIVE)
valid_achievement_catch = TRUE
if(valid_achievement_catch)
var/obj/item/fish/fish_reward = reward_path
var/obj/item/fish/path_to_use = initial(fish_reward.fish_id_redirect_path) || fish_reward
var/fish_id = initial(path_to_use.fish_id)
if(fish_id)
user.client?.give_award(/datum/award/score/progress/fish, user, fish_id, initial(path_to_use.name))
SEND_SIGNAL(user, COMSIG_MOB_COMPLETE_FISHING, src, TRUE)
if(!QDELETED(src))
qdel(src)
#undef EXPERIENCE_MALUS_MULT
/datum/fishing_challenge/proc/start_baiting_phase(penalty = FALSE)
reward_path = null //In case we missed the biting phase, set the path back to null
var/wait_time
last_baiting_click = world.time
if(penalty)
wait_time = min(timeleft(next_phase_timer) + rand(3 SECONDS, 5 SECONDS), 30 SECONDS)
else
wait_time = rand(wait_time_range[1], wait_time_range[2])
if(special_effects & FISHING_MINIGAME_AUTOREEL && wait_time >= 15 SECONDS)
wait_time = max(wait_time - 7.5 SECONDS, 15 SECONDS)
deltimer(next_phase_timer)
phase = WAIT_PHASE
//Bobbing animation
animate(float, pixel_z = 1, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_z = -1, time = 1 SECONDS, flags = ANIMATION_RELATIVE)
next_phase_timer = addtimer(CALLBACK(src, PROC_REF(start_biting_phase)), wait_time, TIMER_STOPPABLE|TIMER_DELETE_ME)
if(float.spin_frequency)
set_lure_timers()
/datum/fishing_challenge/proc/start_biting_phase()
phase = BITING_PHASE
var/list/rewards = list()
SEND_SIGNAL(src, COMSIG_FISHING_CHALLENGE_ROLL_REWARD, used_rod, user, location, rewards)
if(length(rewards))
reward_path = pick(rewards)
playsound(location, 'sound/effects/fish_splash.ogg', 100)
if(HAS_MIND_TRAIT(user, TRAIT_REVEAL_FISH))
var/possible_icon
if(isdatum(reward_path))
var/datum/reward = reward_path
possible_icon = GLOB.specific_fish_icons[reward.type]
else
possible_icon = GLOB.specific_fish_icons[reward_path]
fish_icon = possible_icon || FISH_ICON_DEF
switch(fish_icon)
if(FISH_ICON_DEF)
send_alert("fish!!!")
if(FISH_ICON_HOSTILE)
send_alert("hostile!!!")
if(FISH_ICON_STAR)
send_alert("starfish!!!")
if(FISH_ICON_CHUNKY)
send_alert("round fish!!!")
if(FISH_ICON_JELLYFISH)
send_alert("jellyfish!!!")
if(FISH_ICON_SLIME)
send_alert("slime!!!")
if(FISH_ICON_COIN)
send_alert("valuable!!!")
if(FISH_ICON_GEM)
send_alert("ore!!!")
if(FISH_ICON_CRAB)
send_alert("crustacean!!!")
if(FISH_ICON_BONE)
send_alert("bones!!!")
if(FISH_ICON_ELECTRIC)
send_alert("zappy!!!")
if(FISH_ICON_WEAPON)
send_alert("weapon!!!")
if(FISH_ICON_CRITTER)
send_alert("critter!!!")
if(FISH_ICON_SEED)
send_alert("seed!!!")
if(FISH_ICON_BOTTLE)
send_alert("bottle!!!")
if(FISH_ICON_ORGAN)
send_alert("organ!!!")
else
send_alert("!!!")
animate(float, pixel_z = 3, time = 5, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_z = -3, time = 5, flags = ANIMATION_RELATIVE)
if(special_effects & FISHING_MINIGAME_AUTOREEL)
addtimer(CALLBACK(src, PROC_REF(automatically_start_minigame)), 0.2 SECONDS)
// Setup next phase
next_phase_timer = addtimer(CALLBACK(src, PROC_REF(start_baiting_phase)), BITING_TIME_WINDOW, TIMER_STOPPABLE|TIMER_DELETE_ME)
///If we're using a lure, we want the float to show a little green light during the minigame phase and not a red one.
float.spin_ready = TRUE
float.update_appearance(UPDATE_OVERLAYS)
/datum/fishing_challenge/proc/automatically_start_minigame()
if(phase == BITING_PHASE)
start_minigame_phase(auto_reel = TRUE)
///The damage dealt per second to the fish when FISHING_MINIGAME_RULE_KILL is active.
#define FISH_DAMAGE_PER_SECOND 2
///The player is no longer around to play the minigame, so we interrupt it.
/datum/fishing_challenge/proc/on_user_logout(datum/source)
SIGNAL_HANDLER
interrupt()
/datum/fishing_challenge/proc/on_reward_removed(datum/source)
SIGNAL_HANDLER
send_alert("reward gone!")
interrupt()
/datum/fishing_challenge/proc/on_fish_death(obj/item/fish/source)
SIGNAL_HANDLER
if(source.status == FISH_DEAD)
win_anyway()
/datum/fishing_challenge/proc/win_anyway()
if(completed)
return
//winning by timeout / fish death shouldn't give as much experience.
experience_multiplier *= 0.5
complete(TRUE)
/datum/fishing_challenge/proc/hurt_fish(datum/source, obj/item/fish/reward)
SIGNAL_HANDLER
if(istype(reward))
var/damage = CEILING((world.time - start_time)/10 * FISH_DAMAGE_PER_SECOND, 1)
reward.damage_fish(damage)
/datum/fishing_challenge/proc/get_difficulty()
var/new_difficulty = fish_source?.calculate_difficulty_minigame(src, reward_path, used_rod, user) || initial(difficulty)
for(var/source, modifier in user?.fishing_difficulty_mods_by_source)
new_difficulty += modifier
//If you manage to be so well-equipped and skilled to completely crush the difficulty, just skip to the reward.
if(new_difficulty <= 0)
complete(TRUE)
return FALSE
difficulty = clamp(round(new_difficulty), FISHING_MINIMUM_DIFFICULTY, 100)
return TRUE
/datum/fishing_challenge/proc/update_difficulty()
if(phase != MINIGAME_PHASE)
return
var/old_difficulty = difficulty
//early return if the difficulty is the same or we crush the minigame all the way to 0 difficulty
if(!get_difficulty() || difficulty == old_difficulty)
return
bait_height = initial(bait_height) * used_rod.bait_height_mult
experience_multiplier -= difficulty * FISHING_SKILL_DIFFIULTY_EXP_MULT
mover.reset_difficulty_values()
adjust_to_difficulty()
/datum/fishing_challenge/proc/adjust_to_difficulty()
mover.adjust_to_difficulty()
bait_height -= round(difficulty * BAIT_HEIGHT_DIFFICULTY_MALUS)
bait_pixel_height = round(MINIGAME_BAIT_HEIGHT * (bait_height/initial(bait_height)), 1)
experience_multiplier += difficulty * FISHING_SKILL_DIFFIULTY_EXP_MULT
fishing_hud.hud_bait.adjust_to_difficulty(src)
///Get the difficulty and other variables, than start the minigame
/datum/fishing_challenge/proc/start_minigame_phase(auto_reel = FALSE)
SEND_SIGNAL(user, COMSIG_MOB_BEGIN_FISHING_MINIGAME, src)
if(!get_difficulty()) //we totalized 0 or less difficulty, instant win.
return
if(difficulty > FISHING_DEFAULT_DIFFICULTY)
completion -= MAX_FISH_COMPLETION_MALUS * (difficulty * 0.01)
var/is_fish_instance = isfish(reward_path)
/// Fish minigame properties
if(ispath(reward_path,/obj/item/fish) || is_fish_instance)
var/obj/item/fish/fish = reward_path
var/movement_path = initial(fish.fish_movement_type)
mover = new movement_path(src)
// Apply fish trait modifiers
var/list/fish_traits = is_fish_instance ? fish.fish_traits : SSfishing.fish_properties[fish][FISH_PROPERTIES_TRAITS]
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
trait.minigame_mod(used_rod, user, src)
else
mover = new /datum/fish_movement(src)
SEND_SIGNAL(src, COMSIG_FISHING_CHALLENGE_MOVER_INITIALIZED, mover)
if(auto_reel)
completion *= 1.3
else
var/time_left = timeleft(next_phase_timer)
switch(time_left)
if(0 to BITING_TIME_WINDOW - 3 SECONDS)
completion *= 0.65
if(BITING_TIME_WINDOW - 3 SECONDS to BITING_TIME_WINDOW - 2 SECONDS)
completion *= 0.82
if(BITING_TIME_WINDOW - 1 SECONDS to BITING_TIME_WINDOW - 0.5 SECONDS)
completion *= 1.2
if(BITING_TIME_WINDOW - 0.5 SECONDS to BITING_TIME_WINDOW)
completion *= 1.4
//randomize the position of the fish a little
fish_position = rand(0, (FISHING_MINIGAME_AREA - fish_height) * 0.8)
var/diff_dist = 100 + difficulty
bait_position = clamp(round(fish_position + rand(-diff_dist, diff_dist) - bait_height * 0.5), 0, FISHING_MINIGAME_AREA - bait_height)
if(!prepare_minigame_hud())
get_stack_trace("couldn't prepare minigame hud for a fishing challenge.") //just to be sure. This shouldn't happen.
qdel(src)
return
adjust_to_difficulty()
phase = MINIGAME_PHASE
deltimer(next_phase_timer)
if((FISHING_MINIGAME_RULE_KILL in special_effects) && ispath(reward_path,/obj/item/fish))
var/obj/item/fish/fish = reward_path
var/wait_time = ((initial(fish.max_integrity) * (1 - initial(fish.integrity_failure))) / FISH_DAMAGE_PER_SECOND) SECONDS
addtimer(CALLBACK(src, PROC_REF(win_anyway)), wait_time, TIMER_DELETE_ME)
else if(ismovable(reward_path))
var/atom/movable/reward = reward_path
RegisterSignal(reward, COMSIG_MOVABLE_MOVED, PROC_REF(on_reward_removed))
if(is_fish_instance)
RegisterSignal(reward, COMSIG_FISH_STATUS_CHANGED, PROC_REF(on_fish_death))
start_time = world.time
///Throws a stack with prefixed text.
/datum/fishing_challenge/proc/get_stack_trace(init_text)
var/text = "[init_text] "
text += "used rod: [used_rod || "null"], "
if(used_rod)
text += "bait: [used_rod.bait || "null"], "
text += "reward: [reward_path || "null"], "
text += "user: [user || "null"]"
if(user)
if(QDELING(user))
text += ", user qdeling"
else if(!user.client)
text += ", user clientless"
text += "."
stack_trace(text)
#undef FISH_DAMAGE_PER_SECOND
///Initialize the minigame hud and register some signals to make it work.
/datum/fishing_challenge/proc/prepare_minigame_hud()
if(!user.client || user.incapacitated)
return FALSE
. = TRUE
fishing_hud = new
fishing_hud.prepare_minigame(src)
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(start_reeling))
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(stop_reeling))
RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(on_user_logout))
if(length(active_effects))
// Give the player a moment to prepare for active minigame effects
COOLDOWN_START(src, active_effect_cd, rand(5, 9) SECONDS)
START_PROCESSING(SSfishing, src)
///Stop processing and remove references to the minigame hud
/datum/fishing_challenge/proc/remove_minigame_hud()
STOP_PROCESSING(SSfishing, src)
QDEL_NULL(fishing_hud)
///While the mouse button is held down, the bait will be reeling up (or down on r-click if the bidirectional rule is enabled)
/datum/fishing_challenge/proc/start_reeling(client/source, datum/object, location, control, params)
SIGNAL_HANDLER
var/bidirectional = special_effects & FISHING_MINIGAME_RULE_BIDIRECTIONAL
var/list/modifiers = params2list(params)
if(bidirectional && LAZYACCESS(modifiers, RIGHT_CLICK))
reeling_state = REELING_STATE_DOWN
else
reeling_state = REELING_STATE_UP
///Reset the reeling state to idle once the mouse button is released
/datum/fishing_challenge/proc/stop_reeling(client/source, datum/object, location, control, params)
SIGNAL_HANDLER
reeling_state = REELING_STATE_IDLE
///Update the state of the fish, the bait and the hud
/datum/fishing_challenge/process(seconds_per_tick)
if(length(active_effects) && COOLDOWN_FINISHED(src, active_effect_cd))
select_active_effect()
mover.move_fish(seconds_per_tick)
move_bait(seconds_per_tick)
if(!QDELETED(fishing_hud))
update_visuals(seconds_per_tick)
///The proc that handles fancy effects like flipping the hud or skewing movement
/datum/fishing_challenge/proc/select_active_effect()
///bring forth an active effect
if(isnull(current_active_effect))
current_active_effect = pick(active_effects)
switch(current_active_effect)
if(FISHING_MINIGAME_RULE_ANTIGRAV)
fishing_hud.icon_state = "background_antigrav"
SEND_SOUND(user, sound('sound/effects/arcade_jump.ogg', volume = 50))
COOLDOWN_START(src, active_effect_cd, rand(6, 9) SECONDS)
if(FISHING_MINIGAME_RULE_FLIP)
fishing_hud.icon_state = "background_flip"
fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
SEND_SOUND(user, sound('sound/effects/boing.ogg'))
COOLDOWN_START(src, active_effect_cd, rand(5, 6) SECONDS)
if(FISHING_MINIGAME_RULE_CAMO)
fishing_hud.icon_state = "background_camo"
SEND_SOUND(user, sound('sound/effects/nightmare_poof.ogg', volume = 15))
COOLDOWN_START(src, active_effect_cd, rand(6, 8) SECONDS)
animate(fishing_hud.hud_fish, alpha = 7, time = 2 SECONDS)
return
///go back to normal
switch(current_active_effect)
if(FISHING_MINIGAME_RULE_ANTIGRAV)
var/sound/inverted_sound = sound('sound/effects/arcade_jump.ogg', volume = 50)
inverted_sound.frequency = -1
SEND_SOUND(user, inverted_sound)
COOLDOWN_START(src, active_effect_cd, rand(10, 13) SECONDS)
if(FISHING_MINIGAME_RULE_FLIP)
fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
COOLDOWN_START(src, active_effect_cd, rand(8, 12) SECONDS)
if(FISHING_MINIGAME_RULE_CAMO)
COOLDOWN_START(src, active_effect_cd, rand(9, 16) SECONDS)
SEND_SOUND(user, sound('sound/effects/nightmare_reappear.ogg', volume = 15))
animate(fishing_hud.hud_fish, alpha = 255, time = 1.2 SECONDS)
fishing_hud.icon_state = background
current_active_effect = null
///The proc that moves the bait around, just like in the old TGUI, mostly.
/datum/fishing_challenge/proc/move_bait(seconds_per_tick)
var/should_bounce = abs(bait_velocity) > BAIT_MIN_VELOCITY_BOUNCE
bait_position += bait_velocity * seconds_per_tick
// Hitting the top bound
if(bait_position > FISHING_MINIGAME_AREA - bait_height)
bait_position = FISHING_MINIGAME_AREA - bait_height
if(reeling_state == REELING_STATE_UP || !should_bounce)
bait_velocity = 0
else
bait_velocity = -bait_velocity * bait_bounce_mult
// Hitting rock bottom
else if(bait_position < 0)
bait_position = 0
if(reeling_state == REELING_STATE_DOWN || !should_bounce)
bait_velocity = 0
else
bait_velocity = -bait_velocity * bait_bounce_mult
var/fish_on_bait = (fish_position + fish_height >= bait_position) && (bait_position + bait_height >= fish_position)
var/bidirectional = special_effects & FISHING_MINIGAME_RULE_BIDIRECTIONAL
var/velocity_change
switch(reeling_state)
if(REELING_STATE_UP)
velocity_change = reeling_velocity
if(REELING_STATE_DOWN)
velocity_change = -reeling_velocity
if(REELING_STATE_IDLE)
if(!bidirectional || bait_velocity > 0)
velocity_change = gravity_velocity
else
velocity_change = -gravity_velocity
velocity_change *= (fish_on_bait ? overlap_velocity_mult : 1) * seconds_per_tick
velocity_change = round(velocity_change)
if(current_active_effect == FISHING_MINIGAME_RULE_ANTIGRAV)
velocity_change = -velocity_change
if(reeling_state == REELING_STATE_IDLE && (special_effects & FISHING_MINIGAME_AUTOREEL))
var/bait_center = bait_position + bait_height / 2
var/auto_adjustment = reeling_velocity * 0.2
if(fish_position > bait_center)
velocity_change += auto_adjustment
else
velocity_change -= auto_adjustment
/**
* Pull the brake on the velocity if the current velocity and the acceleration
* have different directions, making the bait less slippery, thus easier to control
*/
if(bait_velocity > 0 && velocity_change < 0)
bait_velocity += max(-bait_velocity, velocity_change * deceleration_mult)
else if(bait_velocity < 0 && velocity_change > 0)
bait_velocity += min(-bait_velocity, velocity_change * deceleration_mult)
///bidirectional baits stay bouyant while idle
if(bidirectional && reeling_state == REELING_STATE_IDLE)
if(velocity_change < 0)
bait_velocity = max(bait_velocity + velocity_change, 0)
else if(velocity_change > 0)
bait_velocity = min(bait_velocity + velocity_change, 0)
else
bait_velocity += velocity_change
//check that the fish area is still intersecting the bait now that it has moved
if(fish_on_bait)
completion += completion_gain * seconds_per_tick
if(completion >= 100)
complete(TRUE)
else
completion -= completion_loss * seconds_per_tick
if(completion <= 0 && !(special_effects & FISHING_MINIGAME_RULE_NO_ESCAPE))
user.balloon_alert(user, "it got away!")
complete(FALSE)
completion = clamp(completion, 0, 100)
///update the vertical pixel position of both fish and bait, and the icon state of the completion bar
/datum/fishing_challenge/proc/update_visuals(seconds_per_tick)
var/bait_offset_mult = bait_position / FISHING_MINIGAME_AREA
animate(fishing_hud.hud_bait, pixel_z = MINIGAME_SLIDER_HEIGHT * bait_offset_mult, time = seconds_per_tick SECONDS)
var/fish_offset_mult = fish_position / FISHING_MINIGAME_AREA
animate(fishing_hud.hud_fish, pixel_z = MINIGAME_SLIDER_HEIGHT * fish_offset_mult, time = seconds_per_tick SECONDS)
fishing_hud.hud_completion.update_state(completion, seconds_per_tick)
///The screen object which bait, fish, and completion bar are visually attached to.
/atom/movable/screen/fishing_hud
icon = 'icons/hud/fishing_hud.dmi'
screen_loc = "CENTER+1:8,CENTER:2"
name = "fishing minigame"
appearance_flags = APPEARANCE_UI|KEEP_TOGETHER
///The fish as shown in the minigame
var/atom/movable/screen/hud_fish/hud_fish
///The bait as shown in the minigame
var/atom/movable/screen/hud_bait/hud_bait
///The completion bar as shown in the minigame
var/atom/movable/screen/hud_completion/hud_completion
///Initialize bait, fish and completion bar and add them to the visual appearance of this screen object.
/atom/movable/screen/fishing_hud/proc/prepare_minigame(datum/fishing_challenge/challenge)
icon_state = challenge.background
add_overlay(challenge.used_rod?.get_frame(challenge) || "frame_wood")
hud_bait = new(null, null, challenge)
hud_fish = new(null, null, challenge)
hud_completion = new(null, null)
vis_contents += list(hud_bait, hud_fish, hud_completion)
challenge.user.client.screen += src
challenge.update_visuals(0) // Set all states to their initial positions so they don't jump around when the game starts
master_ref = WEAKREF(challenge)
/atom/movable/screen/fishing_hud/Destroy()
var/datum/fishing_challenge/challenge = master_ref?.resolve()
if(!isnull(challenge))
challenge.user.client.screen -= src
QDEL_NULL(hud_fish)
QDEL_NULL(hud_bait)
QDEL_NULL(hud_completion)
return ..()
/atom/movable/screen/hud_bait
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "bait_bottom"
vis_flags = VIS_INHERIT_ID
var/cur_height = MINIGAME_BAIT_HEIGHT
/atom/movable/screen/hud_bait/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
. = ..()
if(!challenge || challenge.bait_pixel_height == MINIGAME_BAIT_HEIGHT)
update_icon()
return
adjust_to_difficulty(challenge)
/atom/movable/screen/hud_bait/proc/adjust_to_difficulty(datum/fishing_challenge/challenge)
cur_height = challenge.bait_pixel_height
update_icon()
/atom/movable/screen/hud_bait/update_overlays()
. = ..()
var/mutable_appearance/bait_top = mutable_appearance(icon, "bait_top")
bait_top.pixel_z += cur_height - MINIGAME_BAIT_TOP_AND_BOTTOM_HEIGHT
. += bait_top
for (var/i in 1 to (cur_height - MINIGAME_BAIT_TOP_AND_BOTTOM_HEIGHT))
var/mutable_appearance/bait_bar = mutable_appearance(icon, "bait_bar")
bait_bar.pixel_z += i
. += bait_bar
/atom/movable/screen/hud_fish
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "fish"
vis_flags = VIS_INHERIT_ID
/atom/movable/screen/hud_fish/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
. = ..()
if(challenge)
icon_state = challenge.fish_icon
/atom/movable/screen/hud_completion
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "completion_overlay"
vis_flags = VIS_INHERIT_ID
/atom/movable/screen/hud_completion/Initialize(mapload, datum/hud/hud_owner)
. = ..()
add_filter("completion_mask", 1, alpha_mask_filter(icon = icon(icon, "completion_overlay")))
/atom/movable/screen/hud_completion/proc/update_state(completion, seconds_per_tick)
animate(get_filter("completion_mask"), y = -MINIGAME_COMPLETION_BAR_HEIGHT * (1 - completion * 0.01), time = seconds_per_tick SECONDS)
/// The visual that appears over the fishing spot
/obj/effect/fishing_float
name = "float"
icon = 'icons/obj/fishing.dmi'
icon_state = "float"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
/**
* A list with two keys delimiting the spinning interval in which a mouse click has to be pressed while fishing.
* If set, an emissive overlay will be added, colored green when the lure is ready to be spun, otherwise red.
*/
var/list/spin_frequency
///Is the bait ready to be spun?
var/spin_ready = FALSE
/obj/effect/fishing_float/Initialize(mapload, atom/spot)
. = ..()
if(!spot)
return
if(ismovable(spot)) // we want the float and therefore the fishing line to stay connected with the fishing spot.
RegisterSignal(spot, COMSIG_MOVABLE_MOVED, PROC_REF(follow_movable))
SET_BASE_PIXEL(spot.pixel_x, spot.pixel_y)
SET_BASE_VISUAL_PIXEL(spot.pixel_w, spot.pixel_z)
// early return for spots with a plane lower than this. the floor plane is topdown and we don't want to inherit their layers.
if(spot.plane < plane)
return
if(spot.plane > plane) //We want this to render above the fishing spot.
var/turf/turf = get_turf(spot)
SET_PLANE_EXPLICIT(src, PLANE_TO_TRUE(spot.plane), turf)
if(spot.layer > layer) //Ditto. New stuff renders above old stuff if the layer is the same iirc (with some caveats).
layer = spot.layer
/obj/effect/fishing_float/proc/follow_movable(atom/movable/source)
SIGNAL_HANDLER
set_glide_size(source.glide_size)
forceMove(source.loc)
/obj/effect/fishing_float/update_overlays()
. = ..()
if(!spin_frequency)
return
var/mutable_appearance/overlay = mutable_appearance(icon, "lure_light")
overlay.color = spin_ready ? COLOR_GREEN : COLOR_RED
. += overlay
. += emissive_appearance(icon, "lure_light_emissive", src, alpha = src.alpha, effect_type = EMISSIVE_NO_BLOOM)
#undef WAIT_PHASE
#undef BITING_PHASE
#undef MINIGAME_PHASE
#undef MINIGAME_SLIDER_HEIGHT
#undef MINIGAME_BAIT_HEIGHT
#undef MINIGAME_FISH_HEIGHT
#undef MINIGAME_BAIT_TOP_AND_BOTTOM_HEIGHT
#undef MINIGAME_COMPLETION_BAR_HEIGHT
#undef BAIT_HEIGHT_DIFFICULTY_MALUS
#undef REELING_STATE_IDLE
#undef REELING_STATE_UP
#undef REELING_STATE_DOWN
#undef BAIT_MIN_VELOCITY_BOUNCE
#undef MAX_FISH_COMPLETION_MALUS
#undef BITING_TIME_WINDOW