From da42afcbaeaa04e5ba288ade027c011efb3ef0ab Mon Sep 17 00:00:00 2001
From: Ghom <42542238+Ghommie@users.noreply.github.com>
Date: Mon, 18 Sep 2023 11:18:27 +0200
Subject: [PATCH] Reworks the fishing minigame into a game screen object from a
TGUI interface (#78052)
Refactors fishing minigame from tgui window to dm screen objects
---
code/__DEFINES/fishing.dm | 12 +-
.../subsystem/processing/fishing.dm | 7 +
code/modules/asset_cache/assets/fish.dm | 7 -
code/modules/fishing/fish/fish_traits.dm | 15 +-
code/modules/fishing/fishing_equipment.dm | 4 +-
code/modules/fishing/fishing_minigame.dm | 449 ++++++++++++---
code/modules/fishing/sources/_fish_source.dm | 4 +-
code/modules/fishing/sources/source_types.dm | 5 +-
icons/hud/fishing_hud.dmi | Bin 0 -> 2956 bytes
icons/ui_icons/fishing/default.png | Bin 136 -> 0 bytes
icons/ui_icons/fishing/lavaland.png | Bin 136 -> 0 bytes
tgstation.dme | 1 +
tgui/packages/tgui/interfaces/Fishing.tsx | 526 ------------------
13 files changed, 411 insertions(+), 619 deletions(-)
create mode 100644 code/controllers/subsystem/processing/fishing.dm
create mode 100644 icons/hud/fishing_hud.dmi
delete mode 100644 icons/ui_icons/fishing/default.png
delete mode 100644 icons/ui_icons/fishing/lavaland.png
delete mode 100644 tgui/packages/tgui/interfaces/Fishing.tsx
diff --git a/code/__DEFINES/fishing.dm b/code/__DEFINES/fishing.dm
index 2481f0ae3e7..dc73623f27c 100644
--- a/code/__DEFINES/fishing.dm
+++ b/code/__DEFINES/fishing.dm
@@ -51,14 +51,10 @@
/// Much like FISHING_HOOK_ENSNARE but for the reel.
#define FISHING_LINE_BOUNCY (1 << 2)
-#define FISHING_MINIGAME_RULE_HEAVY_FISH "heavy"
-#define FISHING_MINIGAME_RULE_LUBED_FISH "lubed"
-#define FISHING_MINIGAME_RULE_WEIGHTED_BAIT "weighted"
-#define FISHING_MINIGAME_RULE_LIMIT_LOSS "limit_loss"
-#define FISHING_MINIGAME_RULE_BIDIRECTIONAL "bidirectional"
-#define FISHING_MINIGAME_RULE_NO_ESCAPE "no_escape"
-#define FISHING_MINIGAME_RULE_KILL "kill"
-#define FISHING_MINIGAME_RULE_NO_EXP "no_exp"
+#define FISHING_MINIGAME_RULE_BIDIRECTIONAL (1 << 2)
+#define FISHING_MINIGAME_RULE_NO_ESCAPE (1 << 3)
+#define FISHING_MINIGAME_RULE_KILL (1 << 4)
+#define FISHING_MINIGAME_RULE_NO_EXP (1 << 5)
/// The default additive value for fishing hook catch weight modifiers.
#define FISHING_DEFAULT_HOOK_BONUS_ADDITIVE 0
diff --git a/code/controllers/subsystem/processing/fishing.dm b/code/controllers/subsystem/processing/fishing.dm
new file mode 100644
index 00000000000..da10d3d631a
--- /dev/null
+++ b/code/controllers/subsystem/processing/fishing.dm
@@ -0,0 +1,7 @@
+/**
+ * So far, only used by the fishing minigame. Feel free to rename it to something like veryfastprocess
+ * if you need one that fires 10 times a second
+ */
+PROCESSING_SUBSYSTEM_DEF(fishing)
+ name = "Fishing"
+ wait = 0.1 SECONDS
diff --git a/code/modules/asset_cache/assets/fish.dm b/code/modules/asset_cache/assets/fish.dm
index 14258663d9b..2fcf2b803e3 100644
--- a/code/modules/asset_cache/assets/fish.dm
+++ b/code/modules/asset_cache/assets/fish.dm
@@ -12,10 +12,3 @@
continue
id_list += id
Insert(id, fish_icon, fish_icon_state)
-
-
-/datum/asset/simple/fishing_minigame
- assets = list(
- "fishing_background_default" = 'icons/ui_icons/fishing/default.png',
- "fishing_background_lavaland" = 'icons/ui_icons/fishing/lavaland.png'
- )
diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm
index 6c7fb38e279..bec868ad24e 100644
--- a/code/modules/fishing/fish/fish_traits.dm
+++ b/code/modules/fishing/fish/fish_traits.dm
@@ -23,9 +23,9 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
SHOULD_CALL_PARENT(TRUE)
return list(ADDITIVE_FISHING_MOD = 0, MULTIPLICATIVE_FISHING_MOD = 1)
-/// Returns special minigame rules applied by this trait
-/datum/fish_trait/proc/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman)
- return list()
+/// Returns special minigame rules and effects applied by this trait
+/datum/fish_trait/proc/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ return
/// Applies some special qualities to the fish that has been spawned
/datum/fish_trait/proc/apply_to_fish(obj/item/fish/fish)
@@ -100,8 +100,8 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
name = "Heavy"
catalog_description = "This fish tends to stay near the waterbed.";
-/datum/fish_trait/heavy/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman)
- return list(FISHING_MINIGAME_RULE_HEAVY_FISH)
+/datum/fish_trait/heavy/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ minigame.fish_idle_velocity -= 10
/datum/fish_trait/carnivore
name = "Carnivore"
@@ -338,8 +338,9 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/lubed/apply_to_fish(obj/item/fish/fish)
fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP)
-/datum/fish_trait/lubed/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman)
- return list(FISHING_MINIGAME_RULE_LUBED_FISH)
+/datum/fish_trait/lubed/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ minigame.reeling_velocity *= 1.4
+ minigame.gravity_velocity *= 1.4
/datum/fish_trait/amphibious
name = "Amphibious"
diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm
index e94e13ed4e8..6169b41fd88 100644
--- a/code/modules/fishing/fishing_equipment.dm
+++ b/code/modules/fishing/fishing_equipment.dm
@@ -162,14 +162,14 @@
/obj/item/fishing_hook/stabilized
name = "gyro-stabilized hook"
- desc = "A quirky hook that grants the user a better control of the tool, allowing them to move the hook both and up and down when reeling in, otherwise keeping it stabilized."
+ desc = "A quirky hook that grants the user a better control of the tool, allowing them to move the bait both and up and down when reeling in, otherwise keeping it in place."
icon_state = "gyro"
fishing_hook_traits = FISHING_HOOK_BIDIRECTIONAL
rod_overlay_icon_state = "hook_gyro_overlay"
/obj/item/fishing_hook/stabilized/examine(mob/user)
. = ..()
- . += span_notice("While fishing, you can hold the Ctrl key to move the bait down, rather than up.")
+ . += span_notice("While fishing, you can hold the Right Mouse Button to move the bait down, rather than up.")
/obj/item/fishing_hook/jaws
name = "jawed hook"
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index d2c38fb484f..478279749e4 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -5,6 +5,35 @@
// UI minigame phase
#define MINIGAME_PHASE 3
+/// The height of the minigame slider. Not in pixels, but minigame units.
+#define FISHING_MINIGAME_AREA 1000
+/// Any lower than this, and the target position of the fish is considered null
+#define FISH_TARGET_MIN_DISTANCE 6
+/// The friction applied to fish jumps, so that it decelerates over time
+#define FISH_FRICTION_MULT 0.9
+/// Used to decide whether the fish can jump in a certain direction
+#define FISH_SHORT_JUMP_MIN_DISTANCE 100
+/// The maximum distance for a short jump
+#define FISH_SHORT_JUMP_MAX_DISTANCE 200
+// Acceleration mod when bait is over fish
+#define FISH_ON_BAIT_ACCELERATION_MULT 0.6
+/// The minimum velocity required for the bait to bounce
+#define BAIT_MIN_VELOCITY_BOUNCE 200
+/// The extra deceleration of velocity that happens when the bait switches direction
+#define BAIT_DECELERATION_MULT 2
+
+///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 24
+///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
+
/datum/fishing_challenge
/// When the ui minigame phase started
var/start_time
@@ -13,7 +42,7 @@
/// Fish AI type to use
var/fish_ai = FISH_AI_DUMB
/// Rule modifiers (eg weighted bait)
- var/list/special_effects = list()
+ var/special_effects = NONE
/// Did the game get past the baiting phase, used to track if bait should be consumed afterwards
var/bait_taken = FALSE
/// Result path
@@ -30,14 +59,67 @@
var/obj/item/fishing_rod/used_rod
/// Lure visual
var/obj/effect/fishing_lure/lure
- /// Background image from /datum/asset/simple/fishing_minigame
- var/background = "default"
+ /// Background icon state from fishing_hud.dmi
+ var/background = "background_default"
/// 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 = 320
+ /// 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 fish is moving at
+ var/fish_velocity = 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
+
+ /// How likely the fish is to perform a standard jump, then multiplied by difficulty
+ var/short_jump_chance = 2.25
+ /// How likely the fish is to perform a long jump, then multiplied by difficulty
+ var/long_jump_chance = 0.0625
+ /// The speed limit for the short jump
+ var/short_jump_velocity_limit = 400
+ /// The speed limit for the long jump
+ var/long_jump_velocity_limit = 200
+ /// The current speed limit used
+ var/current_velocity_limit = 200
+ /// The base velocity of the fish, which may affect jump distances and falling speed.
+ var/fish_idle_velocity = 0
+ /// A position on the slider the fish wants to get to
+ var/target_position
+ /// If true, the fish can jump while a target position is set, thus overriding it
+ var/can_interrupt_move = TRUE
+
+ /// 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
+ var/bait_bounce_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
+
/datum/fishing_challenge/New(datum/component/fishing_spot/comp, reward_path, obj/item/fishing_rod/rod, mob/user)
src.user = user
src.reward_path = reward_path
@@ -52,32 +134,61 @@
if(ispath(reward_path,/obj/item/fish))
var/obj/item/fish/fish = reward_path
fish_ai = initial(fish.fish_ai_type)
+ switch(fish_ai)
+ if(FISH_AI_ZIPPY) // Keeps on jumping
+ short_jump_chance *= 3
+ if(FISH_AI_SLOW) // Only does long jump, and doesn't change direction until it gets there
+ short_jump_chance = 0
+ long_jump_chance = 1.5
+ long_jump_velocity_limit = 150
+ long_jump_velocity_limit = FALSE
// Apply fish trait modifiers
var/list/fish_list_properties = collect_fish_properties()
var/list/fish_traits = fish_list_properties[fish][NAMEOF(fish, fish_traits)]
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
- special_effects += trait.minigame_mod(rod, user)
+ trait.minigame_mod(rod, user, src)
/// Enable special parameters
if(rod.line)
if(rod.line.fishing_line_traits & FISHING_LINE_BOUNCY)
- special_effects |= FISHING_MINIGAME_RULE_LIMIT_LOSS
+ completion_loss -= 2
if(rod.hook)
if(rod.hook.fishing_hook_traits & FISHING_HOOK_WEIGHTED)
- special_effects |= FISHING_MINIGAME_RULE_WEIGHTED_BAIT
+ 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)
- special_effects |= FISHING_MINIGAME_RULE_LIMIT_LOSS
+ completion_loss -= 2
if(rod.hook.fishing_hook_traits & FISHING_HOOK_KILL)
special_effects |= FISHING_MINIGAME_RULE_KILL
- if((FISHING_MINIGAME_RULE_KILL in special_effects) && ispath(reward_path,/obj/item/fish))
+ if(special_effects & FISHING_MINIGAME_RULE_KILL && ispath(reward_path,/obj/item/fish))
RegisterSignal(user, COMSIG_MOB_FISHING_REWARD_DISPENSED, PROC_REF(hurt_fish))
difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src)
+ difficulty = round(difficulty)
+
+ /**
+ * If the chances are higher than 1% (100% at maximum difficulty), they'll scale
+ * less than proportionally (exponent less than 1) instead.
+ * This way we ensure fish with high jump chances won't get TOO jumpy until
+ * they near the maximum difficulty, at which they hit 100%
+ */
+ var/square_angle_rad = TORADIANS(90)
+ var/zero_one_difficulty = difficulty/100
+ if(short_jump_chance > 1)
+ short_jump_chance = (zero_one_difficulty**(square_angle_rad-TORADIANS(arctan(short_jump_chance * 1/square_angle_rad))))*100
+ else
+ short_jump_chance *= difficulty
+ if(long_jump_chance > 1)
+ long_jump_chance = (zero_one_difficulty**(square_angle_rad-TORADIANS(arctan(long_jump_chance * 1/square_angle_rad))))*100
+ else
+ long_jump_chance *= difficulty
+
+ bait_height -= difficulty
+ bait_pixel_height = round(MINIGAME_BAIT_HEIGHT * (bait_height/initial(bait_height)), 1)
/datum/fishing_challenge/Destroy(force, ...)
if(!completed)
@@ -120,13 +231,13 @@
/datum/fishing_challenge/proc/handle_click(mob/source, atom/target, modifiers)
SIGNAL_HANDLER
//You need to be holding the rod to use it.
- if(!source.get_active_held_item(used_rod) || LAZYACCESS(modifiers, SHIFT_CLICK))
+ if(!source.get_active_held_item(used_rod) || LAZYACCESS(modifiers, SHIFT_CLICK) || LAZYACCESS(modifiers, CTRL_CLICK) || LAZYACCESS(modifiers, ALT_CLICK))
return
if(phase == WAIT_PHASE) //Reset wait
send_alert("miss!")
start_baiting_phase()
else if(phase == BITING_PHASE)
- INVOKE_ASYNC(src, PROC_REF(start_minigame_phase))
+ start_minigame_phase()
return COMSIG_MOB_CANCEL_CLICKON
/// Challenge interrupted by something external
@@ -148,16 +259,18 @@
send_alert("stopped fishing")
complete(FALSE)
-/datum/fishing_challenge/proc/complete(win = FALSE, perfect_win = FALSE)
+/datum/fishing_challenge/proc/complete(win = FALSE)
if(completed)
return
deltimer(next_phase_timer)
completed = TRUE
+ if(phase == MINIGAME_PHASE)
+ remove_minigame_hud()
if(user)
REMOVE_TRAIT(user, TRAIT_GONE_FISHING, REF(src))
if(start_time)
var/seconds_spent = (world.time - start_time) * 0.1
- if(!(FISHING_MINIGAME_RULE_NO_EXP in special_effects))
+ if(!(special_effects & FISHING_MINIGAME_RULE_NO_EXP))
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)
@@ -191,16 +304,10 @@
///The damage dealt per second to the fish when FISHING_MINIGAME_RULE_KILL is active.
#define FISH_DAMAGE_PER_SECOND 2
-/datum/fishing_challenge/proc/start_minigame_phase()
- 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.health) / FISH_DAMAGE_PER_SECOND) SECONDS
- addtimer(CALLBACK(src, PROC_REF(win_anyway)), wait_time)
- start_time = world.time
- experience_multiplier += difficulty * FISHING_SKILL_DIFFIULTY_EXP_MULT
- ui_interact(user)
+///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(balloon_alert = FALSE)
/datum/fishing_challenge/proc/win_anyway()
if(!completed)
@@ -214,57 +321,251 @@
var/damage = CEILING((world.time - start_time)/10 * FISH_DAMAGE_PER_SECOND, 1)
reward.adjust_health(reward.health - damage)
+/datum/fishing_challenge/proc/start_minigame_phase()
+ if(!prepare_minigame_hud())
+ return
+ 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.health) / FISH_DAMAGE_PER_SECOND) SECONDS
+ addtimer(CALLBACK(src, PROC_REF(win_anyway)), wait_time)
+ start_time = world.time
+ experience_multiplier += difficulty * FISHING_SKILL_DIFFIULTY_EXP_MULT
+
#undef FISH_DAMAGE_PER_SECOND
-/datum/fishing_challenge/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "Fishing")
- ui.set_autoupdate(FALSE)
- ui.set_mouse_hook(TRUE)
- ui.open()
+///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))
+ START_PROCESSING(SSfishing, src)
-/datum/fishing_challenge/ui_host(mob/user)
- return lure //Could be the target really
+///Stop processing and remove references to the minigame hud
+/datum/fishing_challenge/proc/remove_minigame_hud()
+ STOP_PROCESSING(SSfishing, src)
+ QDEL_NULL(fishing_hud)
-// Manually closing the ui is treated as lose
-/datum/fishing_challenge/ui_close(mob/user)
+///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)
+ move_fish(seconds_per_tick)
+ move_bait(seconds_per_tick)
+ if(!QDELETED(fishing_hud))
+ update_visuals()
+
+///The proc that moves the fish around, just like in the old TGUI, mostly.
+/datum/fishing_challenge/proc/move_fish(seconds_per_tick)
+ var/long_chance = long_jump_chance * seconds_per_tick * 10
+ var/short_chance = short_jump_chance * seconds_per_tick * 10
+
+ // If we have the target but we're close enough, mark as target reached
+ if(abs(target_position - fish_position) < FISH_TARGET_MIN_DISTANCE)
+ target_position = null
+
+ // Switching to new long jump target can interrupt any other
+ if((can_interrupt_move || isnull(target_position)) && prob(long_chance))
+ /**
+ * Move at least 0.75 to full of the availible bar in given direction,
+ * and more likely to move in the direction where there's more space
+ */
+ var/distance_from_top = FISHING_MINIGAME_AREA - fish_position - fish_height
+ var/distance_from_bottom = fish_position
+ var/top_chance
+ if(distance_from_top < FISH_SHORT_JUMP_MIN_DISTANCE)
+ top_chance = 0
+ else
+ top_chance = (distance_from_top/max(distance_from_bottom, 1)) * 100
+ var/new_target = fish_position
+ if(prob(top_chance))
+ new_target += distance_from_top * rand(75, 100)/100
+ else
+ new_target -= distance_from_bottom * rand(75, 100)/100
+ target_position = round(new_target)
+ current_velocity_limit = long_jump_velocity_limit
+
+ // Move towards target
+ if(!isnull(target_position))
+ var/distance = target_position - fish_position
+ // about 5 at diff 15 , 10 at diff 30, 30 at diff 100
+ var/acceleration_mult = 0.3 * difficulty + 0.5
+ var/target_acceleration = distance * acceleration_mult * seconds_per_tick
+
+ fish_velocity = fish_velocity * FISH_FRICTION_MULT + target_acceleration
+ else if(prob(short_chance))
+ var/distance_from_top = FISHING_MINIGAME_AREA - fish_position - fish_height
+ var/distance_from_bottom = fish_position
+ var/jump_length
+ if(distance_from_top >= FISH_SHORT_JUMP_MIN_DISTANCE)
+ jump_length = rand(FISH_SHORT_JUMP_MIN_DISTANCE, FISH_SHORT_JUMP_MAX_DISTANCE)
+ if(distance_from_bottom >= FISH_SHORT_JUMP_MIN_DISTANCE && (!jump_length || prob(50)))
+ jump_length = -rand(FISH_SHORT_JUMP_MIN_DISTANCE, FISH_SHORT_JUMP_MAX_DISTANCE)
+ target_position = clamp(fish_position + jump_length, 0, FISHING_MINIGAME_AREA - fish_height)
+ current_velocity_limit = short_jump_velocity_limit
+
+ fish_velocity = clamp(fish_velocity + fish_idle_velocity, -current_velocity_limit, current_velocity_limit)
+ fish_position = clamp(fish_position + fish_velocity * seconds_per_tick, 0, FISHING_MINIGAME_AREA - fish_height)
+
+///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 ? FISH_ON_BAIT_ACCELERATION_MULT : 1) * seconds_per_tick
+
+ velocity_change = round(velocity_change)
+
+ /**
+ * 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 * BAIT_DECELERATION_MULT)
+ else if(bait_velocity < 0 && velocity_change > 0)
+ bait_velocity += min(-bait_velocity, velocity_change * BAIT_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
+ fish_on_bait = (fish_position + fish_height >= bait_position) && (bait_position + bait_height >= fish_position)
+
+ 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()
+ var/bait_offset_mult = bait_position/FISHING_MINIGAME_AREA
+ fishing_hud.hud_bait.pixel_y = round(MINIGAME_SLIDER_HEIGHT * bait_offset_mult, 1)
+ var/fish_offset_mult = fish_position/FISHING_MINIGAME_AREA
+ fishing_hud.hud_fish.pixel_y = round(MINIGAME_SLIDER_HEIGHT * fish_offset_mult, 1)
+ fishing_hud.hud_completion.icon_state = "completion_[FLOOR(completion, 5)]"
+
+///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
+ alpha = 230
+ ///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("frame")
+ hud_bait = new(null, null, challenge)
+ hud_fish = new
+ hud_completion = new(null, null, challenge)
+ vis_contents += list(hud_bait, hud_fish, hud_completion)
+ challenge.user.client.screen += src
+
+/atom/movable/screen/fishing_hud/Destroy()
+ 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"
+ vis_flags = VIS_INHERIT_ID
+
+/atom/movable/screen/hud_bait/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
. = ..()
- if(!completed)
- send_alert("stopped fishing")
- complete(FALSE)
-
-/datum/fishing_challenge/ui_static_data(mob/user)
- . = ..()
- .["difficulty"] = clamp(difficulty, 1, 100)
- .["fish_ai"] = fish_ai
- .["special_effects"] = special_effects
- .["background_image"] = background
-
-/datum/fishing_challenge/ui_assets(mob/user)
- return list(get_asset_datum(/datum/asset/simple/fishing_minigame)) //preset screens
-
-/datum/fishing_challenge/ui_status(mob/user, datum/ui_state/state)
- return min(
- get_dist(user, lure) > 5 ? UI_CLOSE : UI_INTERACTIVE,
- ui_status_user_has_free_hands(user),
- ui_status_user_is_abled(user, lure),
- )
-
-/datum/fishing_challenge/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
+ if(!challenge || challenge.bait_pixel_height == MINIGAME_BAIT_HEIGHT)
return
+ var/static/icon_height
+ if(!icon_height)
+ var/list/icon_dimensions = get_icon_dimensions(icon)
+ icon_height = icon_dimensions["height"]
+ var/height_percent_diff = challenge.bait_pixel_height/MINIGAME_BAIT_HEIGHT
+ transform = transform.Scale(1, height_percent_diff)
+ pixel_z = -icon_height * (1 - height_percent_diff) * 0.5
- if(phase != MINIGAME_PHASE)
- return
+/atom/movable/screen/hud_fish
+ icon = 'icons/hud/fishing_hud.dmi'
+ icon_state = "fish"
+ vis_flags = VIS_INHERIT_ID
- switch(action)
- if("win")
- complete(win = TRUE)
- if("lose")
- send_alert("it got away")
- complete(win = FALSE)
+/atom/movable/screen/hud_completion
+ icon = 'icons/hud/fishing_hud.dmi'
+ icon_state = "completion_0"
+ vis_flags = VIS_INHERIT_ID
+
+/atom/movable/screen/hud_completion/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
+ . = ..()
+ if(challenge)
+ icon_state = "completion_[FLOOR(challenge.completion, 5)]"
/// The visual that appears over the fishing spot
/obj/effect/fishing_lure
@@ -283,3 +584,21 @@
#undef WAIT_PHASE
#undef BITING_PHASE
#undef MINIGAME_PHASE
+
+#undef FISHING_MINIGAME_AREA
+#undef FISH_TARGET_MIN_DISTANCE
+#undef FISH_FRICTION_MULT
+#undef FISH_SHORT_JUMP_MIN_DISTANCE
+#undef FISH_SHORT_JUMP_MAX_DISTANCE
+#undef FISH_ON_BAIT_ACCELERATION_MULT
+#undef BAIT_MIN_VELOCITY_BOUNCE
+#undef BAIT_DECELERATION_MULT
+
+#undef MINIGAME_SLIDER_HEIGHT
+#undef MINIGAME_BAIT_HEIGHT
+#undef MINIGAME_FISH_HEIGHT
+
+#undef REELING_STATE_IDLE
+#undef REELING_STATE_UP
+#undef REELING_STATE_DOWN
+
diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm
index e8156f26cba..e0f16914570 100644
--- a/code/modules/fishing/sources/_fish_source.dm
+++ b/code/modules/fishing/sources/_fish_source.dm
@@ -16,7 +16,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
/// How the spot type is described in fish catalog section about fish sources, will be skipped if null
var/catalog_description
/// Background image name from /datum/asset/simple/fishing_minigame
- var/background = "fishing_background_default"
+ var/background = "background_default"
/datum/fish_source/New()
if(!PERFORM_ALL_TESTS(focus_only/fish_sources_tables))
@@ -45,7 +45,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
. += SETTLER_DIFFICULTY_MOD
// Difficulty modifier added by the fisher's skill level
- if(!challenge || !(FISHING_MINIGAME_RULE_NO_EXP in challenge.special_effects))
+ if(!challenge || !(challenge.special_effects & FISHING_MINIGAME_RULE_NO_EXP))
. += fisherman.mind?.get_skill_modifier(/datum/skill/fishing, SKILL_VALUE_MODIFIER)
// Difficulty modifier added by the rod
diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm
index cea97005f28..ffb37753881 100644
--- a/code/modules/fishing/sources/source_types.dm
+++ b/code/modules/fishing/sources/source_types.dm
@@ -24,9 +24,10 @@
/obj/item/fish/guppy = 10,
)
catalog_description = "Fish dimension (Fishing portal generator)"
+
/datum/fish_source/chasm
catalog_description = "Chasm depths"
- background = "fishing_background_lavaland"
+ background = "background_lavaland"
fish_table = list(
FISHING_DUD = 5,
/obj/item/fish/chasm_crab = 15,
@@ -45,7 +46,7 @@
/datum/fish_source/lavaland
catalog_description = "Lava vents"
- background = "fishing_background_lavaland"
+ background = "background_lavaland"
fish_table = list(
FISHING_DUD = 5,
/obj/item/stack/ore/slag = 20,
diff --git a/icons/hud/fishing_hud.dmi b/icons/hud/fishing_hud.dmi
new file mode 100644
index 0000000000000000000000000000000000000000..b68acee09b76afe9a5471710bb7a2b11f946ddc1
GIT binary patch
literal 2956
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
diff --git a/tgstation.dme b/tgstation.dme
index 15f3b915bcf..6e27a29a3d7 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -674,6 +674,7 @@
#include "code\controllers\subsystem\processing\digital_clock.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\fire_burning.dm"
+#include "code\controllers\subsystem\processing\fishing.dm"
#include "code\controllers\subsystem\processing\greyscale.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
#include "code\controllers\subsystem\processing\obj.dm"
diff --git a/tgui/packages/tgui/interfaces/Fishing.tsx b/tgui/packages/tgui/interfaces/Fishing.tsx
deleted file mode 100644
index 50e19fb8c53..00000000000
--- a/tgui/packages/tgui/interfaces/Fishing.tsx
+++ /dev/null
@@ -1,526 +0,0 @@
-import { clamp } from 'common/math';
-import { KEY_CTRL } from 'common/keycodes';
-import { randomInteger, randomNumber, randomPick, randomProb } from 'common/random';
-import { useDispatch } from 'common/redux';
-import { Component } from 'inferno';
-import { resolveAsset } from '../assets';
-import { useBackend } from '../backend';
-import { Icon, KeyListener } from '../components';
-import { globalEvents, KeyEvent } from '../events';
-import { Window } from '../layouts';
-
-type Bait = {
- position: number;
- height: number;
- velocity: number;
-};
-
-type Fish = {
- position: number;
- height: number;
- velocity: number;
- target: number | null;
-};
-
-type FishAI = 'dumb' | 'zippy' | 'slow';
-
-enum ReelingState {
- Idle,
- Reeling,
- ReelingDown,
-}
-
-type FishingMinigameProps = {
- difficulty: number;
- fish_ai: FishAI;
- special_rules: SpecialRule[];
- background: string;
- win: () => void;
- lose: () => void;
-};
-
-type FishingMinigameState = {
- completion: number;
- bait: Bait;
- fish: Fish;
-};
-
-type SpecialRule =
- | 'weighted'
- | 'limit_loss'
- | 'heavy'
- | 'bidirectional'
- | 'no_escape'
- | 'lubed';
-
-class FishingMinigame extends Component<
- FishingMinigameProps,
- FishingMinigameState
-> {
- animation_id: number;
- last_frame: number;
- reeling: ReelingState = ReelingState.Idle;
- area_height: number = 1000;
- state: FishingMinigameState;
- currentVelocityLimit: number = 200;
- // Difficulty & special rules dependent variables
- completionLossPerSecond: number;
- baitBounceCoeff: number;
- difficultyActionFreqCoeff: number = 1;
- longJumpVelocityLimit: number = 200;
- shortJumpVelocityLimit: number = 400;
- idleVelocity: number = 0;
- accel_up_coeff: number = 1;
- bidirectional: boolean = false;
- no_escape: boolean = false;
-
- baseLongJumpChancePerSecond: number = 0.0075;
- baseShortJumpChancePerSecond: number = 0.255;
- interruptMove: boolean = true;
-
- constructor(props: FishingMinigameProps) {
- super(props);
-
- const fishHeight = 50;
- const startingCompletion = 30;
-
- // Set things depending on difficulty
- const baitHeight = 170 + (150 - props.difficulty);
-
- this.completionLossPerSecond = props.special_rules.includes('limit_loss')
- ? -4
- : -6;
- this.baitBounceCoeff = props.special_rules.includes('weighted') ? 0.1 : 0.6;
- this.idleVelocity = props.special_rules.includes('heavy') ? 10 : 0;
- this.bidirectional = props.special_rules.includes('bidirectional');
- this.no_escape = props.special_rules.includes('no_escape');
- this.accel_up_coeff = props.special_rules.includes('lubed') ? 1.4 : 1;
-
- switch (props.fish_ai) {
- case 'dumb':
- // This is just using defaults
- break;
- case 'slow':
- // Only does long jump, and doesn't change direction until it gets there
- this.baseShortJumpChancePerSecond = 0;
- this.baseLongJumpChancePerSecond = 0.15;
- this.longJumpVelocityLimit = 150;
- this.interruptMove = false;
- break;
- case 'zippy':
- this.baseShortJumpChancePerSecond *= 3;
- break;
- }
-
- // Start at the bottom
- this.state = {
- completion: startingCompletion,
- bait: {
- position: this.area_height - baitHeight,
- height: baitHeight,
- velocity: this.idleVelocity,
- },
- fish: {
- position: this.area_height - fishHeight,
- height: fishHeight,
- velocity: this.idleVelocity,
- target: null,
- },
- };
-
- this.handle_mousedown = this.handle_mousedown.bind(this);
- this.handle_mouseup = this.handle_mouseup.bind(this);
- this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleKeyUp = this.handleKeyUp.bind(this);
- this.handle_ctrldown = this.handle_ctrldown.bind(this);
- this.handle_ctrlup = this.handle_ctrlup.bind(this);
- this.updateAnimation = this.updateAnimation.bind(this);
- this.moveFish = this.moveFish.bind(this);
- this.moveBait = this.moveBait.bind(this);
- this.updateCompletion = this.updateCompletion.bind(this);
- }
-
- componentDidMount() {
- // add binds blah blah
- document.addEventListener('mousedown', this.handle_mousedown);
- document.addEventListener('mouseup', this.handle_mouseup);
- this.animation_id = window.requestAnimationFrame(this.updateAnimation);
- globalEvents.on('byond/mousedown', this.handle_mousedown);
- globalEvents.on('byond/mouseup', this.handle_mouseup);
- globalEvents.on('byond/ctrldown', this.handle_ctrldown);
- globalEvents.on('byond/ctrlup', this.handle_ctrlup);
- }
-
- componentWillUnmount() {
- document.removeEventListener('mousedown', this.handle_mousedown);
- document.removeEventListener('mouseup', this.handle_mouseup);
- window.cancelAnimationFrame(this.animation_id);
- globalEvents.off('byond/mousedown', this.handle_mousedown);
- globalEvents.off('byond/mouseup', this.handle_mouseup);
- globalEvents.off('byond/ctrldown', this.handle_ctrldown);
- globalEvents.off('byond/ctrlup', this.handle_ctrlup);
- }
-
- updateAnimation(timestamp: DOMHighResTimeStamp) {
- const last = this.last_frame === undefined ? timestamp : this.last_frame;
- const delta = timestamp - last;
- let newState: FishingMinigameState = { ...this.state };
- newState = this.moveFish(newState, delta, timestamp);
- newState = this.moveBait(newState, delta);
- newState = this.updateCompletion(newState, delta);
- this.setState(newState);
- // wait for next frame
- this.last_frame = timestamp;
- this.animation_id = window.requestAnimationFrame(this.updateAnimation);
- }
-
- moveFish(
- currentState: FishingMinigameState,
- delta: number,
- timestamp: DOMHighResTimeStamp
- ): FishingMinigameState {
- const seconds = delta / 1000;
- const { fish: currentFishState } = this.state;
-
- const longJumpChance =
- this.baseLongJumpChancePerSecond * this.props.difficulty * seconds * 100;
-
- const shortJumpChance =
- this.baseShortJumpChancePerSecond * this.props.difficulty * seconds * 100;
-
- const nextFishState = { ...currentFishState };
-
- // Switching to new long jump target can interrupt any other
- if (
- (this.interruptMove || currentFishState.target === null) &&
- randomProb(longJumpChance)
- ) {
- /*
- Move at least 0.75 to full of the availible bar in given direction,
- and more likely to move in the direction where there's more space
- */
- const distanceFromTop = 0 - currentFishState.position;
- const distanceFromBottom =
- this.area_height -
- (currentFishState.position + currentFishState.height);
-
- const absTop = Math.abs(distanceFromTop);
- const absBottom = Math.abs(distanceFromBottom);
- const topChance = (absTop / (absTop + absBottom)) * 100;
-
- const maxFishPosition = this.area_height - currentFishState.height;
- if (randomProb(topChance)) {
- // Moving to top
- const delta = Math.floor(distanceFromTop * randomNumber(0.75, 1));
- } else {
- // Moving to bottom
- const delta = Math.floor(distanceFromBottom * randomNumber(0.75, 1));
- }
- const newTarget = currentFishState.position + delta;
- nextFishState.target = clamp(newTarget, 0, maxFishPosition);
- this.currentVelocityLimit = this.longJumpVelocityLimit;
- }
-
- const activeTarget =
- currentFishState.target &&
- Math.abs(currentFishState.target - currentFishState.position) > 5;
-
- if (activeTarget) {
- // Move towards target
- const distance = currentFishState.target! - currentFishState.position;
- const friction = 0.9;
- // about 5 at diff 15 , 10 at diff 30, 30 at diff 100;
- const diffCoeff = 0.3 * this.props.difficulty + 0.5;
- const targetAcceleration = distance * diffCoeff * seconds;
-
- nextFishState.velocity =
- currentFishState.velocity * friction + targetAcceleration;
- } else {
- // If we have the target but we're close enough, mark as target reached
- if (
- currentFishState.target &&
- Math.abs(currentFishState.target - currentFishState.position) < 5
- ) {
- nextFishState.target = null;
- }
- // Try to do a short jump - these can't really be interrupted
- if (randomProb(shortJumpChance)) {
- const distanceFromTop = 0 - currentFishState.position;
- const distanceFromBottom =
- this.area_height -
- (currentFishState.position + currentFishState.height);
- let possibleMoves: number[] = [];
- if (Math.abs(distanceFromBottom) > 100) {
- possibleMoves.push(randomInteger(100, 200));
- }
- if (Math.abs(distanceFromTop) > 100) {
- possibleMoves.push(randomInteger(-200, -100));
- }
- const delta = randomPick(possibleMoves);
- const maxFishPosition = this.area_height - currentFishState.height;
- const rawTarget = currentFishState.position + delta;
- nextFishState.target = clamp(rawTarget, 0, maxFishPosition);
- this.currentVelocityLimit = this.shortJumpVelocityLimit;
- }
- }
- nextFishState.velocity = clamp(
- nextFishState.velocity + this.idleVelocity,
- -this.currentVelocityLimit,
- this.currentVelocityLimit
- );
-
- nextFishState.position =
- currentFishState.position + seconds * currentFishState.velocity;
-
- // Top bound
- if (nextFishState.position < 0) {
- nextFishState.position = 0;
- }
- // Bottom bound
- if (nextFishState.position + nextFishState.height > this.area_height) {
- nextFishState.position = this.area_height - nextFishState.height;
- }
-
- const newState: FishingMinigameState = {
- ...currentState,
- fish: nextFishState,
- };
- return newState;
- }
-
- moveBait(
- currentState: FishingMinigameState,
- delta: number
- ): FishingMinigameState {
- const seconds = delta / 1000;
- const { fish, bait } = this.state;
-
- // Speedup when reeling
- const acceleration_up = -1200 * this.accel_up_coeff;
- // Gravity
- const acceleration_down = 800;
- // Velocity is multiplied by this when bouncing off the bottom/top
- const bounce_coeff = this.baitBounceCoeff;
- // Acceleration mod when bait is over fish
- const on_point_coeff = 0.6;
-
- let newPosition = bait.position + seconds * bait.velocity;
- let newVelocity = bait.velocity;
-
- // Top bound
- if (newPosition < 0) {
- newPosition = 0;
- if (this.reeling === ReelingState.Reeling) {
- newVelocity = 0;
- } else {
- newVelocity = -bait.velocity * bounce_coeff;
- }
- }
- // Bottom bound
- if (newPosition + bait.height > this.area_height) {
- newPosition = this.area_height - bait.height;
- if (this.reeling === ReelingState.ReelingDown) {
- newVelocity = 0;
- } else {
- newVelocity = -bait.velocity * bounce_coeff;
- }
- }
-
- let acceleration = 0;
- switch (this.reeling) {
- case ReelingState.Reeling:
- acceleration = acceleration_up;
- break;
- case ReelingState.ReelingDown:
- acceleration = -acceleration_up;
- break;
- case ReelingState.Idle:
- acceleration =
- this.bidirectional && newVelocity > 0
- ? -acceleration_down
- : acceleration_down;
- break;
- }
-
- // Slowdown both ways when on fish
- const velocity_change =
- acceleration *
- seconds *
- (this.fishOnBait(fish, bait) ? on_point_coeff : 1);
-
- const brake_coeff = 2;
- /*
- * Basically, if current velocity and the change of velocity
- * are going in different directions, we ensure the bait decelerates
- * towards 0 velocity, making it less slippery, thus easier to control.
- */
- if (newVelocity > 0 && velocity_change < 0) {
- newVelocity += Math.max(-newVelocity, velocity_change * brake_coeff);
- } else if (newVelocity < 0 && velocity_change > 0) {
- newVelocity += Math.min(-newVelocity, velocity_change * brake_coeff);
- }
-
- newVelocity += velocity_change;
- // Ensure that bidirectional baits stay in place
- if (this.bidirectional && this.reeling === ReelingState.Idle) {
- newVelocity =
- velocity_change < 0
- ? Math.max(newVelocity, 0)
- : Math.min(newVelocity, 0);
- }
-
- // Round it off and cap
- if (Math.abs(newVelocity) < 0.01) {
- newVelocity = 0;
- }
-
- const newState: FishingMinigameState = {
- ...currentState,
- bait: { ...bait, position: newPosition, velocity: newVelocity },
- };
- return newState;
- }
-
- updateCompletion(
- currentState: FishingMinigameState,
- delta: number
- ): FishingMinigameState {
- const seconds = delta / 1000;
- const completion_gain_per_second = 5;
- const completion_lost_per_second = this.completionLossPerSecond;
-
- const { fish, bait } = currentState;
-
- let completion_delta = 0;
- if (this.fishOnBait(fish, bait)) {
- completion_delta = seconds * completion_gain_per_second;
- } else {
- completion_delta = seconds * completion_lost_per_second;
- }
- const rawCompletion = currentState.completion + completion_delta;
- const newCompletion = clamp(rawCompletion, 0, 100);
- const newState: FishingMinigameState = {
- ...currentState,
- completion: newCompletion,
- };
-
- const dispatch = useDispatch(this.context);
-
- if (newCompletion <= 0 && !this.no_escape) {
- this.props.lose();
- } else if (newCompletion >= 100) {
- this.props.win();
- }
-
- return newState;
- }
-
- fishOnBait(fish: Fish, bait: Bait): boolean {
- const upperBoundCheck = fish.position >= bait.position;
- const fishLowerBound = fish.position + fish.height;
- const baitLowerBound = bait.position + bait.height;
- const lowerBoundCheck = fishLowerBound <= baitLowerBound;
- return lowerBoundCheck && upperBoundCheck;
- }
-
- handle_mousedown(event: MouseEvent) {
- if (this.reeling === ReelingState.Idle) {
- this.reeling = ReelingState.Reeling;
- }
- }
-
- handle_mouseup(event: MouseEvent) {
- if (this.reeling === ReelingState.Reeling) {
- this.reeling = ReelingState.Idle;
- }
- }
-
- handleKeyDown(keyEvent: KeyEvent) {
- if (keyEvent.code === KEY_CTRL) {
- this.handle_ctrldown();
- }
- }
-
- handleKeyUp(keyEvent: KeyEvent) {
- if (keyEvent.code === KEY_CTRL) {
- this.handle_ctrlup();
- }
- }
-
- handle_ctrldown() {
- if (this.bidirectional && this.reeling === ReelingState.Idle) {
- this.reeling = ReelingState.ReelingDown;
- }
- }
-
- handle_ctrlup() {
- if (this.bidirectional && this.reeling === ReelingState.ReelingDown) {
- this.reeling = ReelingState.Idle;
- }
- }
-
- render() {
- const { completion, fish, bait } = this.state;
- const posToStyle = (value: number) => (value / this.area_height) * 100;
- const background_image = resolveAsset(this.props.background);
- return (
-
- );
- }
-}
-
-type FishingData = {
- difficulty: number;
- fish_ai: FishAI;
- special_effects: SpecialRule[];
- background_image: string;
-};
-
-export const Fishing = (props, context) => {
- const { act, data } = useBackend(context);
- return (
-
-
- act('win')}
- lose={() => act('lose')}
- />
-
-
- );
-};