diff --git a/code/__DEFINES/dcs/signals/signals_beam.dm b/code/__DEFINES/dcs/signals/signals_beam.dm new file mode 100644 index 00000000000..edfbc7c4371 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_beam.dm @@ -0,0 +1,3 @@ +/// Called before beam is redrawn +#define COMSIG_BEAM_BEFORE_DRAW "beam_before_draw" + #define BEAM_CANCEL_DRAW (1 << 0) diff --git a/code/__DEFINES/dcs/signals/signals_fish.dm b/code/__DEFINES/dcs/signals/signals_fish.dm index d40d6f3e595..89e25d85136 100644 --- a/code/__DEFINES/dcs/signals/signals_fish.dm +++ b/code/__DEFINES/dcs/signals/signals_fish.dm @@ -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" diff --git a/code/__DEFINES/fishing.dm b/code/__DEFINES/fishing.dm new file mode 100644 index 00000000000..40b4d21a9e8 --- /dev/null +++ b/code/__DEFINES/fishing.dm @@ -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" diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm index 0d486296f96..9f1066c2984 100644 --- a/code/__DEFINES/food.dm +++ b/code/__DEFINES/food.dm @@ -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 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 6ca33a53847..ae00cf0ed4a 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -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" diff --git a/code/__HELPERS/maths.dm b/code/__HELPERS/maths.dm index 49cab9bf273..9d67466cf5b 100644 --- a/code/__HELPERS/maths.dm +++ b/code/__HELPERS/maths.dm @@ -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) diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm index aab842e3747..5185461ea49 100644 --- a/code/_globalvars/lists/maintenance_loot.dm +++ b/code/_globalvars/lists/maintenance_loot.dm @@ -290,6 +290,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 diff --git a/code/datums/beam.dm b/code/datums/beam.dm index bb67f55632d..a92b0480caa 100644 --- a/code/datums/beam.dm +++ b/code/datums/beam.dm @@ -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 + + diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm new file mode 100644 index 00000000000..78b9d64cbd2 --- /dev/null +++ b/code/datums/components/fishing_spot.dm @@ -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) diff --git a/code/datums/elements/lazy_fishing_spot.dm b/code/datums/elements/lazy_fishing_spot.dm new file mode 100644 index 00000000000..603cd56e22f --- /dev/null +++ b/code/datums/elements/lazy_fishing_spot.dm @@ -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) diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm index 8706990d918..ad3b54efce7 100644 --- a/code/datums/mood_events/generic_positive_events.dm +++ b/code/datums/mood_events/generic_positive_events.dm @@ -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 diff --git a/code/game/objects/items/food/_food.dm b/code/game/objects/items/food/_food.dm index 97288efa60f..fd9c9d3be77 100644 --- a/code/game/objects/items/food/_food.dm +++ b/code/game/objects/items/food/_food.dm @@ -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() diff --git a/code/game/objects/items/food/bait.dm b/code/game/objects/items/food/bait.dm new file mode 100644 index 00000000000..30856b3a18a --- /dev/null +++ b/code/game/objects/items/food/bait.dm @@ -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 diff --git a/code/game/objects/items/food/dough.dm b/code/game/objects/items/food/dough.dm index 36c9076fc70..39a699c84ea 100644 --- a/code/game/objects/items/food/dough.dm +++ b/code/game/objects/items/food/dough.dm @@ -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." diff --git a/code/game/objects/structures/maintenance.dm b/code/game/objects/structures/maintenance.dm index 84030d00712..8c4e887c3c5 100644 --- a/code/game/objects/structures/maintenance.dm +++ b/code/game/objects/structures/maintenance.dm @@ -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) diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm index 7d4312fd818..e99c79a0659 100644 --- a/code/game/turfs/open/lava.dm +++ b/code/game/turfs/open/lava.dm @@ -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 diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm index 6d8c1316c3f..151a4c8e1d4 100644 --- a/code/game/turfs/open/water.dm +++ b/code/game/turfs/open/water.dm @@ -28,6 +28,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 diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index c33e5d0ff73..fa1cbf13d22 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -89,6 +89,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) diff --git a/code/modules/aquarium/aquarium_behaviour.dm b/code/modules/aquarium/aquarium_behaviour.dm deleted file mode 100644 index e735b3ec45d..00000000000 --- a/code/modules/aquarium/aquarium_behaviour.dm +++ /dev/null @@ -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 -*/ diff --git a/code/modules/asset_cache/assets/fish.dm b/code/modules/asset_cache/assets/fish.dm index a76e8b096fe..44300c63587 100644 --- a/code/modules/asset_cache/assets/fish.dm +++ b/code/modules/asset_cache/assets/fish.dm @@ -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' + ) diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm index 95d4854c9cf..c7404a154e0 100644 --- a/code/modules/cargo/goodies.dm +++ b/code/modules/cargo/goodies.dm @@ -168,3 +168,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) diff --git a/code/modules/cargo/packs.dm b/code/modules/cargo/packs.dm index 06b5dd763ea..c9b98eab64d 100644 --- a/code/modules/cargo/packs.dm +++ b/code/modules/cargo/packs.dm @@ -2817,6 +2817,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 ///////////////////////////// ////////////////////////////////////////////////////////////////////////////// diff --git a/code/modules/fishing/admin.dm b/code/modules/fishing/admin.dm new file mode 100644 index 00000000000..ad97ab890b4 --- /dev/null +++ b/code/modules/fishing/admin.dm @@ -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 diff --git a/code/modules/aquarium/aquarium.dm b/code/modules/fishing/aquarium/aquarium.dm similarity index 100% rename from code/modules/aquarium/aquarium.dm rename to code/modules/fishing/aquarium/aquarium.dm diff --git a/code/modules/aquarium/aquarium_kit.dm b/code/modules/fishing/aquarium/aquarium_kit.dm similarity index 66% rename from code/modules/aquarium/aquarium_kit.dm rename to code/modules/fishing/aquarium/aquarium_kit.dm index f091a7c6f05..3221acce755 100644 --- a/code/modules/aquarium/aquarium_kit.dm +++ b/code/modules/fishing/aquarium/aquarium_kit.dm @@ -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." diff --git a/code/modules/fishing/bait.dm b/code/modules/fishing/bait.dm new file mode 100644 index 00000000000..b67298fab9f --- /dev/null +++ b/code/modules/fishing/bait.dm @@ -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 diff --git a/code/modules/aquarium/fish.dm b/code/modules/fishing/fish/_fish.dm similarity index 58% rename from code/modules/aquarium/fish.dm rename to code/modules/fishing/fish/_fish.dm index 220918499aa..51990a0e340 100644 --- a/code/modules/aquarium/fish.dm +++ b/code/modules/fishing/fish/_fish.dm @@ -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) diff --git a/code/modules/fishing/fish/fish_types.dm b/code/modules/fishing/fish/fish_types.dm new file mode 100644 index 00000000000..f6e99c73586 --- /dev/null +++ b/code/modules/fishing/fish/fish_types.dm @@ -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) diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm new file mode 100644 index 00000000000..396d112bd43 --- /dev/null +++ b/code/modules/fishing/fish_catalog.dm @@ -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) + ) diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm new file mode 100644 index 00000000000..6ad286c5285 --- /dev/null +++ b/code/modules/fishing/fishing_equipment.dm @@ -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) diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm new file mode 100644 index 00000000000..ce91cbf0332 --- /dev/null +++ b/code/modules/fishing/fishing_minigame.dm @@ -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 diff --git a/code/modules/fishing/fishing_portal_machine.dm b/code/modules/fishing/fishing_portal_machine.dm new file mode 100644 index 00000000000..3fc6b0eb938 --- /dev/null +++ b/code/modules/fishing/fishing_portal_machine.dm @@ -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() diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm new file mode 100644 index 00000000000..66700957e7a --- /dev/null +++ b/code/modules/fishing/fishing_rod.dm @@ -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 + diff --git a/code/modules/fishing/fishing_traits.dm b/code/modules/fishing/fishing_traits.dm new file mode 100644 index 00000000000..3a58ae5157c --- /dev/null +++ b/code/modules/fishing/fishing_traits.dm @@ -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 diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm new file mode 100644 index 00000000000..f27d4aed837 --- /dev/null +++ b/code/modules/fishing/sources/_fish_source.dm @@ -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 diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm new file mode 100644 index 00000000000..9b626de6956 --- /dev/null +++ b/code/modules/fishing/sources/source_types.dm @@ -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 diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm index de07c952c72..79baf2e195c 100644 --- a/code/modules/keybindings/setup.dm +++ b/code/modules/keybindings/setup.dm @@ -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 diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 8028f6d0e45..a59f350c938 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -182,6 +182,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 diff --git a/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm index 10505e89af5..7a2777d8bd0 100644 --- a/code/modules/research/designs/autolathe_designs.dm +++ b/code/modules/research/designs/autolathe_designs.dm @@ -1288,3 +1288,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") diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index f168e7c6adc..abcc07164ab 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -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") diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index 1730b508c14..185b213bb97 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -1279,7 +1279,7 @@ "cybernetic_stomach_tier2", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1000) - + /datum/techweb_node/cyber_organs_upgraded id = "cyber_organs_upgraded" @@ -2255,6 +2255,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() diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index dff46365f48..cace17568c9 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -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 @@ -110,6 +112,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 @@ -148,6 +152,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 * diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index ded443f37a0..674016bcafa 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -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 diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi index 7ff9807d9e5..b050a6dfd59 100644 Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ diff --git a/icons/hud/fishing.dmi b/icons/hud/fishing.dmi new file mode 100644 index 00000000000..c607ed13e2a Binary files /dev/null and b/icons/hud/fishing.dmi differ diff --git a/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi b/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi new file mode 100644 index 00000000000..846c36522cc Binary files /dev/null and b/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi differ diff --git a/icons/mob/inhands/equipment/fishing_rod_righthand.dmi b/icons/mob/inhands/equipment/fishing_rod_righthand.dmi new file mode 100644 index 00000000000..fdc2e770c99 Binary files /dev/null and b/icons/mob/inhands/equipment/fishing_rod_righthand.dmi differ diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi new file mode 100644 index 00000000000..13b2409ed28 Binary files /dev/null and b/icons/obj/fishing.dmi differ diff --git a/icons/obj/storage.dmi b/icons/obj/storage.dmi index 16971814404..01f35df64ff 100644 Binary files a/icons/obj/storage.dmi and b/icons/obj/storage.dmi differ diff --git a/icons/ui_icons/fishing/default.png b/icons/ui_icons/fishing/default.png new file mode 100644 index 00000000000..f21074ac2dd Binary files /dev/null and b/icons/ui_icons/fishing/default.png differ diff --git a/icons/ui_icons/fishing/lavaland.png b/icons/ui_icons/fishing/lavaland.png new file mode 100644 index 00000000000..6c97f66432e Binary files /dev/null and b/icons/ui_icons/fishing/lavaland.png differ diff --git a/sound/effects/bigsplash.ogg b/sound/effects/bigsplash.ogg new file mode 100644 index 00000000000..772e8c5b260 Binary files /dev/null and b/sound/effects/bigsplash.ogg differ diff --git a/sound/effects/fish_splash.ogg b/sound/effects/fish_splash.ogg new file mode 100644 index 00000000000..4c5a68bab79 Binary files /dev/null and b/sound/effects/fish_splash.ogg differ diff --git a/sound/effects/splash.ogg b/sound/effects/splash.ogg new file mode 100644 index 00000000000..a02121ae02f Binary files /dev/null and b/sound/effects/splash.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 4d1fb77f25b..205c1cd5f35 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -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" @@ -812,6 +814,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" @@ -1048,6 +1051,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" @@ -1682,6 +1686,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" @@ -2352,10 +2357,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" @@ -2933,6 +2934,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" diff --git a/tgui/packages/common/random.ts b/tgui/packages/common/random.ts new file mode 100644 index 00000000000..7f99cda39f3 --- /dev/null +++ b/tgui/packages/common/random.ts @@ -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 = (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; +}; + diff --git a/tgui/packages/tgui/backend.ts b/tgui/packages/tgui/backend.ts index 4a58d55c6a7..bf84aa80c41 100644 --- a/tgui/packages/tgui/backend.ts +++ b/tgui/packages/tgui/backend.ts @@ -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. diff --git a/tgui/packages/tgui/interfaces/FishCatalog.js b/tgui/packages/tgui/interfaces/FishCatalog.tsx similarity index 57% rename from tgui/packages/tgui/interfaces/FishCatalog.js rename to tgui/packages/tgui/interfaces/FishCatalog.tsx index 33e6544a5c4..a40f33443da 100644 --- a/tgui/packages/tgui/interfaces/FishCatalog.js +++ b/tgui/packages/tgui/interfaces/FishCatalog.tsx @@ -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(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(context, 'currentFish', null); return ( - +
{fish_by_name.map(f => ( + + + + + Outcome + Weight + Probabilty + Difficulty + Count + + {data.info?.map(result => ( + + {result.result} + {result.weight} + + {round(result.weight / weight_sum * 100, 2) }% + + {result.difficulty} + {result.count} + ))} +
+
+ + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/FishingPortalGenerator.tsx b/tgui/packages/tgui/interfaces/FishingPortalGenerator.tsx new file mode 100644 index 00000000000..c499b945ec9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FishingPortalGenerator.tsx @@ -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(context); + + return ( + + +
+ {!data.active && ( + + {data.presets.map(x => ( + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/FishingRod.tsx b/tgui/packages/tgui/interfaces/FishingRod.tsx new file mode 100644 index 00000000000..f2aeceb9ff8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/FishingRod.tsx @@ -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 => (); + + return ( +
+ + + + + +
); +}; + +export const FishingRod = (props, context) => { + const { act, data } = useBackend(context); + + return ( + + +
+ + + +
+
+ {data.description} +
+
+
+ ); +}; + diff --git a/tgui/packages/tgui/styles/interfaces/Fishing.scss b/tgui/packages/tgui/styles/interfaces/Fishing.scss new file mode 100644 index 00000000000..37c4422e4e6 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/Fishing.scss @@ -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; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 40000734786..b02ea5aa4fb 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -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');