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 00000000000..b68acee09b7 Binary files /dev/null and b/icons/hud/fishing_hud.dmi differ diff --git a/icons/ui_icons/fishing/default.png b/icons/ui_icons/fishing/default.png deleted file mode 100644 index 20dabae9210..00000000000 Binary files a/icons/ui_icons/fishing/default.png and /dev/null differ diff --git a/icons/ui_icons/fishing/lavaland.png b/icons/ui_icons/fishing/lavaland.png deleted file mode 100644 index b6e2487bdc4..00000000000 Binary files a/icons/ui_icons/fishing/lavaland.png and /dev/null differ 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')} - /> - - - ); -};