Fishing, Version 1 (#67691)

Adds fishing and fishing minigame.
You use fishing rod to fish.
Equipping specific bait/hook/reels will affect your success chances.
You can fish out fish,items and other things.

Fishing Equipment
Fishing rods have three slots: Bait, Reel and Hook.
Any food can be used as bait but dedicated bait makes fishing easier.
You can buy hook and line sets
New bait types:

Worms : Buy can of them at cargo (alternative acquirement method pending)
Doughballs : Use knife on flat piece of dough to get five of them.
Fishing rod types:

Basic : Print these at the lathe, nothing fancy here.
Tech: Experimental tech. Provides infinite bait
Fishing rods can also hook and reel normal items.

Equipment screen and reeling video
Fishing spots
Keep in mind this PR is meant to add the basic systems and i intend to fill these with more fish in future PR's so wait with suggestions until then.

Lavaland lava (no fish here right now, just other stuff), requires reinforced line to fish in.
Maintenance moisture traps.
Beach away mission water.
Fishing portal available for purchase from cargo - This is stopgap until we fill more spots.
Difficulty depends on fishing spot, fish type, and the fish traits and rod setup combinations.
All fish types can have specific traits, most common ones being favourite and disliked bait types/categories.

Other
Fishing catalog now lists fishing related info
New admin debug verb, fishing calculator that show probabilities with different setups so it's easier to balance this.
Fish now have average weight and size. Make sure to boast if you catch a big one.
Adds tgui mouse passthrough
Screens
Sprites:

Fishing portal sprite by @ArcaneMusic
Other sprites by @Mey-Ha-Zah
Bad ones by me. (Could still use better fishing minigame backgrounds)
Sounds:

https://freesound.org/people/soundscalpel.com/sounds/110393/
https://freesound.org/people/soundslikewillem/sounds/343748/
This commit is contained in:
AnturK
2022-06-16 22:36:10 +01:00
committed by GitHub
parent 31c4cc2e0b
commit fddb6ea124
64 changed files with 2975 additions and 306 deletions
@@ -0,0 +1,3 @@
/// Called before beam is redrawn
#define COMSIG_BEAM_BEFORE_DRAW "beam_before_draw"
#define BEAM_CANCEL_DRAW (1 << 0)
@@ -5,3 +5,15 @@
// Fish signals
#define COMSIG_FISH_STATUS_CHANGED "fish_status_changed"
#define COMSIG_FISH_STIRRED "fish_stirred"
/// Fishing challenge completed
#define COMSIG_FISHING_CHALLENGE_COMPLETED "fishing_completed"
/// Called when you try to use fishing rod on anything
#define COMSIG_PRE_FISHING "pre_fishing"
/// Sent by the target of the fishing rod cast
#define COMSIG_FISHING_ROD_CAST "fishing_rod_cast"
#define FISHING_ROD_CAST_HANDLED (1 << 0)
/// Sent when fishing line is snapped
#define COMSIG_FISHING_LINE_SNAPPED "fishing_line_interrupted"
+41
View File
@@ -0,0 +1,41 @@
/// Use in fish tables to denote miss chance.
#define FISHING_DUD "dud"
#define FISHING_BAIT_TRAIT "fishing_bait"
#define BASIC_QUALITY_BAIT_TRAIT "removes_felinids_pr"
#define GOOD_QUALITY_BAIT_TRAIT "adds_bitcoin_miner_pr"
#define GREAT_QUALITY_BAIT_TRAIT "perspective_walls_pr"
// Baseline fishing difficulty levels
#define FISHING_DEFAULT_DIFFICULTY 15
/// Difficulty modifier when bait is fish's favorite
#define FAV_BAIT_DIFFICULTY_MOD -5
/// Difficulty modifier when bait is fish's disliked
#define DISLIKED_BAIT_DIFFICULTY_MOD 15
#define FISH_TRAIT_MINOR_DIFFICULTY_BOOST 5
// These define how the fish will behave in the minigame
#define FISH_AI_DUMB "dumb"
#define FISH_AI_ZIPPY "zippy"
#define FISH_AI_SLOW "slow"
#define ADDITIVE_FISHING_MOD "additive"
#define MULTIPLICATIVE_FISHING_MOD "multiplicative"
#define FISHING_HOOK_MAGNETIC (1 << 0)
#define FISHING_HOOK_SHINY (1 << 1)
#define FISHING_HOOK_WEIGHTED (1 << 2)
#define FISHING_LINE_CLOAKED (1 << 0)
#define FISHING_LINE_REINFORCED (1 << 1)
#define FISHING_LINE_BOUNCY (1 << 2)
#define FISHING_SPOT_PRESET_BEACH "beach"
#define FISHING_SPOT_PRESET_LAVALAND_LAVA "lavaland lava"
#define FISHING_MINIGAME_RULE_HEAVY_FISH "heavy"
#define FISHING_MINIGAME_RULE_WEIGHTED_BAIT "weighted"
#define FISHING_MINIGAME_RULE_LIMIT_LOSS "limit_loss"
+23
View File
@@ -41,6 +41,29 @@
"BUGS", \
)
/// IC meaning (more or less) for food flags
#define FOOD_FLAGS_IC list( \
"Meat", \
"Vegetables", \
"Raw food", \
"Junk food", \
"Grain", \
"Fruits", \
"Dairy products", \
"Fried food", \
"Alcohol", \
"Sugary food", \
"Gross food", \
"Toxic food", \
"Pineapples", \
"Breakfast food", \
"Clothing", \
"Nuts", \
"Seafood", \
"Oranges", \
"Bugs", \
)
#define DRINK_NICE 1
#define DRINK_GOOD 2
#define DRINK_VERYGOOD 3
+3
View File
@@ -894,3 +894,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Ignores body_parts_covered during the add_fingerprint() proc. Works both on the person and the item in the glove slot.
#define TRAIT_FINGERPRINT_PASSTHROUGH "fingerprint_passthrough"
/// Currently fishing
#define TRAIT_GONE_FISHING "fishing"
+15 -5
View File
@@ -1,11 +1,21 @@
///Calculate the angle between two points and the west|east coordinate
///Calculate the angle between two movables and the west|east coordinate
/proc/get_angle(atom/movable/start, atom/movable/end)//For beams.
if(!start || !end)
return 0
var/dy
var/dx
dy=(32 * end.y + end.pixel_y) - (32 * start.y + start.pixel_y)
dx=(32 * end.x + end.pixel_x) - (32 * start.x + start.pixel_x)
var/dy =(32 * end.y + end.pixel_y) - (32 * start.y + start.pixel_y)
var/dx =(32 * end.x + end.pixel_x) - (32 * start.x + start.pixel_x)
if(!dy)
return (dx >= 0) ? 90 : 270
. = arctan(dx/dy)
if(dy < 0)
. += 180
else if(dx < 0)
. += 360
/// Angle between two arbitrary points and horizontal line same as [/proc/get_angle]
/proc/get_angle_raw(start_x, start_y, start_pixel_x, start_pixel_y, end_x, end_y, end_pixel_x, end_pixel_y)
var/dy = (32 * end_y + end_pixel_y) - (32 * start_y + start_pixel_y)
var/dx = (32 * end_x + end_pixel_x) - (32 * start_x + start_pixel_x)
if(!dy)
return (dx >= 0) ? 90 : 270
. = arctan(dx/dy)
@@ -289,6 +289,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items
/obj/item/shield/riot/buckler = 1,
/obj/item/throwing_star = 1,
/obj/item/weldingtool/hugetank = 1,
/obj/item/fishing_rod/master = 1,
) = 1,
list(//equipment
+41 -17
View File
@@ -30,8 +30,16 @@
var/obj/effect/ebeam/visuals
///The color of the beam we're drawing.
var/beam_color
/// If set will be used instead of origin's pixel_x in offset calculations
var/override_origin_pixel_x = null
/// If set will be used instead of origin's pixel_y in offset calculations
var/override_origin_pixel_y = null
/// If set will be used instead of targets's pixel_x in offset calculations
var/override_target_pixel_x = null
/// If set will be used instead of targets's pixel_y in offset calculations
var/override_target_pixel_y = null
/datum/beam/New(origin, target, icon = 'icons/effects/beam.dmi', icon_state = "b_beam", time = INFINITY, max_distance = INFINITY, beam_type = /obj/effect/ebeam, beam_color = null)
/datum/beam/New(origin, target, icon = 'icons/effects/beam.dmi', icon_state = "b_beam", time = INFINITY, max_distance = INFINITY, beam_type = /obj/effect/ebeam, beam_color = null, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, override_target_pixel_y = null)
src.origin = origin
src.target = target
src.icon = icon
@@ -39,6 +47,10 @@
src.max_distance = max_distance
src.beam_type = beam_type
src.beam_color = beam_color
src.override_origin_pixel_x = override_origin_pixel_x
src.override_origin_pixel_y = override_origin_pixel_y
src.override_target_pixel_x = override_target_pixel_x
src.override_target_pixel_y = override_target_pixel_y
if(time < INFINITY)
QDEL_IN(src, time)
@@ -85,33 +97,41 @@
* Creates the beam effects and places them in a line from the origin to the target. Sets their rotation to make the beams face the target, too.
*/
/datum/beam/proc/Draw()
var/Angle = round(get_angle(origin,target))
if(SEND_SIGNAL(src, COMSIG_BEAM_BEFORE_DRAW) & BEAM_CANCEL_DRAW)
return
var/origin_px = isnull(override_origin_pixel_x) ? origin.pixel_x : override_origin_pixel_x
var/origin_py = isnull(override_origin_pixel_y) ? origin.pixel_y : override_origin_pixel_y
var/target_px = isnull(override_target_pixel_x) ? target.pixel_x : override_target_pixel_x
var/target_py = isnull(override_target_pixel_y) ? target.pixel_y : override_target_pixel_y
var/Angle = get_angle_raw(origin.x, origin.y, origin_px, origin_py, target.x , target.y, target_px, target_py)
///var/Angle = round(get_angle(origin,target))
var/matrix/rot_matrix = matrix()
var/turf/origin_turf = get_turf(origin)
rot_matrix.Turn(Angle)
//Translation vector for origin and target
var/DX = (32*target.x+target.pixel_x)-(32*origin.x+origin.pixel_x)
var/DY = (32*target.y+target.pixel_y)-(32*origin.y+origin.pixel_y)
var/DX = (32*target.x+target_px)-(32*origin.x+origin_px)
var/DY = (32*target.y+target_py)-(32*origin.y+origin_py)
var/N = 0
var/length = round(sqrt((DX)**2+(DY)**2)) //hypotenuse of the triangle formed by target and origin's displacement
for(N in 0 to length-1 step 32)//-1 as we want < not <=, but we want the speed of X in Y to Z and step X
if(QDELETED(src))
break
var/obj/effect/ebeam/X = new beam_type(origin_turf)
X.owner = src
elements += X
var/obj/effect/ebeam/segment = new beam_type(origin_turf)
segment.owner = src
elements += segment
//Assign our single visual ebeam to each ebeam's vis_contents
//ends are cropped by a transparent box icon of length-N pixel size laid over the visuals obj
if(N+32>length) //went past the target, we draw a box of space to cut away from the beam sprite so the icon actually ends at the center of the target sprite
var/icon/II = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam...
II.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon
X.icon = II
segment.icon = II
segment.color = beam_color
else
X.vis_contents += visuals
X.transform = rot_matrix
segment.vis_contents += visuals
segment.transform = rot_matrix
//Calculate pixel offsets (If necessary)
var/Pixel_x
@@ -129,15 +149,15 @@
var/a
if(abs(Pixel_x)>32)
a = Pixel_x > 0 ? round(Pixel_x/32) : CEILING(Pixel_x/32, 1)
X.x += a
segment.x += a
Pixel_x %= 32
if(abs(Pixel_y)>32)
a = Pixel_y > 0 ? round(Pixel_y/32) : CEILING(Pixel_y/32, 1)
X.y += a
segment.y += a
Pixel_y %= 32
X.pixel_x = Pixel_x
X.pixel_y = Pixel_y
segment.pixel_x = origin_px + Pixel_x
segment.pixel_y = origin_py + Pixel_y
CHECK_TICK
/obj/effect/ebeam
@@ -147,7 +167,9 @@
/obj/effect/ebeam/update_overlays()
. = ..()
. += emissive_appearance(icon, icon_state)
var/mutable_appearance/emmisive = emissive_appearance(icon, icon_state)
emmisive.transform = transform
. += emmisive
/obj/effect/ebeam/Destroy()
owner = null
@@ -170,7 +192,9 @@
* maxdistance: how far the beam will go before stopping itself. Used mainly for two things: preventing lag if the beam may go in that direction and setting a range to abilities that use beams.
* beam_type: The type of your custom beam. This is for adding other wacky stuff for your beam only. Most likely, you won't (and shouldn't) change it.
*/
/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=INFINITY,maxdistance=INFINITY,beam_type=/obj/effect/ebeam, beam_color = null)
var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color)
/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=INFINITY,maxdistance=INFINITY,beam_type=/obj/effect/ebeam, beam_color = null, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, override_target_pixel_y = null)
var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y )
INVOKE_ASYNC(newbeam, /datum/beam/.proc/Start)
return newbeam
+62
View File
@@ -0,0 +1,62 @@
// A thing you can fish in
/datum/component/fishing_spot
/// Defines the probabilities and fish availibilty
var/datum/fish_source/fish_source
/datum/component/fishing_spot/Initialize(configuration)
if(ispath(configuration,/datum/fish_source))
//Create new one of the given type
fish_source = new configuration
else if(istype(configuration,/datum/fish_source))
//Use passed in instance
fish_source = configuration
else
/// Check if it's a preset key
var/datum/fish_source/preset_configuration = GLOB.preset_fish_sources[configuration]
if(!preset_configuration)
stack_trace("Invalid fishing spot configuration \"[configuration]\" passed down to fishing spot component.")
return COMPONENT_INCOMPATIBLE
fish_source = preset_configuration
RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/handle_attackby)
RegisterSignal(parent, COMSIG_FISHING_ROD_CAST, .proc/handle_cast)
/datum/component/fishing_spot/proc/handle_cast(datum/source, obj/item/fishing_rod/rod, mob/user)
SIGNAL_HANDLER
if(try_start_fishing(rod,user))
return FISHING_ROD_CAST_HANDLED
return NONE
/datum/component/fishing_spot/proc/handle_attackby(datum/source, obj/item/item, mob/user, params)
SIGNAL_HANDLER
if(try_start_fishing(item,user))
return COMPONENT_NO_AFTERATTACK
return NONE
/datum/component/fishing_spot/proc/try_start_fishing(obj/item/possibly_rod, mob/user)
SIGNAL_HANDLER
var/obj/item/fishing_rod/rod = possibly_rod
if(!istype(rod))
return
if(HAS_TRAIT(user,TRAIT_GONE_FISHING) || rod.currently_hooked_item)
user.balloon_alert(user, "already fishing")
return COMPONENT_NO_AFTERATTACK
var/denial_reason = fish_source.can_fish(rod, user)
if(denial_reason)
to_chat(user, span_warning(denial_reason))
return COMPONENT_NO_AFTERATTACK
start_fishing_challenge(rod, user)
return COMPONENT_NO_AFTERATTACK
/datum/component/fishing_spot/proc/start_fishing_challenge(obj/item/fishing_rod/rod, mob/user)
/// Roll what we caught based on modified table
var/result = fish_source.roll_reward(rod, user)
var/datum/fishing_challenge/challenge = new(parent, result, rod, user)
challenge.background = fish_source.background
challenge.difficulty = fish_source.calculate_difficulty(result, rod, user)
RegisterSignal(challenge, COMSIG_FISHING_CHALLENGE_COMPLETED, .proc/fishing_completed)
challenge.start(user)
/datum/component/fishing_spot/proc/fishing_completed(datum/fishing_challenge/source, mob/user, success, perfect)
if(success)
fish_source.dispense_reward(source.reward_path, user)
+25
View File
@@ -0,0 +1,25 @@
// Lazy fishing spot element so fisheable turfs do not have a component each since they're usually pretty common on their respective maps (lava/water/etc)
/datum/element/lazy_fishing_spot
element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
id_arg_index = 2
var/configuration
/datum/element/lazy_fishing_spot/Attach(datum/target, configuration)
. = ..()
if(!isatom(target))
return ELEMENT_INCOMPATIBLE
if(!configuration)
CRASH("Lazy fishing spot had no configuration passed in.")
src.configuration = configuration
RegisterSignal(target, COMSIG_PRE_FISHING, .proc/create_fishing_spot)
/datum/element/lazy_fishing_spot/Detach(datum/target)
UnregisterSignal(target, COMSIG_PRE_FISHING)
return ..()
/datum/element/lazy_fishing_spot/proc/create_fishing_spot(datum/source)
SIGNAL_HANDLER
source.AddComponent(/datum/component/fishing_spot, configuration)
Detach(source)
@@ -305,3 +305,8 @@
/datum/mood_event/russian_roulette_win/add_effects(loaded_rounds)
mood_change = 2 ** loaded_rounds
/datum/mood_event/fishing
description = "Fishing is relaxing."
mood_change = 5
timeout = 3 MINUTES
+1
View File
@@ -65,6 +65,7 @@
MakeGrillable()
MakeDecompose(mapload)
MakeBakeable()
ADD_TRAIT(src, FISHING_BAIT_TRAIT, INNATE_TRAIT)
///This proc adds the edible component, overwrite this if you for some reason want to change some specific args like callbacks.
/obj/item/food/proc/MakeEdible()
+47
View File
@@ -0,0 +1,47 @@
/obj/item/food/bait
name = "this is bait"
desc = "you got baited."
icon = 'icons/obj/fishing.dmi'
/// Quality trait of this bait
var/bait_quality = BASIC_QUALITY_BAIT_TRAIT
/// Icon state added to main fishing rod icon when this bait is equipped
var/rod_overlay_icon_state
/obj/item/food/bait/Initialize(mapload)
. = ..()
ADD_TRAIT(src, bait_quality, INNATE_TRAIT)
/obj/item/food/bait/worm
name = "worm"
desc = "It's a wriggling worm from a can of fishing bait. You're not going to eat it are you ?"
icon = 'icons/obj/fishing.dmi'
icon_state = "worm"
food_reagents = list(/datum/reagent/consumable/nutriment/protein = 1)
tastes = list("meat" = 1, "worms" = 1)
foodtypes = GROSS | MEAT | BUGS
w_class = WEIGHT_CLASS_TINY
bait_quality = BASIC_QUALITY_BAIT_TRAIT
rod_overlay_icon_state = "worm_overlay"
/obj/item/food/bait/worm/premium
name = "extra slimy worm"
desc = "This worm looks very sophisticated."
bait_quality = GOOD_QUALITY_BAIT_TRAIT
/obj/item/food/bait/doughball
name = "doughball"
desc = "Small piece of dough. Simple but effective fishing bait."
icon = 'icons/obj/fishing.dmi'
icon_state = "doughball"
food_reagents = list(/datum/reagent/consumable/nutriment/protein = 1)
tastes = list("dough" = 1)
foodtypes = GRAIN
w_class = WEIGHT_CLASS_TINY
bait_quality = BASIC_QUALITY_BAIT_TRAIT
rod_overlay_icon_state = "dough_overlay"
/// These are generated by tech fishing rod
/obj/item/food/bait/doughball/synthetic
name = "synthetic doughball"
icon_state = "doughball"
preserved_food = TRUE
+3
View File
@@ -59,6 +59,9 @@
/obj/item/food/doughslice/MakeBakeable()
AddComponent(/datum/component/bakeable, /obj/item/food/bun, rand(20 SECONDS, 25 SECONDS), TRUE, TRUE)
/obj/item/food/doughslice/MakeProcessable()
AddElement(/datum/element/processable, TOOL_KNIFE, /obj/item/food/bait/doughball, 5, 30)
/obj/item/food/bun
name = "bun"
desc = "A base for any self-respecting burger."
+7 -1
View File
@@ -25,7 +25,6 @@ at the cost of risking a vicious bite.**/
/obj/item/restraints/handcuffs/cable/pink = 1,
/obj/item/restraints/handcuffs/alien = 2,
/obj/item/coin/bananium = 9,
/obj/item/fish/ratfish = 10,
/obj/item/knife/butcher = 5,
/obj/item/coin/mythril = 1,
)
@@ -41,6 +40,13 @@ at the cost of risking a vicious bite.**/
var/picked_item = pick_weight(loot_table)
hidden_item = new picked_item(src)
var/datum/fish_source/moisture_trap/fish_source = new
if(prob(50)) // 50% chance there's another item to fish out of there
var/picked_item = pick_weight(loot_table)
fish_source.fish_table[picked_item] = 5
fish_source.fish_counts[picked_item] = 1;
AddComponent(/datum/component/fishing_spot, fish_source)
/obj/structure/moisture_trap/Destroy()
if(hidden_item)
+4
View File
@@ -27,6 +27,10 @@
/// objects with these flags won't burn.
var/immunity_resistance_flags = LAVA_PROOF
/turf/open/lava/Initialize(mapload)
. = ..()
AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_LAVALAND_LAVA)
/turf/open/lava/ex_act(severity, target)
return
+4
View File
@@ -29,6 +29,10 @@
base_icon_state = "water"
baseturfs = /turf/open/water/beach
/turf/open/water/beach/Initialize(mapload)
. = ..()
AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_BEACH)
//Same turf, but instead used in the Beach Biodome
/turf/open/water/beach/biodome
initial_gas_mix = OPENTURF_DEFAULT_ATMOS
+1
View File
@@ -79,6 +79,7 @@ GLOBAL_PROTECT(admin_verbs_admin)
/datum/admins/proc/known_alts_panel,
/datum/admins/proc/paintings_manager,
/datum/admins/proc/display_tags,
/datum/admins/proc/fishing_calculator,
)
GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel))
GLOBAL_PROTECT(admin_verbs_ban)
@@ -1,19 +0,0 @@
/*
// Default size of the "greyscale_fish" icon_state
sprite_height = 3
sprite_width = 3
/// This path exists mostly for admin abuse.
/datum/aquarium_behaviour/fish/auto
name = "automatic fish"
desc = "generates fish appearance automatically from component parent appearance"
available_in_random_cases = FALSE
sprite_width = 8
sprite_height = 8
show_in_catalog = FALSE
/datum/aquarium_behaviour/fish/auto/apply_appearance(obj/effect/holder)
holder.appearance = parent.parent
holder.transform = base_transform()
holder.dir = WEST
*/
+7
View File
@@ -10,3 +10,10 @@
if(sprites[id]) //no dupes
continue
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'
)
+24
View File
@@ -167,3 +167,27 @@
desc = "A complete meal package for the terminally lazy. Contains one Ready-Donk meal."
cost = PAYCHECK_CREW * 2
contains = list(/obj/item/food/ready_donk)
/datum/supply_pack/goody/fishing_toolbox
name = "Fishing toolbox"
desc = "Complete toolbox set for your fishing adventure. Advanced hooks and lines sold separetely."
cost = PAYCHECK_CREW * 2
contains = list(/obj/item/storage/toolbox/fishing)
/datum/supply_pack/goody/fishing_hook_set
name = "Fishing Hooks Set"
desc = "Set of various fishing hooks."
cost = PAYCHECK_CREW
contains = list(/obj/item/storage/box/fishing_hooks)
/datum/supply_pack/goody/fishing_line_set
name = "Fishing Lines Set"
desc = "Set of various fishing lines."
cost = PAYCHECK_CREW
contains = list(/obj/item/storage/box/fishing_lines)
/datum/supply_pack/goody/premium_bait
name = "Deluxe fishing bait"
desc = "When the standard variety is not good enough for you."
cost = PAYCHECK_CREW
contains = list(/obj/item/bait_can/worm/premium)
+8
View File
@@ -2800,6 +2800,14 @@
crate_value -= uplink_item.cost
new uplink_item.item(C)
/datum/supply_pack/misc/fishing_portal
name = "Fishing Portal Generator Crate"
desc = "Not enough fish near your location? Fishing portal has your back."
cost = CARGO_CRATE_VALUE * 4
contains = list(/obj/machinery/fishing_portal_generator)
crate_name = "fishing portal crate"
//////////////////////////////////////////////////////////////////////////////
/////////////////////// General Vending Restocks /////////////////////////////
//////////////////////////////////////////////////////////////////////////////
+75
View File
@@ -0,0 +1,75 @@
// Helper tool to see fishing probabilities with different setups
/datum/admins/proc/fishing_calculator()
set name = "Fishing Calculator"
set category = "Debug"
if(!check_rights(R_DEBUG))
return
var/datum/fishing_calculator/ui = new(usr)
ui.ui_interact(usr)
/datum/fishing_calculator
var/list/current_table
/datum/fishing_calculator/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishingCalculator")
ui.open()
/datum/fishing_calculator/ui_state(mob/user)
return GLOB.admin_state
/datum/fishing_calculator/ui_close(mob/user)
qdel(src)
/datum/fishing_calculator/ui_static_data(mob/user)
. = ..()
.["rod_types"] = typesof(/obj/item/fishing_rod)
.["hook_types"] = typesof(/obj/item/fishing_hook)
.["line_types"] = typesof(/obj/item/fishing_line)
var/list/spot_keys = list()
for(var/key in GLOB.preset_fish_sources)
spot_keys += key
.["spot_types"] = subtypesof(/datum/fish_source) + spot_keys
/datum/fishing_calculator/ui_data(mob/user)
return list("info" = current_table)
/datum/fishing_calculator/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = usr
switch(action)
if("recalc")
var/rod_type = text2path(params["rod"])
var/bait_type = text2path(params["bait"])
var/hook_type = text2path(params["hook"])
var/line_type = text2path(params["line"])
var/spot_type = text2path(params["spot"]) || params["spot"] //can be also key from presets
//validate here against nonsense values
var/datum/fish_source/spot
if(ispath(spot_type))
spot = new spot_type
else
spot = GLOB.preset_fish_sources[spot_type]
var/obj/item/fishing_rod/temporary_rod = new rod_type
if(bait_type)
temporary_rod.bait = new bait_type
if(hook_type)
temporary_rod.hook = new hook_type
if(line_type)
temporary_rod.line = new line_type
var/result_table = list()
var/modified_table = spot.get_modified_fish_table(temporary_rod,user)
for(var/result_type in spot.fish_table) // through this not modified to display 0 chance ones too
var/list/info = list()
info["result"] = result_type
info["weight"] = modified_table[result_type] || 0
info["difficulty"] = spot.calculate_difficulty(result_type,temporary_rod, user)
info["count"] = spot.fish_counts[result_type] || "Infinite"
result_table += list(info)
current_table = result_table
qdel(temporary_rod)
return TRUE
@@ -1,4 +1,3 @@
///Fish feed can
/obj/item/fish_feed
name = "fish feed can"
@@ -59,57 +58,6 @@
var/fish_type = pick(/obj/item/fish/dwarf_moonfish, /obj/item/fish/gunner_jellyfish, /obj/item/fish/needlefish, /obj/item/fish/armorfish)
new fish_type(src)
///Book detailing where to get the fish and their properties.
/obj/item/book/fish_catalog
name = "Fish Encyclopedia"
desc = "Indexes all fish known to mankind (and related species)."
icon_state = "fishbook"
starting_content = "Lot of fish stuff" //book wrappers could use cleaning so this is not necessary
/obj/item/book/fish_catalog/on_read(mob/user)
ui_interact(user)
/obj/item/book/fish_catalog/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishCatalog", name)
ui.open()
/obj/item/book/fish_catalog/ui_static_data(mob/user)
. = ..()
var/static/fish_info
if(!fish_info)
fish_info = list()
for(var/_fish_type in subtypesof(/obj/item/fish))
var/obj/item/fish/fish = _fish_type
var/list/fish_data = list()
if(!initial(fish.show_in_catalog))
continue
fish_data["name"] = initial(fish.name)
fish_data["desc"] = initial(fish.desc)
fish_data["fluid"] = initial(fish.required_fluid_type)
fish_data["temp_min"] = initial(fish.required_temperature_min)
fish_data["temp_max"] = initial(fish.required_temperature_max)
fish_data["icon"] = sanitize_css_class_name("[initial(fish.icon)][initial(fish.icon_state)]")
fish_data["color"] = initial(fish.color)
fish_data["source"] = initial(fish.available_in_random_cases) ? "[AQUARIUM_COMPANY] Fish Packs" : "Unknown"
var/datum/reagent/food_type = initial(fish.food)
if(food_type != /datum/reagent/consumable/nutriment)
fish_data["feed"] = initial(food_type.name)
else
fish_data["feed"] = "[AQUARIUM_COMPANY] Fish Feed"
fish_info += list(fish_data)
// TODO: Custom entries
.["fish_info"] = fish_info
.["sponsored_by"] = AQUARIUM_COMPANY
/obj/item/book/fish_catalog/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet/fish)
)
/obj/item/aquarium_kit
name = "DIY Aquarium Construction Kit"
desc = "Everything you need to build your own aquarium. Raw materials sold separately."
+35
View File
@@ -0,0 +1,35 @@
/obj/item/bait_can
name = "can o bait"
desc = "there's a lot of them in there, getting them out takes a while though"
icon = 'icons/obj/fishing.dmi'
icon_state = "bait_can"
w_class = WEIGHT_CLASS_SMALL
/// Tracking until we can take out another bait item
COOLDOWN_DECLARE(bait_removal_cooldown)
/// What bait item it produces
var/bait_type
/// Time between bait retrievals
var/cooldown_time = 10 SECONDS
/obj/item/bait_can/attack_self(mob/user, modifiers)
. = ..()
var/fresh_bait = retrieve_bait(user)
if(fresh_bait)
user.put_in_hands(fresh_bait)
/obj/item/bait_can/proc/retrieve_bait(mob/user)
if(!COOLDOWN_FINISHED(src, bait_removal_cooldown))
user.balloon_alert(user, "wait a bit") //I can't think of generic ic reason.
return
COOLDOWN_START(src, bait_removal_cooldown, cooldown_time)
return new bait_type(src)
/obj/item/bait_can/worm
name = "can o' worm"
desc = "this can got worms."
bait_type = /obj/item/food/bait/worm
/obj/item/bait_can/worm/premium
name = "can o' worm deluxe"
desc = "this can got fancy worms."
bait_type = /obj/item/food/bait/worm/premium
@@ -64,6 +64,41 @@
var/in_stasis = FALSE
// Fishing related properties
/// List of fishing trait types, these modify probabilty/difficulty depending on rod/user properties
var/list/fishing_traits = list()
/// Fishing behaviour
var/fish_ai_type = FISH_AI_DUMB
/// Base additive modifier to fishing difficulty
var/fishing_difficulty_modifier = 0
/**
* Bait identifiers that make catching this fish easier and more likely
* Bait identifiers: Path | Trait | list("Type"="Foodtype","Value"= Food Type Flag like [MEAT])
*/
var/list/favorite_bait = list()
/**
* Bait identifiers that make catching this fish harder and less likely
* Bait identifiers: Path | Trait | list("Type"="Foodtype","Value"= Food Type Flag like [MEAT])
*/
var/list/disliked_bait = list()
/// Size in centimeters
var/size = 50
/// Average size for this fish type in centimeters. Will be used as gaussian distribution with 20% deviation for fishing, bought fish are always standard size
var/average_size = 50
/// Weight in grams
var/weight = 1000
/// Average weight for this fish type in grams
var/average_weight = 1000
/obj/item/fish/Initialize(mapload)
. = ..()
if(fillet_type)
@@ -75,6 +110,24 @@
if(status != FISH_DEAD)
START_PROCESSING(SSobj, src)
size = average_size
weight = average_weight
/obj/item/fish/examine(mob/user)
. = ..()
// All spacemen have magic eyes of fish weight perception until fish scale (get it?) is implemented.
. += span_notice("It's [size] cm long.")
. += span_notice("It weighs [weight] g.")
/obj/item/fish/proc/randomize_weight_and_size(modifier = 0)
var/size_deviation = 0.2 * average_size
var/size_mod = modifier * average_size
size = max(1,gaussian(average_size + size_mod, size_deviation))
var/weight_deviation = 0.2 * average_weight
var/weight_mod = modifier * average_weight
weight = max(1,gaussian(average_weight + weight_mod, weight_deviation))
/obj/item/fish/Moved(atom/OldLoc, Dir)
. = ..()
check_environment_after_movement()
@@ -123,7 +176,7 @@
if(QDELETED(src)) //we don't care anymore
return
// Apply/remove stasis as needed
if(HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE))
if(loc && HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE))
enter_stasis()
else if(in_stasis)
exit_stasis()
@@ -134,7 +187,7 @@
on_aquarium_insertion(loc)
// Start flopping if outside of fish container
var/should_be_flopping = status == FISH_ALIVE && !HAS_TRAIT(loc,TRAIT_FISH_SAFE_STORAGE) && !in_aquarium
var/should_be_flopping = status == FISH_ALIVE && loc && !HAS_TRAIT(loc,TRAIT_FISH_SAFE_STORAGE) && !in_aquarium
if(should_be_flopping)
start_flopping()
@@ -314,202 +367,4 @@
probability_table[argkey] = chance_table
return pick_weight(probability_table[argkey])
// Freshwater fish
/obj/item/fish/goldfish
name = "goldfish"
desc = "Despite common belief, goldfish do not have three-second memories. They can actually remember things that happened up to three months ago."
icon_state = "goldfish"
sprite_width = 8
sprite_height = 8
stable_population = 3
/obj/item/fish/angelfish
name = "angelfish"
desc = "Young Angelfish often live in groups, while adults prefer solitary life. They become territorial and aggressive toward other fish when they reach adulthood."
icon_state = "angelfish"
dedicated_in_aquarium_icon_state = "bigfish"
sprite_height = 7
source_height = 7
stable_population = 3
/obj/item/fish/guppy
name = "guppy"
desc = "Guppy is also known as rainbow fish because of the brightly colored body and fins."
icon_state = "guppy"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#91AE64"
sprite_width = 8
sprite_height = 5
stable_population = 6
/obj/item/fish/plasmatetra
name = "plasma tetra"
desc = "Due to their small size, tetras are prey to many predators in their watery world, including eels, crustaceans, and invertebrates."
icon_state = "plastetra"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#D30EB0"
stable_population = 3
/obj/item/fish/catfish
name = "cory catfish"
desc = "A catfish has about 100,000 taste buds, and their bodies are covered with them to help detect chemicals present in the water and also to respond to touch."
icon_state = "catfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#907420"
stable_population = 3
// Saltwater fish below
/obj/item/fish/clownfish
name = "clownfish"
desc = "Clownfish catch prey by swimming onto the reef, attracting larger fish, and luring them back to the anemone. The anemone will sting and eat the larger fish, leaving the remains for the clownfish."
icon_state = "clownfish"
dedicated_in_aquarium_icon_state = "clownfish_small"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 5
stable_population = 4
/obj/item/fish/cardinal
name = "cardinalfish"
desc = "Cardinalfish are often found near sea urchins, where the fish hide when threatened."
icon_state = "cardinalfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 4
/obj/item/fish/greenchromis
name = "green chromis"
desc = "The Chromis can vary in color from blue to green depending on the lighting and distance from the lights."
icon_state = "greenchromis"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#00ff00"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 5
/obj/item/fish/firefish
name = "firefish goby"
desc = "To communicate in the wild, the firefish uses its dorsal fin to alert others of potential danger."
icon_state = "firefish"
sprite_width = 6
sprite_height = 5
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 3
/obj/item/fish/pufferfish
name = "pufferfish"
desc = "One Pufferfish contains enough toxins in its liver to kill 30 people."
icon_state = "pufferfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 8
stable_population = 3
/obj/item/fish/lanternfish
name = "lanternfish"
desc = "Typically found in areas below 6600 feet below the surface of the ocean, they live in complete darkness."
icon_state = "lanternfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
random_case_rarity = FISH_RARITY_VERY_RARE
source_width = 28
source_height = 21
sprite_width = 8
sprite_height = 8
stable_population = 3
//Tiziran Fish
/obj/item/fish/dwarf_moonfish
name = "dwarf moonfish"
desc = "Ordinarily in the wild, the Zagoskian moonfish is around the size of a tuna, however through selective breeding a smaller breed suitable for being kept as an aquarium pet has been created."
icon_state = "dwarf_moonfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 2
fillet_type = /obj/item/food/fishmeat/moonfish
/obj/item/fish/gunner_jellyfish
name = "gunner jellyfish"
desc = "So called due to their resemblance to an artillery shell, the gunner jellyfish is native to Tizira, where it is enjoyed as a delicacy. Produces a mild hallucinogen that is destroyed by cooking."
icon_state = "gunner_jellyfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 4
fillet_type = /obj/item/food/fishmeat/gunner_jellyfish
/obj/item/fish/needlefish
name = "needlefish"
desc = "A tiny, transparent fish which resides in large schools in the oceans of Tizira. A common food for other, larger fish."
icon_state = "needlefish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 12
fillet_type = null
/obj/item/fish/armorfish
name = "armorfish"
desc = "A small shellfish native to Tizira's oceans, known for its exceptionally hard shell. Consumed similarly to prawns."
icon_state = "armorfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 10
fillet_type = /obj/item/food/fishmeat/armorfish
/obj/item/storage/box/fish_debug
name = "box full of fish"
/obj/item/storage/box/fish_debug/PopulateContents()
for(var/fish_type in subtypesof(/obj/item/fish))
new fish_type(src)
/obj/item/fish/donkfish
name = "donk co. company patent donkfish"
desc = "A lab-grown donkfish. Its invention was an accident for the most part, as it was intended to be consumed in donk pockets. Unfortunately, it tastes horrible, so it has now become a pseudo-mascot."
icon_state = "donkfish"
random_case_rarity = FISH_RARITY_VERY_RARE
required_fluid_type = AQUARIUM_FLUID_FRESHWATER
stable_population = 4
fillet_type = /obj/item/food/fishmeat/donkfish
/obj/item/fish/emulsijack
name = "toxic emulsijack"
desc = "Ah, the terrifying emulsijack. Created in a laboratory, this slimey, scaleless fish emits an invisible toxin that emulsifies other fish for it to feed on. Its only real use is for completely ruining a tank."
icon_state = "emulsijack"
random_case_rarity = FISH_RARITY_GOOD_LUCK_FINDING_THIS
required_fluid_type = AQUARIUM_FLUID_ANADROMOUS
stable_population = 3
/obj/item/fish/emulsijack/process(delta_time = SSOBJ_DT)
var/emulsified = FALSE
var/obj/structure/aquarium/aquarium = loc
if(istype(aquarium))
for(var/obj/item/fish/victim in aquarium)
if(istype(victim, /obj/item/fish/emulsijack))
continue //no team killing
victim.adjust_health((victim.health - 3) * delta_time) //the victim may heal a bit but this will quickly kill
emulsified = TRUE
if(emulsified)
adjust_health((health + 3) * delta_time)
last_feeding = world.time //emulsijack feeds on the emulsion!
..()
/obj/item/fish/ratfish
name = "ratfish"
desc = "A rat exposed to the murky waters of maintenance too long. Any higher power, if it revealed itself, would state that the ratfish's continued existence is extremely unwelcome."
icon_state = "ratfish"
random_case_rarity = FISH_RARITY_RARE
required_fluid_type = AQUARIUM_FLUID_FRESHWATER
stable_population = 10 //set by New, but this is the default config value
fillet_type = /obj/item/food/meat/slab/human/mutant/zombie //eww...
/obj/item/fish/ratfish/Initialize(mapload)
. = ..()
//stable pop reflects the config for how many mice migrate. powerful...
stable_population = CONFIG_GET(number/mice_roundstart)
+242
View File
@@ -0,0 +1,242 @@
// Freshwater fish
/obj/item/fish/goldfish
name = "goldfish"
desc = "Despite common belief, goldfish do not have three-second memories. They can actually remember things that happened up to three months ago."
icon_state = "goldfish"
sprite_width = 8
sprite_height = 8
stable_population = 3
average_size = 30
average_weight = 500
favorite_bait = list(/obj/item/food/bait/worm)
/obj/item/fish/angelfish
name = "angelfish"
desc = "Young Angelfish often live in groups, while adults prefer solitary life. They become territorial and aggressive toward other fish when they reach adulthood."
icon_state = "angelfish"
dedicated_in_aquarium_icon_state = "bigfish"
sprite_height = 7
source_height = 7
average_size = 30
average_weight = 500
stable_population = 3
/obj/item/fish/guppy
name = "guppy"
desc = "Guppy is also known as rainbow fish because of the brightly colored body and fins."
icon_state = "guppy"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#91AE64"
sprite_width = 8
sprite_height = 5
average_size = 30
average_weight = 500
stable_population = 6
/obj/item/fish/plasmatetra
name = "plasma tetra"
desc = "Due to their small size, tetras are prey to many predators in their watery world, including eels, crustaceans, and invertebrates."
icon_state = "plastetra"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#D30EB0"
average_size = 30
average_weight = 500
stable_population = 3
/obj/item/fish/catfish
name = "cory catfish"
desc = "A catfish has about 100,000 taste buds, and their bodies are covered with them to help detect chemicals present in the water and also to respond to touch."
icon_state = "catfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#907420"
average_size = 100
average_weight = 2000
stable_population = 3
favorite_bait = list(
list(
"Type" = "Foodtype",
"Value" = JUNKFOOD
)
)
// Saltwater fish below
/obj/item/fish/clownfish
name = "clownfish"
desc = "Clownfish catch prey by swimming onto the reef, attracting larger fish, and luring them back to the anemone. The anemone will sting and eat the larger fish, leaving the remains for the clownfish."
icon_state = "clownfish"
dedicated_in_aquarium_icon_state = "clownfish_small"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 5
average_size = 30
average_weight = 500
stable_population = 4
fishing_traits = list(/datum/fishing_trait/picky_eater)
/obj/item/fish/cardinal
name = "cardinalfish"
desc = "Cardinalfish are often found near sea urchins, where the fish hide when threatened."
icon_state = "cardinalfish"
dedicated_in_aquarium_icon_state = "fish_greyscale"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
average_size = 30
average_weight = 500
stable_population = 4
fishing_traits = list(/datum/fishing_trait/vegan)
/obj/item/fish/greenchromis
name = "green chromis"
desc = "The Chromis can vary in color from blue to green depending on the lighting and distance from the lights."
icon_state = "greenchromis"
dedicated_in_aquarium_icon_state = "fish_greyscale"
aquarium_vc_color = "#00ff00"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
average_size = 30
average_weight = 500
stable_population = 5
fishing_difficulty_modifier = 5 // Bit harder
/obj/item/fish/firefish
name = "firefish goby"
desc = "To communicate in the wild, the firefish uses its dorsal fin to alert others of potential danger."
icon_state = "firefish"
sprite_width = 6
sprite_height = 5
required_fluid_type = AQUARIUM_FLUID_SALTWATER
average_size = 30
average_weight = 500
stable_population = 3
disliked_bait = list(/obj/item/food/bait/worm, /obj/item/food/bait/doughball)
fish_ai_type = FISH_AI_ZIPPY
/obj/item/fish/pufferfish
name = "pufferfish"
desc = "One Pufferfish contains enough toxins in its liver to kill 30 people."
icon_state = "pufferfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
sprite_width = 8
sprite_height = 8
average_size = 60
average_weight = 1000
stable_population = 3
fishing_traits = list(/datum/fishing_trait/heavy)
/obj/item/fish/lanternfish
name = "lanternfish"
desc = "Typically found in areas below 6600 feet below the surface of the ocean, they live in complete darkness."
icon_state = "lanternfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
random_case_rarity = FISH_RARITY_VERY_RARE
source_width = 28
source_height = 21
sprite_width = 8
sprite_height = 8
average_size = 100
average_weight = 1500
stable_population = 3
fishing_traits = list(/datum/fishing_trait/nocturnal)
//Tiziran Fish
/obj/item/fish/dwarf_moonfish
name = "dwarf moonfish"
desc = "Ordinarily in the wild, the Zagoskian moonfish is around the size of a tuna, however through selective breeding a smaller breed suitable for being kept as an aquarium pet has been created."
icon_state = "dwarf_moonfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 2
fillet_type = /obj/item/food/fishmeat/moonfish
average_size = 100
average_weight = 2000
/obj/item/fish/gunner_jellyfish
name = "gunner jellyfish"
desc = "So called due to their resemblance to an artillery shell, the gunner jellyfish is native to Tizira, where it is enjoyed as a delicacy. Produces a mild hallucinogen that is destroyed by cooking."
icon_state = "gunner_jellyfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 4
fillet_type = /obj/item/food/fishmeat/gunner_jellyfish
/obj/item/fish/needlefish
name = "needlefish"
desc = "A tiny, transparent fish which resides in large schools in the oceans of Tizira. A common food for other, larger fish."
icon_state = "needlefish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 12
fillet_type = null
average_size = 30
average_weight = 300
fishing_traits = list(/datum/fishing_trait/carnivore)
/obj/item/fish/armorfish
name = "armorfish"
desc = "A small shellfish native to Tizira's oceans, known for its exceptionally hard shell. Consumed similarly to prawns."
icon_state = "armorfish"
required_fluid_type = AQUARIUM_FLUID_SALTWATER
stable_population = 10
fillet_type = /obj/item/food/fishmeat/armorfish
fish_ai_type = FISH_AI_SLOW
/obj/item/storage/box/fish_debug
name = "box full of fish"
/obj/item/storage/box/fish_debug/PopulateContents()
for(var/fish_type in subtypesof(/obj/item/fish))
new fish_type(src)
/obj/item/fish/donkfish
name = "donk co. company patent donkfish"
desc = "A lab-grown donkfish. Its invention was an accident for the most part, as it was intended to be consumed in donk pockets. Unfortunately, it tastes horrible, so it has now become a pseudo-mascot."
icon_state = "donkfish"
random_case_rarity = FISH_RARITY_VERY_RARE
required_fluid_type = AQUARIUM_FLUID_FRESHWATER
stable_population = 4
fillet_type = /obj/item/food/fishmeat/donkfish
/obj/item/fish/emulsijack
name = "toxic emulsijack"
desc = "Ah, the terrifying emulsijack. Created in a laboratory, this slimey, scaleless fish emits an invisible toxin that emulsifies other fish for it to feed on. Its only real use is for completely ruining a tank."
icon_state = "emulsijack"
random_case_rarity = FISH_RARITY_GOOD_LUCK_FINDING_THIS
required_fluid_type = AQUARIUM_FLUID_ANADROMOUS
stable_population = 3
/obj/item/fish/emulsijack/process(delta_time)
var/emulsified = FALSE
var/obj/structure/aquarium/aquarium = loc
if(istype(aquarium))
for(var/obj/item/fish/victim in aquarium)
if(istype(victim, /obj/item/fish/emulsijack))
continue //no team killing
victim.adjust_health((victim.health - 3) * delta_time) //the victim may heal a bit but this will quickly kill
emulsified = TRUE
if(emulsified)
adjust_health((health + 3) * delta_time)
last_feeding = world.time //emulsijack feeds on the emulsion!
..()
/obj/item/fish/ratfish
name = "ratfish"
desc = "A rat exposed to the murky waters of maintenance too long. Any higher power, if it revealed itself, would state that the ratfish's continued existence is extremely unwelcome."
icon_state = "ratfish"
random_case_rarity = FISH_RARITY_RARE
required_fluid_type = AQUARIUM_FLUID_FRESHWATER
stable_population = 10 //set by New, but this is the default config value
fillet_type = /obj/item/food/meat/slab/human/mutant/zombie //eww...
fish_ai_type = FISH_AI_ZIPPY
favorite_bait = list(
list(
"Type" = "Foodtype",
"Value" = DAIRY
)
)
/obj/item/fish/ratfish/Initialize(mapload)
. = ..()
//stable pop reflects the config for how many mice migrate. powerful...
stable_population = CONFIG_GET(number/mice_roundstart)
+109
View File
@@ -0,0 +1,109 @@
///Book detailing where to get the fish and their properties.
/obj/item/book/fish_catalog
name = "Fish Encyclopedia"
desc = "Indexes all fish known to mankind (and related species)."
icon_state = "fishbook"
starting_content = "Lot of fish stuff" //book wrappers could use cleaning so this is not necessary
/obj/item/book/fish_catalog/on_read(mob/user)
ui_interact(user)
/obj/item/book/fish_catalog/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishCatalog", name)
ui.open()
/obj/item/book/fish_catalog/ui_static_data(mob/user)
. = ..()
var/static/fish_info
if(!fish_info)
fish_info = list()
for(var/_fish_type as anything in subtypesof(/obj/item/fish))
var/obj/item/fish/fish = _fish_type
var/list/fish_data = list()
if(!initial(fish.show_in_catalog))
continue
fish_data["name"] = initial(fish.name)
fish_data["desc"] = initial(fish.desc)
fish_data["fluid"] = initial(fish.required_fluid_type)
fish_data["temp_min"] = initial(fish.required_temperature_min)
fish_data["temp_max"] = initial(fish.required_temperature_max)
fish_data["icon"] = sanitize_css_class_name("[initial(fish.icon)][initial(fish.icon_state)]")
fish_data["color"] = initial(fish.color)
fish_data["source"] = initial(fish.available_in_random_cases) ? "[AQUARIUM_COMPANY] Fish Packs" : "Unknown"
fish_data["size"] = initial(fish.average_size)
fish_data["weight"] = initial(fish.average_weight)
var/datum/reagent/food_type = initial(fish.food)
if(food_type != /datum/reagent/consumable/nutriment)
fish_data["feed"] = initial(food_type.name)
else
fish_data["feed"] = "[AQUARIUM_COMPANY] Fish Feed"
fish_data["fishing_tips"] = build_fishing_tips(fish)
fish_info += list(fish_data)
// TODO: Custom entries for unusual stuff
.["fish_info"] = fish_info
.["sponsored_by"] = AQUARIUM_COMPANY
/obj/item/book/proc/bait_description(bait)
if(ispath(bait))
var/obj/bait_item = bait
return initial(bait_item.name)
if(islist(bait))
var/list/special_identifier = bait
switch(special_identifier["Type"])
if("Foodtype")
return jointext(bitfield_to_list(special_identifier["Value"], FOOD_FLAGS_IC),",")
else
stack_trace("Unknown bait identifier in fish favourite/disliked list")
return "SOMETHING VERY WEIRD"
else
//Here we handle descriptions of traits fish use as qualifiers
return "something special"
/obj/item/book/fish_catalog/proc/build_fishing_tips(fish_type)
var/obj/item/fish/fishy = fish_type
. = list()
//// Where can it be found - iterate fish sources, how should this handle key
var/list/spot_descriptions = list()
for(var/datum/fish_source/fishing_spot_type as anything in subtypesof(/datum/fish_source))
var/datum/fish_source/temp = new fishing_spot_type
if((fish_type in temp.fish_table) && temp.catalog_description)
spot_descriptions += temp.catalog_description
.["spots"] = english_list(spot_descriptions, nothing_text = "Unknown")
///Difficulty descriptor
switch(initial(fishy.fishing_difficulty_modifier))
if(-INFINITY to 10)
.["difficulty"] = "Easy"
if(20 to 30)
.["difficulty"] = "Medium"
else
.["difficulty"] = "Hard"
var/list/fish_list_properties = collect_fish_properties()
var/list/fav_bait = fish_list_properties[fishy][NAMEOF(fishy, favorite_bait)]
var/list/disliked_bait = fish_list_properties[fishy][NAMEOF(fishy, disliked_bait)]
var/list/bait_list = list()
// Favourite/Disliked bait
for(var/bait_type_or_trait in fav_bait)
bait_list += bait_description(bait_type_or_trait)
.["favorite_bait"] = english_list(bait_list, nothing_text = "None")
bait_list.Cut()
for(var/bait_type_or_trait in disliked_bait)
bait_list += bait_description(bait_type_or_trait)
.["disliked_bait"] = english_list(bait_list, nothing_text = "None")
// Fish traits description
var/list/trait_descriptions = list()
var/list/fish_traits = fish_list_properties[fishy][NAMEOF(fishy, fishing_traits)]
for(var/fish_trait in fish_traits)
var/datum/fishing_trait/trait = fish_trait
trait_descriptions += initial(trait.catalog_description)
if(!length(trait_descriptions))
trait_descriptions += "This fish exhibits no special behavior."
.["traits"] = trait_descriptions
return .
/obj/item/book/fish_catalog/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/spritesheet/fish)
)
+102
View File
@@ -0,0 +1,102 @@
// Reels
/obj/item/fishing_line
name = "fishing line reel"
desc = "simple fishing line"
icon = 'icons/obj/fishing.dmi'
icon_state = "reel_blue"
var/fishing_line_traits = NONE
/// Color of the fishing line
var/line_color = "#808080"
/obj/item/fishing_line/reinforced
name = "reinforced fishing line reel"
desc = "essential for fishing in extreme environments"
icon_state = "reel_green"
fishing_line_traits = FISHING_LINE_REINFORCED
line_color = "#2b9c2b"
/obj/item/fishing_line/cloaked
name = "cloaked fishing line reel"
desc = "even harder to notice than the common variety"
icon_state = "reel_white"
fishing_line_traits = FISHING_LINE_CLOAKED
line_color = "#82cfdd"
/obj/item/fishing_line/bouncy
name = "flexible fishing line reel"
desc = "this specialized line is much harder to snap"
icon_state = "reel_red"
fishing_line_traits = FISHING_LINE_BOUNCY
line_color = "#99313f"
// Hooks
/obj/item/fishing_hook
name = "simple fishing hook"
desc = "a simple fishing hook."
icon = 'icons/obj/fishing.dmi'
icon_state = "hook"
w_class = WEIGHT_CLASS_TINY
var/fishing_hook_traits = NONE
// icon state added to main rod icon when this hook is equipped
var/rod_overlay_icon_state = "hook_overlay"
/obj/item/fishing_hook/magnet
name = "magnetic hook"
desc = "won't make catching fish any easier but might help with looking for other things"
icon_state = "treasure"
fishing_hook_traits = FISHING_HOOK_MAGNETIC
rod_overlay_icon_state = "hook_treasure_overlay"
/obj/item/fishing_hook/shiny
name = "shiny lure hook"
icon_state = "gold_shiny"
fishing_hook_traits = FISHING_HOOK_SHINY
rod_overlay_icon_state = "hook_shiny_overlay"
/obj/item/fishing_hook/weighted
name = "weighted hook"
icon_state = "weighted"
fishing_hook_traits = FISHING_HOOK_WEIGHTED
rod_overlay_icon_state = "hook_weighted_overlay"
/obj/item/storage/toolbox/fishing
name = "fishing toolbox"
desc = "contains everything you need for your fishing trip"
icon_state = "fishing"
inhand_icon_state = "artistic_toolbox"
material_flags = NONE
/obj/item/storage/toolbox/ComponentInitialize()
. = ..()
// Can hold fishing rod despite the size
var/static/list/exception_cache = typecacheof(/obj/item/fishing_rod)
var/datum/component/storage/STR = GetComponent(/datum/component/storage)
STR.exception_hold = exception_cache
/obj/item/storage/toolbox/fishing/PopulateContents()
new /obj/item/bait_can/worm(src)
new /obj/item/fishing_rod(src)
new /obj/item/fishing_hook(src)
new /obj/item/fishing_line(src)
/obj/item/storage/box/fishing_hooks
name = "fishing hook set"
/obj/item/storage/box/fishing_hooks/PopulateContents()
. = ..()
new /obj/item/fishing_hook/magnet(src)
new /obj/item/fishing_hook/shiny(src)
new /obj/item/fishing_hook/weighted(src)
/obj/item/storage/box/fishing_lines
name = "fishing line set"
/obj/item/storage/box/fishing_lines/PopulateContents()
. = ..()
new /obj/item/fishing_line/bouncy(src)
new /obj/item/fishing_line/reinforced(src)
new /obj/item/fishing_line/cloaked(src)
+215
View File
@@ -0,0 +1,215 @@
// Lure bobbing
#define WAIT_PHASE 1
// Click now to start tgui part
#define BITING_PHASE 2
// UI minigame phase
#define MINIGAME_PHASE 3
// Shortest time the minigame can be won
#define MINIMUM_MINIGAME_DURATION 140
/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
/// Fish AI type to use
var/fish_ai = FISH_AI_DUMB
/// Rule modifiers (eg weighted bait)
var/list/special_effects = list()
/// Did the game get past the baiting phase, used to track if bait should be consumed afterwards
var/bait_taken = FALSE
/// 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
/// Fishing mob
var/mob/user
/// Rod that is used for the challenge
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"
/// Max distance we can move from the spot
var/max_distance = 5
/// Fishing line visual
var/datum/beam/fishing_line
/datum/fishing_challenge/New(atom/spot, reward_path, obj/item/fishing_rod/rod, mob/user)
src.user = user
src.reward_path = reward_path
src.used_rod = rod
lure = new(get_turf(spot))
/// Fish minigame properties
if(ispath(reward_path,/obj/item/fish))
var/obj/item/fish/fish = reward_path
fish_ai = initial(fish.fish_ai_type)
// Apply fishing trait modifiers
var/list/fish_list_properties = collect_fish_properties()
var/list/fish_traits = fish_list_properties[fish][NAMEOF(fish, fishing_traits)]
for(var/fish_trait in fish_traits)
var/datum/fishing_trait/trait = new fish_trait
special_effects += trait.minigame_mod(rod, user)
/// Enable special parameters
if(rod.line)
if(rod.line.fishing_line_traits & FISHING_LINE_BOUNCY)
special_effects += FISHING_MINIGAME_RULE_LIMIT_LOSS
if(rod.hook)
if(rod.hook.fishing_hook_traits & FISHING_HOOK_WEIGHTED)
special_effects += FISHING_MINIGAME_RULE_WEIGHTED_BAIT
/datum/fishing_challenge/Destroy(force, ...)
if(!completed)
complete(win = FALSE)
if(fishing_line)
QDEL_NULL(fishing_line)
if(lure)
QDEL_NULL(lure)
. = ..()
/datum/fishing_challenge/proc/start(mob/user)
/// Create fishing line visuals
fishing_line = used_rod.create_fishing_line(lure, target_py = 5)
// If fishing line breaks los / rod gets dropped / deleted
RegisterSignal(fishing_line, COMSIG_FISHING_LINE_SNAPPED, .proc/interrupt)
ADD_TRAIT(user, TRAIT_GONE_FISHING, REF(src))
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "fishing", /datum/mood_event/fishing)
RegisterSignal(user, COMSIG_MOB_CLICKON, .proc/handle_click)
start_baiting_phase()
to_chat(user, span_notice("You start fishing..."))
playsound(lure, 'sound/effects/splash.ogg', 100)
/datum/fishing_challenge/proc/handle_click()
if(phase == WAIT_PHASE) //Reset wait
lure.balloon_alert(user, "miss!")
start_baiting_phase()
else if(phase == BITING_PHASE)
start_minigame_phase()
return COMSIG_MOB_CANCEL_CLICKON
/datum/fishing_challenge/proc/check_distance()
SIGNAL_HANDLER
if(get_dist(user,lure) > max_distance)
interrupt()
/// Challenge interrupted by something external
/datum/fishing_challenge/proc/interrupt()
SIGNAL_HANDLER
if(!completed)
complete(FALSE)
/datum/fishing_challenge/proc/complete(win = FALSE, perfect_win = FALSE)
deltimer(next_phase_timer)
completed = TRUE
if(user)
UnregisterSignal(user, list(COMSIG_MOB_CLICKON, COMSIG_MOVABLE_MOVED))
REMOVE_TRAIT(user, TRAIT_GONE_FISHING, REF(src))
if(used_rod)
UnregisterSignal(used_rod, COMSIG_ITEM_DROPPED)
if(phase == MINIGAME_PHASE)
used_rod.consume_bait()
if(win)
// validate timings to have at least basic abuse prevention, though it's kinda impossible task here
// 140 from minimum completion bar fill time
var/minimum_time = start_time + MINIMUM_MINIGAME_DURATION
if(world.time < minimum_time)
win = FALSE
stack_trace("Fishing minimum time check failed")
if(win)
if(reward_path != FISHING_DUD)
playsound(lure, 'sound/effects/bigsplash.ogg', 100)
else
user.balloon_alert(user, "it got away")
SEND_SIGNAL(src, COMSIG_FISHING_CHALLENGE_COMPLETED, user, win, perfect_win)
qdel(src)
/datum/fishing_challenge/proc/start_baiting_phase()
deltimer(next_phase_timer)
phase = WAIT_PHASE
//Bobbing animation
animate(lure, pixel_y = 1, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_y = -1, time = 1 SECONDS, flags = ANIMATION_RELATIVE)
//Setup next phase
var/wait_time = rand(1 SECONDS, 30 SECONDS)
next_phase_timer = addtimer(CALLBACK(src, .proc/start_biting_phase), wait_time, TIMER_STOPPABLE)
/datum/fishing_challenge/proc/start_biting_phase()
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
lure.balloon_alert(user, "!!!")
animate(lure, pixel_y = 3, time = 5, loop = -1, flags = ANIMATION_RELATIVE)
animate(pixel_y = -3, time = 5, flags = ANIMATION_RELATIVE)
// Setup next phase
var/wait_time = rand(3 SECONDS, 6 SECONDS)
next_phase_timer = addtimer(CALLBACK(src, .proc/start_baiting_phase), wait_time, TIMER_STOPPABLE)
/datum/fishing_challenge/proc/start_minigame_phase()
phase = MINIGAME_PHASE
deltimer(next_phase_timer)
start_time = world.time
ui_interact(user)
/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()
/datum/fishing_challenge/ui_host(mob/user)
return lure //Could be the target really
// Manually closing the ui is treated as lose
/datum/fishing_challenge/ui_close(mob/user)
. = ..()
if(!completed)
complete(FALSE)
/datum/fishing_challenge/ui_static_data(mob/user)
. = ..()
.["difficulty"] = max(1,min(difficulty,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) > max_distance ? 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(.)
return
if(phase != MINIGAME_PHASE)
return
switch(action)
if("win")
complete(win = TRUE, perfect_win = params["perfect"])
if("lose")
complete(win = FALSE)
/// The visual that appears over the fishing spot
/obj/effect/fishing_lure
icon = 'icons/obj/fishing.dmi'
icon_state = "lure_idle"
#undef WAIT_PHASE
#undef BITING_PHASE
#undef MINIGAME_PHASE
#undef MINIMUM_MINIGAME_DURATION
@@ -0,0 +1,48 @@
/obj/machinery/fishing_portal_generator
name = "fish-porter 3000"
desc = "fishing anywhere, anytime, anyway what was i talking about"
icon = 'icons/obj/fishing.dmi'
icon_state = "portal_off"
idle_power_usage = 0
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
anchored = FALSE
density = TRUE
var/fishing_source = /datum/fish_source/portal
var/datum/component/fishing_spot/active
/obj/machinery/fishing_portal_generator/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/machinery/fishing_portal_generator/interact(mob/user, special_state)
. = ..()
if(active)
deactivate()
else
activate()
/obj/machinery/fishing_portal_generator/update_icon(updates)
. = ..()
if(active)
icon_state = "portal_on"
else
icon_state = "portal_off"
/obj/machinery/fishing_portal_generator/proc/activate()
active = AddComponent(/datum/component/fishing_spot, fishing_source)
use_power = ACTIVE_POWER_USE
update_icon()
/obj/machinery/fishing_portal_generator/proc/deactivate()
QDEL_NULL(active)
use_power = IDLE_POWER_USE
update_icon()
/obj/machinery/fishing_portal_generator/on_set_is_operational(old_value)
if(old_value)
deactivate()
+484
View File
@@ -0,0 +1,484 @@
#define ROD_SLOT_BAIT "bait"
#define ROD_SLOT_LINE "line"
#define ROD_SLOT_HOOK "hook"
/obj/item/fishing_rod
name = "fishing rod"
desc = "You can fish with this."
icon = 'icons/obj/fishing.dmi'
icon_state = "fishing_rod"
lefthand_file = 'icons/mob/inhands/equipment/fishing_rod_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/fishing_rod_righthand.dmi'
inhand_icon_state = "rod"
inhand_x_dimension = 64
inhand_y_dimension = 64
force = 8
w_class = WEIGHT_CLASS_HUGE
/// How far can you cast this
var/cast_range = 5
/// Fishing minigame difficulty modifier (additive)
var/difficulty_modifier = 0
/// Explaination of rod functionality shown in the ui
var/ui_description = "A classic fishing rod, with no special qualities."
var/obj/item/bait
var/obj/item/fishing_line/line
var/obj/item/fishing_hook/hook
/// Currently hooked item for item reeling
var/obj/item/currently_hooked_item
/// Fishing line visual for the hooked item
var/datum/beam/hooked_item_fishing_line
/// Are we currently casting
var/casting = FALSE
/// List of fishing line beams
var/list/fishing_lines = list()
var/default_line_color = "gray"
/obj/item/fishing_rod/Initialize(mapload)
. = ..()
register_context()
register_item_context()
/obj/item/fishing_rod/add_context(atom/source, list/context, obj/item/held_item, mob/user)
if(src == held_item)
if(currently_hooked_item)
context[SCREENTIP_CONTEXT_LMB] = "Reel in"
context[SCREENTIP_CONTEXT_RMB] = "Modify"
return CONTEXTUAL_SCREENTIP_SET
return NONE
/obj/item/fishing_rod/add_item_context(obj/item/source, list/context, atom/target, mob/living/user)
. = ..()
if(currently_hooked_item)
context[SCREENTIP_CONTEXT_LMB] = "Reel in"
return CONTEXTUAL_SCREENTIP_SET
return NONE
/obj/item/fishing_rod/Destroy(force)
. = ..()
//Remove any leftover fishing lines
QDEL_LIST(fishing_lines)
/// Catch weight modifier for the given fish_type (or FISHING_DUD), additive
/obj/item/fishing_rod/proc/fish_bonus(fish_type)
return 0
/obj/item/fishing_rod/proc/consume_bait()
if(bait)
QDEL_NULL(bait)
update_icon()
/obj/item/fishing_rod/interact(mob/user)
if(currently_hooked_item)
reel(user)
/obj/item/fishing_rod/proc/reel(mob/user)
//Could use sound here for feedback
if(do_after(user, 1 SECONDS, currently_hooked_item))
// Should probably respect and used force move later
step_towards(currently_hooked_item, get_turf(src))
if(get_dist(currently_hooked_item,get_turf(src)) < 1)
clear_hooked_item()
/obj/item/fishing_rod/attack_self_secondary(mob/user, modifiers)
. = ..()
ui_interact(user)
/obj/item/fishing_rod/pre_attack(atom/targeted_atom, mob/living/user, params)
. = ..()
/// Reel in if able
if(currently_hooked_item)
reel(user)
return TRUE
SEND_SIGNAL(targeted_atom, COMSIG_PRE_FISHING)
/// Generates the fishing line visual from the current user to the target and updates inhands
/obj/item/fishing_rod/proc/create_fishing_line(atom/movable/target, target_py = null)
var/mob/user = loc
if(!istype(user))
return
var/beam_color = line?.line_color || default_line_color
var/datum/beam/fishing_line/fishing_line_beam = new(user, target, icon_state = "fishing_line", beam_color = beam_color, override_target_pixel_y = target_py)
fishing_line_beam.lefthand = user.get_held_index_of_item(src) % 2 == 1
RegisterSignal(fishing_line_beam, COMSIG_BEAM_BEFORE_DRAW, .proc/check_los)
RegisterSignal(fishing_line_beam, COMSIG_PARENT_QDELETING, .proc/clear_line)
fishing_lines += fishing_line_beam
INVOKE_ASYNC(fishing_line_beam, /datum/beam/.proc/Start)
user.update_inv_hands()
return fishing_line_beam
/obj/item/fishing_rod/proc/clear_line(datum/source)
SIGNAL_HANDLER
fishing_lines -= source
if(ismob(loc))
var/mob/user = loc
user.update_inv_hands()
/obj/item/fishing_rod/dropped(mob/user, silent)
. = ..()
if(currently_hooked_item)
clear_hooked_item()
for(var/datum/beam/fishing_line in fishing_lines)
SEND_SIGNAL(fishing_line, COMSIG_FISHING_LINE_SNAPPED)
QDEL_LIST(fishing_lines)
/// Hooks the item
/obj/item/fishing_rod/proc/hook_item(mob/user, atom/target_atom)
if(currently_hooked_item)
return
if(!can_be_hooked(target_atom))
return
currently_hooked_item = target_atom
hooked_item_fishing_line = create_fishing_line(target_atom)
RegisterSignal(hooked_item_fishing_line, COMSIG_FISHING_LINE_SNAPPED, .proc/clear_hooked_item)
/// Checks what can be hooked
/obj/item/fishing_rod/proc/can_be_hooked(atom/movable/target)
// Could be made dependent on actual hook, ie magnet to hook metallic items
return istype(target, /obj/item)
/obj/item/fishing_rod/proc/clear_hooked_item()
SIGNAL_HANDLER
if(!QDELETED(hooked_item_fishing_line))
QDEL_NULL(hooked_item_fishing_line)
currently_hooked_item = null
// Checks fishing line for interruptions and range
/obj/item/fishing_rod/proc/check_los(datum/beam/source)
SIGNAL_HANDLER
. = NONE
if(!CheckToolReach(src, source.target, cast_range))
SEND_SIGNAL(source, COMSIG_FISHING_LINE_SNAPPED) //Stepped out of range or los interrupted
return BEAM_CANCEL_DRAW
/obj/item/fishing_rod/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
/// Reel in if able
if(currently_hooked_item)
reel(user)
return
/// If the line to whatever that is is clear and we're not already busy, try fishing in it
if(!casting && !currently_hooked_item && !proximity_flag && CheckToolReach(user, target, cast_range))
/// Annoyingly pre attack is only called in melee
SEND_SIGNAL(target, COMSIG_PRE_FISHING)
casting = TRUE
var/obj/projectile/fishing_cast/cast_projectile = new(get_turf(src))
cast_projectile.range = cast_range
cast_projectile.owner = src
cast_projectile.original = target
cast_projectile.fired_from = src
cast_projectile.firer = user
cast_projectile.impacted = list(user = TRUE)
cast_projectile.preparePixelProjectile(target, user)
cast_projectile.fire()
/// Called by hook projectile when hitting things
/obj/item/fishing_rod/proc/hook_hit(atom/atom_hit_by_hook_projectile)
var/mob/user = loc
if(!istype(user))
return
if(SEND_SIGNAL(atom_hit_by_hook_projectile, COMSIG_FISHING_ROD_CAST, src, user) & FISHING_ROD_CAST_HANDLED)
return
/// If you can't fish in it, try hooking it
hook_item(user, atom_hit_by_hook_projectile)
/obj/item/fishing_rod/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FishingRod", name)
ui.set_autoupdate(FALSE)
ui.open()
/obj/item/fishing_rod/update_overlays()
. = ..()
var/line_color = line?.line_color || default_line_color
/// Line part by the rod, always visible
var/mutable_appearance/reel_overlay = mutable_appearance(icon, "reel_overlay")
reel_overlay.color = line_color;
. += reel_overlay
// Line & hook is also visible when only bait is equipped but it uses default appearances then
if(hook || bait)
var/mutable_appearance/line_overlay = mutable_appearance(icon, "line_overlay")
line_overlay.color = line_color;
. += line_overlay
var/mutable_appearance/hook_overlay = mutable_appearance(icon, hook?.rod_overlay_icon_state || "hook_overlay")
. += hook_overlay
if(bait)
var/bait_state = "worm_overlay" //default to worm overlay for anything without specific one
if(istype(bait, /obj/item/food/bait))
var/obj/item/food/bait/real_bait = bait
bait_state = real_bait.rod_overlay_icon_state
. += bait_state
/obj/item/fishing_rod/worn_overlays(mutable_appearance/standing, isinhands, icon_file)
. = ..()
var/line_color = line?.line_color || default_line_color
var/mutable_appearance/reel_overlay = mutable_appearance(icon_file, "reel_overlay")
reel_overlay.appearance_flags |= RESET_COLOR
reel_overlay.color = line_color
. += reel_overlay
/// if we don't have anything hooked show the dangling hook & line
if(isinhands && length(fishing_lines) == 0)
var/mutable_appearance/line_overlay = mutable_appearance(icon_file, "line_overlay")
line_overlay.appearance_flags |= RESET_COLOR
line_overlay.color = line_color
. += line_overlay
. += mutable_appearance(icon_file, "hook_overlay")
/obj/item/fishing_rod/attackby(obj/item/attacking_item, mob/user, params)
if(slot_check(attacking_item,ROD_SLOT_LINE))
use_slot(ROD_SLOT_LINE, user, attacking_item)
SStgui.update_uis(src)
return TRUE
else if(slot_check(attacking_item,ROD_SLOT_HOOK))
use_slot(ROD_SLOT_HOOK, user, attacking_item)
SStgui.update_uis(src)
return TRUE
else if(slot_check(attacking_item,ROD_SLOT_BAIT))
use_slot(ROD_SLOT_BAIT, user, attacking_item)
SStgui.update_uis(src)
return TRUE
else if(istype(attacking_item, /obj/item/bait_can)) //Quicker filling from bait can
var/obj/item/bait_can/can = attacking_item
var/bait = can.retrieve_bait(user)
if(bait)
use_slot(ROD_SLOT_BAIT, user, bait)
SStgui.update_uis(src)
return TRUE
. = ..()
/obj/item/fishing_rod/ui_data(mob/user)
. = ..()
var/list/data = list()
data["bait_name"] = format_text(bait?.name)
data["bait_icon"] = bait != null ? icon2base64(icon(bait.icon, bait.icon_state)) : null
data["line_name"] = format_text(line?.name)
data["line_icon"] = line != null ? icon2base64(icon(line.icon, line.icon_state)) : null
data["hook_name"] = format_text(hook?.name)
data["hook_icon"] = hook != null ? icon2base64(icon(hook.icon, hook.icon_state)) : null
data["description"] = ui_description
return data
/// Checks if the item fits the slot
/obj/item/fishing_rod/proc/slot_check(obj/item/item,slot)
if(!istype(item))
return FALSE
switch(slot)
if(ROD_SLOT_HOOK)
if(!istype(item,/obj/item/fishing_hook))
return FALSE
if(ROD_SLOT_LINE)
if(!istype(item,/obj/item/fishing_line))
return FALSE
if(ROD_SLOT_BAIT)
if(!HAS_TRAIT(item, FISHING_BAIT_TRAIT))
return FALSE
return TRUE
/obj/item/fishing_rod/ui_act(action, list/params)
. = ..()
if(.)
return .
var/mob/user = usr
switch(action)
if("slot_action")
// Simple click with empty hand to remove, click with item to insert/switch
var/obj/item/held_item = user.get_active_held_item()
if(held_item == src)
return
use_slot(params["slot"], user, held_item)
return TRUE
/// Ideally this will be replaced with generic slotted storage datum + display
/obj/item/fishing_rod/proc/use_slot(slot, mob/user, obj/item/new_item)
var/obj/item/current_item
switch(slot)
if(ROD_SLOT_BAIT)
current_item = bait
if(ROD_SLOT_HOOK)
current_item = hook
if(ROD_SLOT_LINE)
current_item = line
if(!new_item && !current_item)
return
// Trying to remove the item
if(!new_item && current_item)
user.put_in_hands(current_item)
update_icon()
return
// Trying to insert item into empty slot
if(new_item && !current_item)
if(!slot_check(new_item, slot))
return
if(user.transferItemToLoc(new_item,src))
switch(slot)
if(ROD_SLOT_BAIT)
bait = new_item
if(ROD_SLOT_HOOK)
hook = new_item
if(ROD_SLOT_LINE)
line = new_item
update_icon()
/// Trying to swap item
if(new_item && current_item)
if(!slot_check(new_item,slot))
return
if(user.transferItemToLoc(new_item,src))
switch(slot)
if(ROD_SLOT_BAIT)
bait = new_item
if(ROD_SLOT_HOOK)
hook = new_item
if(ROD_SLOT_LINE)
line = new_item
user.put_in_hands(current_item)
update_icon()
/obj/item/fishing_rod/Exited(atom/movable/gone, direction)
. = ..()
if(gone == bait)
bait = null
if(gone == line)
line = null
if(gone == hook)
hook = null
/obj/item/fishing_rod/master
name = "master fishing rod"
desc = "The mythical rod of a lost fisher king. Said to be imbued with un-paralleled fishing power. There's writing on the back of the pole. \"中国航天制造\""
difficulty_modifier = -10
ui_description = "This rods makes fishing easy even for an absolute beginner."
icon_state = "fishing_rod_master"
/obj/item/fishing_rod/tech
name = "advanced fishing rod"
desc = "An embedded universal constructor along with micro-fusion generator makes this marvel of technology never run out of bait. Interstellar treaties prevent using it outside of recreational fishing. And you can fish with this. "
ui_description = "This rod has an infinite supply of synthetic bait."
icon_state = "fishing_rod_science"
/obj/item/fishing_rod/tech/Initialize(mapload)
. = ..()
var/obj/item/food/bait/doughball/synthetic/infinite_supply_of_bait = new(src)
bait = infinite_supply_of_bait
update_icon()
/obj/item/fishing_rod/tech/consume_bait()
return
/obj/item/fishing_rod/tech/use_slot(slot, mob/user, obj/item/new_item)
if(slot == ROD_SLOT_BAIT)
return
return ..()
#undef ROD_SLOT_BAIT
#undef ROD_SLOT_LINE
#undef ROD_SLOT_HOOK
/obj/projectile/fishing_cast
name = "fishing hook"
icon = 'icons/obj/fishing.dmi'
icon_state = "hook_projectile"
damage = 0
nodamage = TRUE
range = 5
suppressed = SUPPRESSED_VERY
can_hit_turfs = TRUE
var/obj/item/fishing_rod/owner
var/datum/beam/our_line
/obj/projectile/fishing_cast/Impact(atom/hit_atom)
. = ..()
owner.hook_hit(hit_atom)
qdel(src)
/obj/projectile/fishing_cast/fire(angle, atom/direct_target)
. = ..()
our_line = owner.create_fishing_line(src)
/obj/projectile/fishing_cast/Destroy()
. = ..()
QDEL_NULL(our_line)
owner?.casting = FALSE
/datum/beam/fishing_line
// Is the fishing rod held in left side hand
var/lefthand = FALSE
/datum/beam/fishing_line/Start()
update_offsets(origin.dir)
. = ..()
RegisterSignal(origin, COMSIG_ATOM_DIR_CHANGE, .proc/handle_dir_change)
/datum/beam/fishing_line/Destroy()
UnregisterSignal(origin, COMSIG_ATOM_DIR_CHANGE)
. = ..()
/datum/beam/fishing_line/proc/handle_dir_change(atom/movable/source, olddir, newdir)
SIGNAL_HANDLER
update_offsets(newdir)
INVOKE_ASYNC(src, /datum/beam/.proc/redrawing)
/datum/beam/fishing_line/proc/update_offsets(user_dir)
switch(user_dir)
if(SOUTH)
override_origin_pixel_x = lefthand ? lefthand_s_px : righthand_s_px
override_origin_pixel_y = lefthand ? lefthand_s_py : righthand_s_py
if(EAST)
override_origin_pixel_x = lefthand ? lefthand_e_px : righthand_e_px
override_origin_pixel_y = lefthand ? lefthand_e_py : righthand_e_py
if(WEST)
override_origin_pixel_x = lefthand ? lefthand_w_px : righthand_w_px
override_origin_pixel_y = lefthand ? lefthand_w_py : righthand_w_py
if(NORTH)
override_origin_pixel_x = lefthand ? lefthand_n_px : righthand_n_px
override_origin_pixel_y = lefthand ? lefthand_n_py : righthand_n_py
// Make these inline with final sprites
/datum/beam/fishing_line
var/righthand_s_px = 13
var/righthand_s_py = 16
var/righthand_e_px = 18
var/righthand_e_py = 16
var/righthand_w_px = -20
var/righthand_w_py = 18
var/righthand_n_px = -14
var/righthand_n_py = 16
var/lefthand_s_px = -13
var/lefthand_s_py = 15
var/lefthand_e_px = 24
var/lefthand_e_py = 18
var/lefthand_w_px = -17
var/lefthand_w_py = 16
var/lefthand_n_px = 13
var/lefthand_n_py = 15
+82
View File
@@ -0,0 +1,82 @@
/datum/fishing_trait
/// Description of the trait in the fishing catalog
var/catalog_description
/// Difficulty modifier from this mod, needs to return a list with two values
/datum/fishing_trait/proc/difficulty_mod(obj/item/fishing_rod/rod, mob/fisherman)
SHOULD_CALL_PARENT(TRUE) //Technically it doesn't but this makes it saner without custom unit test
return list(ADDITIVE_FISHING_MOD = 0, MULTIPLICATIVE_FISHING_MOD = 1)
/// Catch weight table modifier from this mod, needs to return a list with two values
/datum/fishing_trait/proc/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman)
SHOULD_CALL_PARENT(TRUE)
return list(ADDITIVE_FISHING_MOD = 0, MULTIPLICATIVE_FISHING_MOD = 1)
/// Returns special minigame rules applied by this trait
/datum/fishing_trait/proc/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman)
return list()
/datum/fishing_trait/wary
catalog_description = "This fish will avoid visible fish lines, cloaked line recommended."
/datum/fishing_trait/wary/difficulty_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
// Wary fish require transparent line or they're harder
if(!rod.line || !(rod.line.fishing_line_traits & FISHING_LINE_CLOAKED))
.[ADDITIVE_FISHING_MOD] = -FISH_TRAIT_MINOR_DIFFICULTY_BOOST
/datum/fishing_trait/shiny_lover
catalog_description = "This fish loves shiny things, shiny lure recommended."
/datum/fishing_trait/shiny_lover/difficulty_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
// These fish are easier to catch with shiny lure
if(rod.hook && rod.hook.fishing_hook_traits & FISHING_HOOK_SHINY)
.[ADDITIVE_FISHING_MOD] = FISH_TRAIT_MINOR_DIFFICULTY_BOOST
/datum/fishing_trait/picky_eater
catalog_description = "This fish is very picky and will ignore low quality bait."
/datum/fishing_trait/picky_eater/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
if(!rod.bait || !(HAS_TRAIT(rod.bait, GOOD_QUALITY_BAIT_TRAIT) || HAS_TRAIT(rod.bait, GREAT_QUALITY_BAIT_TRAIT)))
.[MULTIPLICATIVE_FISHING_MOD] = 0
/datum/fishing_trait/nocturnal
catalog_description = "This fish avoids bright lights, fishing in darkness recommended."
/datum/fishing_trait/nocturnal/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
var/turf/T = get_turf(fisherman)
var/light_amount = T.get_lumcount()
if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD)
.[MULTIPLICATIVE_FISHING_MOD] = 0
/datum/fishing_trait/heavy
catalog_description = "This fish tends to stay near the waterbed.";
/datum/fishing_trait/heavy/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman)
return list(FISHING_MINIGAME_RULE_HEAVY_FISH)
/datum/fishing_trait/carnivore
catalog_description = "This fish can only be baited with meat."
/datum/fishing_trait/carnivore/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
.[MULTIPLICATIVE_FISHING_MOD] = 0
if(rod.bait && istype(rod.bait, /obj/item/food))
var/obj/item/food/food_bait = rod.bait
if(food_bait.foodtypes & MEAT)
.[MULTIPLICATIVE_FISHING_MOD] = 1
/datum/fishing_trait/vegan
catalog_description = "This fish can only be baited with fresh produce."
/datum/fishing_trait/vegan/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
.[MULTIPLICATIVE_FISHING_MOD] = 0
if(rod.bait && istype(rod.bait, /obj/item/food/grown))
.[MULTIPLICATIVE_FISHING_MOD] = 1
@@ -0,0 +1,198 @@
/// Keyed list of preset sources to configuration instance
GLOBAL_LIST_INIT(preset_fish_sources,init_fishing_configurations())
/// These are shared between their spots
/proc/init_fishing_configurations()
. = list()
var/datum/fish_source/ocean/beach/beach_preset = new
.[FISHING_SPOT_PRESET_BEACH] = beach_preset
var/datum/fish_source/lavaland/lava_preset = new
.[FISHING_SPOT_PRESET_LAVALAND_LAVA] = lava_preset
/// Where the fish actually come from - every fishing spot has one assigned but multiple fishing holes can share single source, ie single shared one for ocean/lavaland river
/datum/fish_source
/// Fish catch weight table - these are relative weights
var/list/fish_table = list()
/// If a key from fish_table is present here, that fish is availible in limited quantity and is reduced by one on successful fishing
var/list/fish_counts = list()
/// Text shown as baloon alert when you roll a dud in the table
var/duds = list("it was nothing", "the hook is empty")
/// Baseline difficulty for fishing in this spot
var/fishing_difficulty = FISHING_DEFAULT_DIFFICULTY
/// 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"
/// Can we fish in this spot at all. Returns DENIAL_REASON or null if we're good to go
/datum/fish_source/proc/can_fish(obj/item/fishing_rod/rod, mob/fisherman)
return
/// DIFFICULTY = (SPOT_BASE_VALUE + FISH_MODIFIER + ROD_MODIFIER + FAV/DISLIKED_BAIT_MODIFIER + TRAITS_ADDITIVE) * TRAITS_MULTIPLICATIVE , For non-fish it's just SPOT_BASE_VALUE
/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman)
. = fishing_difficulty
if(!ispath(result,/obj/item/fish))
// In the future non-fish rewards can have variable difficulty calculated here
return
var/list/fish_list_properties = collect_fish_properties()
var/obj/item/fish/caught_fish = result
// Baseline fish difficulty
. += initial(caught_fish.fishing_difficulty_modifier)
. += rod.difficulty_modifier
if(rod.bait)
var/obj/item/bait = rod.bait
//Fav bait makes it easier
var/list/fav_bait = fish_list_properties[caught_fish][NAMEOF(caught_fish, favorite_bait)]
for(var/bait_identifer in fav_bait)
if(is_matching_bait(bait, bait_identifer))
. += FAV_BAIT_DIFFICULTY_MOD
break
//Disliked bait makes it harder
var/list/disliked_bait = fish_list_properties[caught_fish][NAMEOF(caught_fish, disliked_bait)]
for(var/bait_identifer in disliked_bait)
if(is_matching_bait(bait, bait_identifer))
. += DISLIKED_BAIT_DIFFICULTY_MOD
break
// Matching/not matching fish traits and equipment
var/list/fish_traits = fish_list_properties[caught_fish][NAMEOF(caught_fish, fishing_traits)]
var/additive_mod = 0
var/multiplicative_mod = 1
for(var/fish_trait in fish_traits)
var/datum/fishing_trait/trait = new fish_trait
var/list/mod = trait.difficulty_mod(rod, fisherman)
additive_mod += mod[ADDITIVE_FISHING_MOD]
multiplicative_mod *= mod[MULTIPLICATIVE_FISHING_MOD]
. += additive_mod
. *= multiplicative_mod
/// In case you want more complex rules for specific spots
/datum/fish_source/proc/roll_reward(obj/item/fishing_rod/rod, mob/fisherman)
return pick_weight(get_modified_fish_table(rod,fisherman))
/// Gives out the reward if possible
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman)
if((reward_path in fish_counts)) // This is limited count result
if(fish_counts[reward_path] > 0)
fish_counts[reward_path] -= 1
else
reward_path = FISHING_DUD //Ran out of these since rolling (multiple fishermen on same source most likely)
if(ispath(reward_path))
if(ispath(reward_path,/obj/item))
var/obj/item/reward = new reward_path
if(ispath(reward_path,/obj/item/fish))
var/obj/item/fish/caught_fish = reward
caught_fish.randomize_weight_and_size()
//fish caught signal if needed goes here and/or fishing achievements
//Try to put it in hand
fisherman.put_in_hands(reward)
fisherman.balloon_alert(fisherman, "caught [reward]!")
else //If someone adds fishing out carp/chests/singularities or whatever just plop it down on the fisher's turf
fisherman.balloon_alert(fisherman, "caught something!")
new reward_path(get_turf(fisherman))
else if (reward_path == FISHING_DUD)
//baloon alert instead
fisherman.balloon_alert(fisherman,pick(duds))
/// Cached fish list properties so we don't have to initalize fish every time, init deffered
GLOBAL_LIST(fishing_property_cache)
/// Awful workaround around initial(x.list_variable) not being a thing while trying to keep some semblance of being structured
/proc/collect_fish_properties()
if(GLOB.fishing_property_cache == null)
var/list/fish_property_table = list()
for(var/fish_type in subtypesof(/obj/item/fish))
var/obj/item/fish/fish = new fish_type(null)
fish_property_table[fish_type] = list()
fish_property_table[fish_type][NAMEOF(fish, favorite_bait)] = fish.favorite_bait.Copy()
fish_property_table[fish_type][NAMEOF(fish, disliked_bait)] = fish.disliked_bait.Copy()
fish_property_table[fish_type][NAMEOF(fish, fishing_traits)] = fish.fishing_traits.Copy()
QDEL_NULL(fish)
GLOB.fishing_property_cache = fish_property_table
return GLOB.fishing_property_cache
/// Checks if bait matches identifier from fav/disliked bait list
/datum/fish_source/proc/is_matching_bait(obj/item/bait, identifier)
if(ispath(identifier)) //Just a path
return istype(bait, identifier)
if(islist(identifier))
var/list/special_identifier = identifier
switch(special_identifier["Type"])
if("Foodtype")
var/obj/item/food/food_bait = bait
return istype(food_bait) && food_bait.foodtypes & special_identifier["Value"]
else
CRASH("Unknown bait identifier in fish favourite/disliked list")
else
return HAS_TRAIT(bait, identifier)
/// Builds a fish weights table modified by bait/rod/user properties
/datum/fish_source/proc/get_modified_fish_table(obj/item/fishing_rod/rod, mob/fisherman)
var/obj/item/bait = rod.bait
var/list/fish_list_properties = collect_fish_properties()
var/list/final_table = fish_table.Copy()
for(var/result in final_table)
if((result in fish_counts) && fish_counts[result] <= 0) //ran out of these, ignore
final_table -= result
continue
final_table[result] += rod.fish_bonus(result) //Decide on order here so it can be multiplicative
if(result == FISHING_DUD)
//Modify dud result
//Bait quality reduces dud chance heavily.
if(bait)
if(HAS_TRAIT(bait, GREAT_QUALITY_BAIT_TRAIT))
final_table[result] *= 0.1
else if(HAS_TRAIT(bait, GOOD_QUALITY_BAIT_TRAIT))
final_table[result] *= 0.3
else if(HAS_TRAIT(bait, BASIC_QUALITY_BAIT_TRAIT))
final_table[result] *= 0.5
else
final_table[result] *= 10 //Fishing without bait is not going to be easy
else if(ispath(result, /obj/item/fish))
//Modify fish roll chance
var/obj/item/fish/caught_fish = result
if(bait)
//Bait matching likes doubles the chance
var/list/fav_bait = fish_list_properties[result][NAMEOF(caught_fish, favorite_bait)]
for(var/bait_identifer in fav_bait)
if(is_matching_bait(bait, bait_identifer))
final_table[result] *= 2
break // could compound possibly
//Bait matching dislikes
var/list/disliked_bait = fish_list_properties[result][NAMEOF(caught_fish, disliked_bait)]
for(var/bait_identifer in disliked_bait)
if(is_matching_bait(bait, bait_identifer))
final_table[result] *= 0.5
break // same question as above
// Apply fishing trait modifiers
var/list/fish_traits = fish_list_properties[caught_fish][NAMEOF(caught_fish, fishing_traits)]
var/additive_mod = 0
var/multiplicative_mod = 1
for(var/fish_trait in fish_traits)
var/datum/fishing_trait/trait = new fish_trait
var/list/mod = trait.catch_weight_mod(rod, fisherman)
additive_mod += mod[ADDITIVE_FISHING_MOD]
multiplicative_mod *= mod[MULTIPLICATIVE_FISHING_MOD]
final_table[result] += additive_mod
final_table[result] *= multiplicative_mod
else
//Modify other paths chance
if(rod.hook && rod.hook.fishing_hook_traits & FISHING_HOOK_MAGNETIC)
final_table[result] *= 5
if(final_table[result] <= 0)
final_table -= result
return final_table
@@ -0,0 +1,54 @@
/datum/fish_source/ocean
fish_table = list(
FISHING_DUD = 15,
/obj/item/coin/gold = 5,
/obj/item/fish/clownfish = 15,
/obj/item/fish/pufferfish = 15,
/obj/item/fish/cardinal = 15,
/obj/item/fish/greenchromis = 15,
/obj/item/fish/lanternfish = 5
)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 5
/datum/fish_source/ocean/beach
catalog_description = "Beach shore water"
/datum/fish_source/portal
fish_table = list(
FISHING_DUD = 5,
/obj/item/fish/goldfish = 10,
/obj/item/fish/guppy = 10,
)
catalog_description = "Fish dimension (Fishing portal generator)"
/datum/fish_source/lavaland
catalog_description = "Lava vents"
background = "fishing_background_lavaland"
fish_table = list(
FISHING_DUD = 5,
/obj/item/stack/ore/slag = 20,
/obj/structure/closet/crate/necropolis/tendril = 1,
/obj/effect/mob_spawn/corpse/human/charredskeleton = 1
)
fish_counts = list(
/obj/structure/closet/crate/necropolis/tendril = 1
)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
/datum/fish_source/lavaland/can_fish(obj/item/fishing_rod/rod, mob/fisherman)
. = ..()
var/turf/approx = get_turf(fisherman) //todo pass the parent
if(!SSmapping.level_trait(approx.z, ZTRAIT_MINING))
return "There doesn't seem to be anything to catch here."
if(!rod.line || !(rod.line.fishing_line_traits & FISHING_LINE_REINFORCED))
return "You'll need reinforced fishing line to fish in there"
/datum/fish_source/moisture_trap
catalog_description = "moisture trap basins"
fish_table = list(
FISHING_DUD = 20,
/obj/item/fish/ratfish = 10
)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+6
View File
@@ -32,6 +32,12 @@
var/command = macro_set[key]
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[command]")
//Reactivate any active tgui windows mouse passthroughs macros
for(var/datum/tgui_window/window in tgui_windows)
if(window.mouse_event_macro_set)
window.mouse_event_macro_set = FALSE
window.set_mouse_macro()
if(hotkeys)
winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]")
else
+4 -2
View File
@@ -183,6 +183,8 @@
var/static/list/projectile_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
/// If true directly targeted turfs can be hit
var/can_hit_turfs = FALSE
/obj/projectile/Initialize(mapload)
. = ..()
@@ -494,7 +496,7 @@
/obj/projectile/proc/can_hit_target(atom/target, direct_target = FALSE, ignore_loc = FALSE, cross_failed = FALSE)
if(QDELETED(target) || impacted[target])
return FALSE
if(!ignore_loc && (loc != target.loc))
if(!ignore_loc && (loc != target.loc) && !(can_hit_turfs && direct_target && loc == target))
return FALSE
// if pass_flags match, pass through entirely - unless direct target is set.
if((target.pass_flags_self & pass_flags) && !direct_target)
@@ -511,7 +513,7 @@
return TRUE
if(!isliving(target))
if(isturf(target)) // non dense turfs
return FALSE
return can_hit_turfs && direct_target
if(target.layer < hit_threshhold)
return FALSE
else if(!direct_target) // non dense objects do not get hit unless specifically clicked
@@ -1281,3 +1281,11 @@
materials = list(/datum/material/iron = 100, /datum/material/glass = 500)
build_path = /obj/item/electronics/tracker
category = list("initial", "Electronics", "Construction")
/datum/design/fishing_rod_basic
name = "Fishing Rod"
id = "fishing_rod"
build_type = AUTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron = 200, /datum/material/glass = 200)
build_path = /obj/item/fishing_rod
category = list("initial", "Misc", "Equipment")
@@ -719,3 +719,15 @@
build_path = /obj/item/plate/oven_tray
category = list("initial","Equipment")
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
/////////////////////////////////////////
/////////Fishing Equipment///////////////
/////////////////////////////////////////
/datum/design/fishing_rod_tech
name = "Advanced Fishing Rod"
id = "fishing_rod_tech"
build_type = PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/uranium = 1000, /datum/material/plastic = 2000)
build_path = /obj/item/fishing_rod/tech
category = list("Equipment")
+13 -1
View File
@@ -1202,7 +1202,7 @@
"cybernetic_stomach_tier2",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1000)
/datum/techweb_node/cyber_organs_upgraded
id = "cyber_organs_upgraded"
@@ -2160,6 +2160,18 @@
hidden = TRUE
experimental = TRUE
/datum/techweb_node/fishing
id = "fishing"
display_name = "Fishing Technology"
description = "Cutting edge fishing advancements."
prereq_ids = list("base")
design_ids = list(
"fishing_rod_tech"
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
hidden = TRUE
experimental = TRUE
//Helpers for debugging/balancing the techweb in its entirety!
/proc/total_techweb_points()
var/list/datum/techweb_node/processing = list()
+16
View File
@@ -37,6 +37,8 @@
var/datum/ui_state/state = null
/// Rate limit client refreshes to prevent DoS.
COOLDOWN_DECLARE(refresh_cooldown)
/// Are byond mouse events beyond the window passed in to the ui
var/mouse_hooked = FALSE
/**
* public
@@ -112,6 +114,8 @@
window.send_message("update", get_payload(
with_data = TRUE,
with_static_data = TRUE))
if(mouse_hooked)
window.set_mouse_macro()
SStgui.on_open(src)
return TRUE
@@ -150,6 +154,18 @@
/datum/tgui/proc/set_autoupdate(autoupdate)
src.autoupdate = autoupdate
/**
* public
*
* Enable/disable passing through byond mouse events to the window
*
* required value bool Enable/disable hooking.
*/
/datum/tgui/proc/set_mouse_hook(value)
src.mouse_hooked = value
//Handle unhooking/hooking on already open windows ?
/**
* public
*
+41
View File
@@ -24,6 +24,7 @@
var/initial_inline_html
var/initial_inline_js
var/initial_inline_css
var/mouse_event_macro_set = FALSE
/**
* public
@@ -219,6 +220,8 @@
/datum/tgui_window/proc/close(can_be_suspended = TRUE)
if(!client)
return
if(mouse_event_macro_set)
remove_mouse_macro()
if(can_be_suspended && can_be_suspended())
log_tgui(client,
context = "[id]/close (suspending)",
@@ -372,3 +375,41 @@
/datum/tgui_window/vv_edit_var(var_name, var_value)
return var_name != NAMEOF(src, id) && ..()
/datum/tgui_window/proc/set_mouse_macro()
if(mouse_event_macro_set)
return
var/list/byondToTguiEventMap = list(
"MouseDown" = "byond/mousedown",
"MouseUp" = "byond/mouseup"
)
for(var/mouseMacro in byondToTguiEventMap)
var/command_template = ".output CONTROL PAYLOAD"
var/event_message = TGUI_CREATE_MESSAGE(byondToTguiEventMap[mouseMacro], null)
var target_control = is_browser \
? "[id]:update" \
: "[id].browser:update"
var/with_id = replacetext(command_template, "CONTROL", target_control)
var/full_command = replacetext(with_id, "PAYLOAD", event_message)
var/list/params = list()
params["parent"] = "default" //Technically this is external to tgui but whatever
params["name"] = mouseMacro
params["command"] = full_command
winset(client, "[mouseMacro]Window[id]Macro", params)
mouse_event_macro_set = TRUE
/datum/tgui_window/proc/remove_mouse_macro()
if(!mouse_event_macro_set)
stack_trace("Unsetting mouse macro on tgui window that has none")
var/list/byondToTguiEventMap = list(
"MouseDown" = "byond/mousedown",
"MouseUp" = "byond/mouseup"
)
for(var/mouseMacro in byondToTguiEventMap)
winset(client, null, "[mouseMacro]Window[id]Macro.parent=null")
mouse_event_macro_set = FALSE
Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
+19 -4
View File
@@ -77,6 +77,7 @@
#include "code\__DEFINES\external_organs.dm"
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
#include "code\__DEFINES\fishing.dm"
#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\fonts.dm"
@@ -205,6 +206,7 @@
#include "code\__DEFINES\dcs\signals\signals_adventure.dm"
#include "code\__DEFINES\dcs\signals\signals_area.dm"
#include "code\__DEFINES\dcs\signals\signals_assembly.dm"
#include "code\__DEFINES\dcs\signals\signals_beam.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
#include "code\__DEFINES\dcs\signals\signals_circuit.dm"
@@ -751,6 +753,7 @@
#include "code\datums\components\engraved.dm"
#include "code\datums\components\explodable.dm"
#include "code\datums\components\faction_granter.dm"
#include "code\datums\components\fishing_spot.dm"
#include "code\datums\components\force_move.dm"
#include "code\datums\components\fov_handler.dm"
#include "code\datums\components\fullauto.dm"
@@ -987,6 +990,7 @@
#include "code\datums\elements\kneecapping.dm"
#include "code\datums\elements\kneejerk.dm"
#include "code\datums\elements\knockback.dm"
#include "code\datums\elements\lazy_fishing_spot.dm"
#include "code\datums\elements\lifesteal.dm"
#include "code\datums\elements\light_blocking.dm"
#include "code\datums\elements\light_eaten.dm"
@@ -1621,6 +1625,7 @@
#include "code\game\objects\items\devices\scanners\slime_scanner.dm"
#include "code\game\objects\items\devices\scanners\t_scanner.dm"
#include "code\game\objects\items\food\_food.dm"
#include "code\game\objects\items\food\bait.dm"
#include "code\game\objects\items\food\bread.dm"
#include "code\game\objects\items\food\burgers.dm"
#include "code\game\objects\items\food\cake.dm"
@@ -2291,10 +2296,6 @@
#include "code\modules\antagonists\wizard\equipment\soulstone.dm"
#include "code\modules\antagonists\wizard\equipment\spellbook.dm"
#include "code\modules\antagonists\xeno\xeno.dm"
#include "code\modules\aquarium\aquarium.dm"
#include "code\modules\aquarium\aquarium_behaviour.dm"
#include "code\modules\aquarium\aquarium_kit.dm"
#include "code\modules\aquarium\fish.dm"
#include "code\modules\art\paintings.dm"
#include "code\modules\art\statues.dm"
#include "code\modules\assembly\assembly.dm"
@@ -2871,6 +2872,20 @@
#include "code\modules\explorer_drone\exploration_events\fluff.dm"
#include "code\modules\explorer_drone\exploration_events\resource.dm"
#include "code\modules\explorer_drone\exploration_events\trader.dm"
#include "code\modules\fishing\admin.dm"
#include "code\modules\fishing\bait.dm"
#include "code\modules\fishing\fish_catalog.dm"
#include "code\modules\fishing\fishing_equipment.dm"
#include "code\modules\fishing\fishing_minigame.dm"
#include "code\modules\fishing\fishing_portal_machine.dm"
#include "code\modules\fishing\fishing_rod.dm"
#include "code\modules\fishing\fishing_traits.dm"
#include "code\modules\fishing\aquarium\aquarium.dm"
#include "code\modules\fishing\aquarium\aquarium_kit.dm"
#include "code\modules\fishing\fish\_fish.dm"
#include "code\modules\fishing\fish\fish_types.dm"
#include "code\modules\fishing\sources\_fish_source.dm"
#include "code\modules\fishing\sources\source_types.dm"
#include "code\modules\flufftext\Dreaming.dm"
#include "code\modules\food_and_drinks\food.dm"
#include "code\modules\food_and_drinks\pizzabox.dm"
+33
View File
@@ -0,0 +1,33 @@
import { clamp } from "./math";
/**
* Returns random number between lowerBound exclusive and upperBound inclusive
*/
export const randomNumber = (lowerBound: number, upperBound: number) => {
return Math.random() * (upperBound - lowerBound) + lowerBound;
};
/**
* Returns random integer between lowerBound exclusive and upperBound inclusive
*/
export const randomInteger = (lowerBound: number, upperBound: number) => {
lowerBound = Math.ceil(lowerBound);
upperBound = Math.floor(upperBound);
return Math.floor(Math.random() * (upperBound - lowerBound) + lowerBound);
};
/**
* Returns random array element
*/
export const randomPick = <T>(array: T[]) => {
return array[Math.floor(Math.random() * array.length)];
};
/**
* Return 1 with probability P percent; otherwise 0
*/
export const randomProb = (probability: number) => {
const normalized = clamp(probability, 0, 100)/100;
return Math.random() <= normalized;
};
+9
View File
@@ -14,6 +14,7 @@
import { perf } from 'common/perf';
import { createAction } from 'common/redux';
import { setupDrag } from './drag';
import { globalEvents } from './events';
import { focusMap } from './focus';
import { createLogger } from './logging';
import { resumeRenderer, suspendRenderer } from './renderer';
@@ -138,6 +139,14 @@ export const backendMiddleware = store => {
return;
}
if (type === "byond/mousedown") {
globalEvents.emit("byond/mousedown");
}
if (type === "byond/mouseup") {
globalEvents.emit("byond/mouseup");
}
if (type === 'backend/suspendStart' && !suspendInterval) {
logger.log(`suspending (${Byond.windowId})`);
// Keep sending suspend messages until it succeeds.
@@ -6,26 +6,53 @@ import { Box, Button, LabeledList, Section, Stack } from '../components';
import { Window } from '../layouts';
import { capitalize } from 'common/string';
type FishingTips = {
spots : string,
difficulty: string,
favorite_bait: string,
disliked_bait: string,
traits: string[]
}
type FishInfo = {
name: string,
desc: string,
fluid: string,
temp_min: number,
temp_max: number,
feed: string,
source: string,
fishing_tips: FishingTips,
weight : string,
size: string,
icon: string
}
type FishCatalogData = {
fish_info : FishInfo[] | null,
sponsored_by: string
}
export const FishCatalog = (props, context) => {
const { act, data } = useBackend(context);
const { act, data } = useBackend<FishCatalogData>(context);
const {
fish_info,
sponsored_by,
} = data;
const fish_by_name = flow([
sortBy(fish => fish.name),
])(data.fish_info || []);
sortBy((fish: FishInfo) => fish.name),
])(fish_info || []);
const [
currentFish,
setCurrentFish,
] = useLocalState(context, 'currentFish', null);
] = useLocalState<FishInfo | null>(context, 'currentFish', null);
return (
<Window
width={500}
height={300}>
<Window.Content>
<Stack fill>
<Stack.Item width="120px">
<Stack.Item width="140px">
<Section fill scrollable>
{fish_by_name.map(f => (
<Button
@@ -63,6 +90,28 @@ export const FishCatalog = (props, context) => {
<LabeledList.Item label="Acquisition">
{currentFish.source}
</LabeledList.Item>
<LabeledList.Item label="Average size">
{currentFish.size} cm
</LabeledList.Item>
<LabeledList.Item label="Average weight">
{currentFish.weight} g
</LabeledList.Item>
<LabeledList.Item label="Fishing tips">
<LabeledList>
<LabeledList.Item label="Fishing locations">
{currentFish.fishing_tips.spots}
</LabeledList.Item>
<LabeledList.Item label="Favourite bait">
{currentFish.fishing_tips.favorite_bait}
</LabeledList.Item>
<LabeledList.Item label="Disliked bait">
{currentFish.fishing_tips.disliked_bait}
</LabeledList.Item>
<LabeledList.Item label="Behavior">
{currentFish.fishing_tips.traits}
</LabeledList.Item>
</LabeledList>
</LabeledList.Item>
<LabeledList.Item label="Illustration">
<Box
className={classes([
+432
View File
@@ -0,0 +1,432 @@
import { clamp } from 'common/math';
import { randomInteger, randomNumber, randomPick, randomProb } from 'common/random';
import { useDispatch } from 'common/redux';
import { Component } from 'inferno';
import { resolveAsset } from '../assets';
import { backendSuspendStart, useBackend } from '../backend';
import { Icon } from '../components';
import { globalEvents } from '../events';
import { Window } from '../layouts';
import { logger } from '../logging';
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
}
type FishingMinigameProps = {
difficulty: number,
fish_ai: FishAI
special_rules: SpecialRule[],
background: string,
win: (perfect: boolean) => void,
lose: () => void
}
type FishingMinigameState = {
completion: number;
bait: Bait;
fish: Fish;
}
type SpecialRule = "weighted" | "limit_loss" | "heavy"
class FishingMinigame
extends Component<FishingMinigameProps, FishingMinigameState> {
animation_id: number;
last_frame: number;
reeling: ReelingState = ReelingState.Idle;
perfect: boolean = true;
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;
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;
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.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);
}
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);
}
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)) {
logger.log(`Short jump with chance ${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 = -1500;
// Gravity
const acceleration_down = 1000;
// 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;
newVelocity = -bait.velocity * bounce_coeff;
}
const acceleration = this.reeling === ReelingState.Reeling
? acceleration_up : acceleration_down;
const velocity_change = acceleration * seconds;
// Slowdown both ways when on fish
if (this.fishOnBait(fish, bait)) {
newVelocity += on_point_coeff * velocity_change;
}
else {
newVelocity += velocity_change;
}
// 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;
this.perfect = false;
}
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.props.lose();
dispatch(backendSuspendStart());
} else if (newCompletion >= 100) {
this.props.win(this.perfect);
dispatch(backendSuspendStart());
}
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) {
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 (
<div class="fishing">
<div class="main">
<div class="background" style={{ "background-image": `url("${background_image}")` }}>
<div class="bait" style={{
height: `${posToStyle(bait.height)}%`,
top: `${posToStyle(bait.position)}%`,
}} />
<div class="fish" style={{
top: `${posToStyle(fish.position)}%`,
height: `${posToStyle(fish.height)}%`,
}} ><Icon name="fish" />
</div>
</div>
</div>
<div class="completion">
<div class="background">
<div class="bar" style={{ height: `${Math.round(completion)}%` }} />
</div>
</div>
</div>);
}
}
type FishingData = {
difficulty: number,
fish_ai: FishAI
special_effects: SpecialRule[]
background_image: string;
}
export const Fishing = (props, context) => {
const { act, data } = useBackend<FishingData>(context);
return (
<Window width={180} height={600}>
<Window.Content fitted>
<FishingMinigame
difficulty={data.difficulty}
fish_ai={data.fish_ai}
special_rules={data.special_effects}
background={data.background_image}
win={(perfect) => act("win", { perfect: perfect })}
lose={() => act("lose")} />
</Window.Content>
</Window>);
};
@@ -0,0 +1,84 @@
import { round } from 'common/math';
import { useBackend, useLocalState } from '../backend';
import { Button, Dropdown, Input, Stack, Table } from '../components';
import { TableCell, TableRow } from '../components/Table';
import { Window } from '../layouts';
type FishCalculatorEntry = {
result: string,
weight: number,
difficulty: number,
count: string,
}
type FishingCalculatorData = {
info : FishCalculatorEntry[] | null,
hook_types: string[],
rod_types: string[],
line_types: string[],
spot_types: string[]
}
export const FishingCalculator = (props, context) => {
const { act, data } = useBackend<FishingCalculatorData>(context);
const [bait, setBait] = useLocalState<string>(context, "bait", "/obj/item/food/bait/worm");
const [spot, setSpot] = useLocalState<string>(context, "spot", data.spot_types[0]);
const [rod, setRod] = useLocalState<string>(context, "rod", data.rod_types[0]);
const [hook, setHook] = useLocalState<string>(context, "hook", data.hook_types[0]);
const [line, setLine] = useLocalState<string>(context, "line", data.line_types[0]);
const weight_sum = data.info?.reduce((s, w) => s + w.weight, 0) || 1;
return (
<Window
width={500}
height={500}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<Dropdown
options={data.spot_types}
selected={spot}
onSelected={e => setSpot(e)} width="100%" />
<Dropdown
options={data.rod_types}
selected={rod}
onSelected={e => setRod(e)} width="100%" />
<Dropdown
options={data.hook_types}
selected={hook}
onSelected={e => setHook(e)} width="100%" />
<Dropdown
options={data.line_types}
selected={line}
onSelected={e => setLine(e)} width="100%" />
<Input value={bait} label="Bait" onChange={(_, value) => setBait(value)} width="100%" />
<Button onClick={() => act("recalc", { "rod": rod, "bait": bait, "hook": hook, "line": line, "spot": spot })}>Calculate</Button>
</Stack.Item>
<Stack.Item>
<Table>
<TableRow header>
<TableCell>Outcome</TableCell>
<TableCell>Weight</TableCell>
<TableCell>Probabilty</TableCell>
<TableCell>Difficulty</TableCell>
<TableCell>Count</TableCell>
</TableRow>
{data.info?.map(result => (
<TableRow key={result.result}>
<TableCell>{result.result}</TableCell>
<TableCell>{result.weight}</TableCell>
<TableCell>
{round(result.weight / weight_sum * 100, 2) }%
</TableCell>
<TableCell>{result.difficulty}</TableCell>
<TableCell>{result.count}</TableCell>
</TableRow>))}
</Table>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,30 @@
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { Button, LabeledList, Section } from '../components';
type FishingPortalData = {
active: boolean;
presets: string[];
active_preset : string;
}
export const FishingPortalGenerator = (props, context) => {
const { act, data } = useBackend<FishingPortalData>(context);
return (
<Window title="Ishmael3000" width={300} height={300}>
<Window.Content>
<Section>
{!data.active && (
<LabeledList>
{data.presets.map(x => (
<LabeledList.Item key={x}>
<Button disabled={data.active} onClick={() => act("preset", { "preset": x })} content={x} selected={x === data.active_preset} />
</LabeledList.Item>))}
</LabeledList>)}
<Button content={data.active && "Deactivate" || "Activate"} onClick={() => data.active && act("toggle")} />
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,98 @@
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { Box, Stack, Button, Section, Flex } from '../components';
type Quality = {
name: string,
kind: "good" | "neutral" | "medium"
}
type FishingRodData = {
bait_name: string,
bait_icon: string,
line_name: string,
line_icon: string,
hook_name: string,
hook_icon: string
description: string,
}
type FishingSlotProps = {
name : string,
slot: string,
current_item_name: string | null,
current_item_icon: string | null
}
const FishingRodSlot = (props: FishingSlotProps, context) => {
const { act } = useBackend(context);
const icon_wrapper = icon => (<Box
as="img"
width="64px" // todo come up with some way to scale this sanely
height="64px"
src={`data:image/jpeg;base64,${icon}`}
style={{
"-ms-interpolation-mode": "nearest-neighbor",
"vertical-align": "middle",
"object-fit": "cover",
}} />);
return (
<Section title={`${props.name}`}>
<Stack>
<Stack.Item grow>
<Button fluid
onClick={() => act("slot_action", { slot: props.slot })}>
<Flex>
<Flex.Item>
{!!props.current_item_icon
&& icon_wrapper(props.current_item_icon)}
</Flex.Item>
<Flex.Item grow align="center">
<Box textAlign="center">{props.current_item_name ?? "None"}</Box>
</Flex.Item>
</Flex>
</Button>
</Stack.Item>
</Stack>
</Section>);
};
export const FishingRod = (props, context) => {
const { act, data } = useBackend<FishingRodData>(context);
return (
<Window>
<Window.Content>
<Section>
<FishingRodSlot
name="Bait"
slot="bait"
current_item_name={data.bait_name}
current_item_icon={data.bait_icon}
/>
<FishingRodSlot
name="Line"
slot="line"
current_item_name={data.line_name}
current_item_icon={data.line_icon}
/>
<FishingRodSlot
name="Hook"
slot="hook"
current_item_name={data.hook_name}
current_item_icon={data.hook_icon}
/>
</Section>
<Section>
{data.description}
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,79 @@
.fishing {
width: 121px;
height: 500px;
-ms-user-select: none;
user-select: none;
min-width: 121px;
min-height: 500px;
}
.fishing .main {
position: absolute;
width: 50px;
height: 500px;
left: 30px;
}
.fishing .main .background {
position: relative;
width: 50px;
height: 500px;
border-radius: 10px;
border: 0px black solid;
-ms-interpolation-mode: nearest-neighbor;
}
.fishing .main .fish {
width: 100%;
position: absolute;
right: 0;
font-size: 3em;
text-align: center;
-ms-interpolation-mode: nearest-neighbor;
color: white;
background-color: rgba(65, 91, 138, 0.5);
}
.fishing .main .fish *::before {
position: absolute;
top: -5px;
left: 5px;
}
.fishing .main .bait {
position: absolute;
width: 100%;
top: 0%;
left: 0;
background: #4d5f2b;
border: 0px #4d5f2b;
border-radius: 10px;
}
.fishing .completion {
position: absolute;
width: 30px;
height: 500px;
left: 95px;
top: 0;
}
.fishing .completion .background {
position: relative;
width: 100%;
height: 100%;
border-radius: 5px;
border: 2px black solid;
background: rgb(186, 217, 231);
box-shadow: 5px 5px 2px rgba(0,0,0,0.3);
}
.fishing .completion .bar {
position: absolute;
width: 100%;
background: #7cb413;
bottom: 0;
border-radius: 5px;
}
+1
View File
@@ -53,6 +53,7 @@
@include meta.load-css('./interfaces/Changelog.scss');
@include meta.load-css('./interfaces/CrewManifest.scss');
@include meta.load-css('./interfaces/ExperimentConfigure.scss');
@include meta.load-css('./interfaces/Fishing.scss');
@include meta.load-css('./interfaces/HellishRunes.scss');
@include meta.load-css('./interfaces/HotKeysHelp.scss');
@include meta.load-css('./interfaces/Hypertorus.scss');