diff --git a/_maps/templates/holodeck_beach.dmm b/_maps/templates/holodeck_beach.dmm index 6260ed27862..d485a495db3 100644 --- a/_maps/templates/holodeck_beach.dmm +++ b/_maps/templates/holodeck_beach.dmm @@ -30,6 +30,9 @@ /obj/item/toy/seashell, /turf/open/floor/holofloor/beach, /area/template_noop) +"N" = ( +/turf/open/floor/holofloor/beach/water, +/area/template_noop) "Q" = ( /obj/effect/holodeck_effect/mobspawner/crab/jon, /turf/open/floor/holofloor/beach, @@ -64,8 +67,8 @@ R R R R -R a +N "} (2,1,1) = {" R @@ -76,8 +79,8 @@ R S R Q -R a +N "} (3,1,1) = {" X @@ -88,8 +91,8 @@ R t t R -R a +N "} (4,1,1) = {" R @@ -100,8 +103,8 @@ h R R R -R a +N "} (5,1,1) = {" R @@ -112,8 +115,8 @@ R t t R -R a +N "} (6,1,1) = {" R @@ -124,8 +127,8 @@ X g R q -R a +N "} (7,1,1) = {" R @@ -136,8 +139,8 @@ R R R f -R a +N "} (8,1,1) = {" W @@ -148,8 +151,8 @@ H R M R -R a +N "} (9,1,1) = {" R @@ -160,6 +163,6 @@ R R R R -R a +N "} diff --git a/code/__DEFINES/achievements.dm b/code/__DEFINES/achievements.dm index d533e0feab2..7307a7e9b01 100644 --- a/code/__DEFINES/achievements.dm +++ b/code/__DEFINES/achievements.dm @@ -50,6 +50,7 @@ //Skill medal hub IDs #define MEDAL_LEGENDARY_MINER "Legendary Miner" +#define MEDAL_LEGENDARY_FISHER "Legendary Fisher" //Mafia medal hub IDs (wins) #define MAFIA_MEDAL_ASSISTANT "Assistant" diff --git a/code/__DEFINES/aquarium.dm b/code/__DEFINES/aquarium.dm index 5e60e2a7a64..263f86db3e6 100644 --- a/code/__DEFINES/aquarium.dm +++ b/code/__DEFINES/aquarium.dm @@ -13,9 +13,20 @@ #define FISH_ALIVE "alive" #define FISH_DEAD "dead" +#define FISH_SIZE_TINY_MAX 30 +#define FISH_SIZE_SMALL_MAX 50 +#define FISH_SIZE_NORMAL_MAX 90 +#define FISH_SIZE_BULKY_MAX 130 + +#define FISH_GRIND_RESULTS_WEIGHT_DIVISOR 500 +#define FISH_FILLET_NUMBER_SIZE_DIVISOR 30 + +#define NEW_FISH_BREEDING_TIMEOUT_MULT 2 +#define NEW_FISH_LAST_FEEDING_MULT 0.5 + #define MIN_AQUARIUM_TEMP T0C #define MAX_AQUARIUM_TEMP (T0C + 100) -#define DEFAULT_AQUARIUM_TEMP 300 +#define DEFAULT_AQUARIUM_TEMP (T0C + 24) #define FISH_RARITY_BASIC 1000 #define FISH_RARITY_RARE 400 @@ -27,5 +38,6 @@ #define AQUARIUM_FLUID_SULPHWATEVER "Sulfuric Water" #define AQUARIUM_FLUID_AIR "Air" #define AQUARIUM_FLUID_ANADROMOUS "Adaptive to both Freshwater and Saltwater" +#define AQUARIUM_FLUID_ANY_WATER "Adaptive to all kind of water" #define AQUARIUM_COMPANY "Aquatech Ltd." diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 76d1ef360c4..98d32719099 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -56,6 +56,7 @@ #define COLOR_VIBRANT_LIME "#00FF00" #define COLOR_SERVICE_LIME "#58C800" #define COLOR_JADE "#5EFB6E" +#define COLOR_EMERALD "#00CC66" #define COLOR_LIME "#32CD32" #define COLOR_DARK_LIME "#00aa00" #define COLOR_VERY_PALE_LIME_GREEN "#DDFFD3" diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm index bae03993319..242385fa3e3 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm @@ -76,7 +76,7 @@ #define COMPONENT_ALLOW_REACH (1<<0) ///for when an atom has been created through processing (atom/original_atom, list/chosen_processing_option) #define COMSIG_ATOM_CREATEDBY_PROCESSING "atom_createdby_processing" -///when an atom is processed (mob/living/user, obj/item/I, list/atom/results) +///when an atom is processed (mob/living/user, obj/item/process_item, list/atom/results) #define COMSIG_ATOM_PROCESSED "atom_processed" ///called when teleporting into a possibly protected turf: (channel, turf/origin, turf/destination) #define COMSIG_ATOM_INTERCEPT_TELEPORTING "intercept_teleporting" diff --git a/code/__DEFINES/dcs/signals/signals_fish.dm b/code/__DEFINES/dcs/signals/signals_fish.dm index 89e25d85136..7da86f90acc 100644 --- a/code/__DEFINES/dcs/signals/signals_fish.dm +++ b/code/__DEFINES/dcs/signals/signals_fish.dm @@ -5,9 +5,18 @@ // Fish signals #define COMSIG_FISH_STATUS_CHANGED "fish_status_changed" #define COMSIG_FISH_STIRRED "fish_stirred" +///From /obj/item/fish/process: (seconds_per_tick) +#define COMSIG_FISH_LIFE "fish_life" +///From /datum/fish_trait/eat_fish: (predator) +#define COMSIG_FISH_EATEN_BY_OTHER_FISH "fish_eaten_by_other_fish" +///From /obj/item/fish/feed: (fed_reagents, fed_reagent_type) +#define COMSIG_FISH_FED "fish_on_fed" /// Fishing challenge completed #define COMSIG_FISHING_CHALLENGE_COMPLETED "fishing_completed" +/// Sent to the fisherman when the reward is dispensed: (reward) +#define COMSIG_MOB_FISHING_REWARD_DISPENSED "mob_fishing_reward_dispensed" + /// Called when you try to use fishing rod on anything #define COMSIG_PRE_FISHING "pre_fishing" @@ -17,3 +26,9 @@ /// Sent when fishing line is snapped #define COMSIG_FISHING_LINE_SNAPPED "fishing_line_interrupted" + +/// Sent when the challenge is to be interrupted: (reason) +#define COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE "fishing_spot_interrupt_challenge" + +/// Sent to the reward after it's been generated by fishing: (fishing_spot) +#define COMSIG_ATOM_FISHING_REWARD "atom_fishing_reward" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 6083a9ae1db..420147f0c1e 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -104,6 +104,10 @@ ///from base of obj/item/equipped(): (mob/equipper, slot) #define COMSIG_ITEM_EQUIPPED "item_equip" +///From base of obj/item/on_equipped() (mob/equipped, slot) +#define COMSIG_ITEM_POST_EQUIPPED "item_post_equipped" + /// This will make the on_equipped proc return FALSE. + #define COMPONENT_EQUIPPED_FAILED (1<<0) /// A mob has just equipped an item. Called on [/mob] from base of [/obj/item/equipped()]: (/obj/item/equipped_item, slot) #define COMSIG_MOB_EQUIPPED_ITEM "mob_equipped_item" /// A mob has just unequipped an item. diff --git a/code/__DEFINES/fishing.dm b/code/__DEFINES/fishing.dm index b20d3ebe1a0..4cc2695b798 100644 --- a/code/__DEFINES/fishing.dm +++ b/code/__DEFINES/fishing.dm @@ -35,18 +35,39 @@ /// Fishing hook trait that's used to make the bait more weighted, for the /// fishing minigame itself. #define FISHING_HOOK_WEIGHTED (1 << 1) +/** + * During the fishing minigame, it stops the bait from being pulled down by gravity, + * while also allowing the player to move it down with right-click. + */ +#define FISHING_HOOK_BIDIRECTIONAL (1 << 2) +///Prevents the user from losing the game by letting the fish get away. +#define FISHING_HOOK_NO_ESCAPE (1 << 3) +///Limits the completion loss of the minigame when the fsh is not on the bait area. +#define FISHING_HOOK_ENSNARE (1 << 4) +///Slowly damages the fish, until it dies, then it's victory. +#define FISHING_HOOK_KILL (1 << 5) +///Reduces the difficulty of the minigame #define FISHING_LINE_CLOAKED (1 << 0) +///Required to cast a line on lava. #define FISHING_LINE_REINFORCED (1 << 1) +/// Much like FISHING_HOOK_ENSNARE but for the reel. #define FISHING_LINE_BOUNCY (1 << 2) #define FISHING_SPOT_PRESET_BEACH "beach" #define FISHING_SPOT_PRESET_LAVALAND_LAVA "lavaland lava" +#define FISHING_SPOT_PRESET_ICEMOON_PLASMA "icemoon plasma" #define FISHING_SPOT_PRESET_CHASM "chasm" +#define FISHING_SPOT_PRESET_TOILET "toilet" #define FISHING_MINIGAME_RULE_HEAVY_FISH "heavy" +#define FISHING_MINIGAME_RULE_LUBED_FISH "lubed" #define FISHING_MINIGAME_RULE_WEIGHTED_BAIT "weighted" #define FISHING_MINIGAME_RULE_LIMIT_LOSS "limit_loss" +#define FISHING_MINIGAME_RULE_BIDIRECTIONAL "bidirectional" +#define FISHING_MINIGAME_RULE_NO_ESCAPE "no_escape" +#define FISHING_MINIGAME_RULE_KILL "kill" +#define FISHING_MINIGAME_RULE_NO_EXP "no_exp" /// The default additive value for fishing hook catch weight modifiers. #define FISHING_DEFAULT_HOOK_BONUS_ADDITIVE 0 diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index a8c25365676..2ef23a0eedd 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -190,6 +190,8 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list( #define isitem(A) (istype(A, /obj/item)) +#define isfish(A) (istype(A, /obj/item/fish)) + #define isstack(A) (istype(A, /obj/item/stack)) #define isgrenade(A) (istype(A, /obj/item/grenade)) @@ -204,6 +206,8 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list( #define isstructure(A) (istype(A, /obj/structure)) +#define isaquarium(A) (istype(A, /obj/structure/aquarium)) + #define ismachinery(A) (istype(A, /obj/machinery)) #define isvendor(A) (istype(A, /obj/machinery/vending)) diff --git a/code/__DEFINES/skills.dm b/code/__DEFINES/skills.dm index 6329989b3c8..26e66eee1f5 100644 --- a/code/__DEFINES/skills.dm +++ b/code/__DEFINES/skills.dm @@ -24,9 +24,14 @@ #define SKILL_EXP_LIST list(SKILL_EXP_NONE, SKILL_EXP_NOVICE, SKILL_EXP_APPRENTICE, SKILL_EXP_JOURNEYMAN, SKILL_EXP_EXPERT, SKILL_EXP_MASTER, SKILL_EXP_LEGENDARY) //Skill modifier types -#define SKILL_SPEED_MODIFIER "skill_speed_modifier"//ideally added/subtracted in speed calculations to make you do stuff faster -#define SKILL_PROBS_MODIFIER "skill_probability_modifier"//ideally added/subtracted where beneficial in prob(x) calls -#define SKILL_RANDS_MODIFIER "skill_randomness_modifier"//ideally added/subtracted where beneficial in rand(x,y) calls +///ideally added/subtracted in speed calculations to make you do stuff faster +#define SKILL_SPEED_MODIFIER "skill_speed_modifier" +///ideally added/subtracted where beneficial in prob(x) calls +#define SKILL_PROBS_MODIFIER "skill_probability_modifier" +///ideally added/subtracted where beneficial in rand(x,y) calls +#define SKILL_RANDS_MODIFIER "skill_randomness_modifier" +///ideally for addittive operations +#define SKILL_VALUE_MODIFIER "skill_value_modifier" // Gets the reference for the skill type that was given #define GetSkillRef(A) (SSskills.all_skills[A]) @@ -34,3 +39,9 @@ //number defines #define CLEAN_SKILL_BEAUTY_ADJUSTMENT -15//It's a denominator so no 0. Higher number = less cleaning xp per cleanable. Negative value means cleanables with negative beauty give xp. #define CLEAN_SKILL_GENERIC_WASH_XP 1.5//Value. Higher number = more XP when cleaning non-cleanables (walls/floors/lips) +///The multiplier of the extra experience given by the fishing minigame based on difficulty. At the default difficulty of 15, the bonus will be of 21%. +#define FISHING_SKILL_DIFFIULTY_EXP_MULT 0.015 +///How much exp one would gain per spent playing the fishing minigame at minimum difficulty. +#define FISHING_SKILL_EXP_PER_SECOND (SKILL_EXP_LEGENDARY / (22 MINUTES)) +///The maximum amount of experience one can get per fishing minigame. I appreciate the effort though. +#define FISHING_SKILL_EXP_CAP_PER_GAME (SKILL_EXP_LEGENDARY / 5) diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index fc319f086c4..7647bec63b2 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -852,6 +852,17 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Trait given to mechs that can have orebox functionality on movement #define TRAIT_OREBOX_FUNCTIONAL "orebox_functional" +///fish traits +#define TRAIT_RESIST_EMULSIFY "resist_emulsify" +#define TRAIT_FISH_SELF_REPRODUCE "fish_self_reproduce" +#define TRAIT_FISH_NO_MATING "fish_no_mating" +#define TRAIT_YUCKY_FISH "yucky_fish" +#define TRAIT_FISH_TOXIN_IMMUNE "fish_toxin_immune" +#define TRAIT_FISH_CROSSBREEDER "fish_crossbreeder" +#define TRAIT_FISH_AMPHIBIOUS "fish_amphibious" +///Trait needed for the lubefish evolution +#define TRAIT_FISH_FED_LUBE "fish_fed_lube" + // common trait sources #define TRAIT_GENERIC "generic" #define UNCONSCIOUS_TRAIT "unconscious" @@ -1189,6 +1200,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// the object has a label applied #define TRAIT_HAS_LABEL "labeled" +///coming from a fish trait datum. +#define FISH_TRAIT_DATUM "fish_trait_datum" +///coming from a fish evolution datum +#define FISH_EVOLUTION "fish_evolution" + /// some trait sorces dirived from bodyparts BODYPART_TRAIT is generic. #define BODYPART_TRAIT "bodypart" #define HEAD_TRAIT "head" diff --git a/code/__HELPERS/atmospherics.dm b/code/__HELPERS/atmospherics.dm index 0139fc08114..940418f2ebc 100644 --- a/code/__HELPERS/atmospherics.dm +++ b/code/__HELPERS/atmospherics.dm @@ -171,3 +171,31 @@ GLOBAL_LIST_EMPTY(gas_handbook) return object return null + +/** + * A simple helped proc that checks if the contents of a list of gases are within acceptable terms. + * + * Arguments: + * * gases: The list of gases which contents are being checked + * * gases to check: An associated list of gas types and acceptable boundaries in moles. e.g. /datum/gas/oxygen = list(16, 30) + * * * if the assoc list is null, then it'll be considered a safe gas and won't return FALSE. + * * extraneous_gas_limit: If a gas not in gases is found, this is the limit above which the proc will return FALSE. + */ +/proc/check_gases(list/gases, list/gases_to_check, extraneous_gas_limit = 0.1) + gases_to_check = gases_to_check.Copy() + for(var/id in gases) + var/gas_moles = gases[id][MOLES] + if(!(id in gases_to_check)) + if(gas_moles > extraneous_gas_limit) + return FALSE + continue + var/list/boundaries = gases_to_check[id] + if(boundaries && !ISINRANGE(gas_moles, boundaries[1], boundaries[2])) + return FALSE + gases_to_check -= id + ///Check that gases absent from the turf have a lower boundary of zero or none at all, otherwise return FALSE + for(var/id in gases_to_check) + var/list/boundaries = gases_to_check[id] + if(boundaries && boundaries[1] > 0) + return FALSE + return TRUE diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm index b08284c5434..d2178d6adfe 100644 --- a/code/_globalvars/lists/maintenance_loot.dm +++ b/code/_globalvars/lists/maintenance_loot.dm @@ -301,7 +301,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items /obj/item/shield/buckler = 1, /obj/item/throwing_star = 1, /obj/item/weldingtool/hugetank = 1, - /obj/item/fishing_rod/master = 1, + /obj/item/fishing_rod/telescopic/master = 1, /obj/item/spess_knife = 1, ) = 1, diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 718e2d0f80c..fc924d2d7f0 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -256,6 +256,15 @@ GLOBAL_LIST_INIT(traits_by_type, list( /obj/item/card/id = list( "TRAIT_MAGNETIC_ID_CARD" = TRAIT_MAGNETIC_ID_CARD, ), + /obj/item/fish = list( + "TRAIT_RESIST_EMULSIFY" = TRAIT_RESIST_EMULSIFY, + "TRAIT_FISH_SELF_REPRODUCE" = TRAIT_FISH_SELF_REPRODUCE, + "TRAIT_FISH_NO_MATING" = TRAIT_FISH_NO_MATING, + "TRAIT_YUCKY_FISH" = TRAIT_YUCKY_FISH, + "TRAIT_FISH_TOXIN_IMMUNE" = TRAIT_FISH_TOXIN_IMMUNE, + "TRAIT_FISH_CROSSBREEDER" = TRAIT_FISH_CROSSBREEDER, + "TRAIT_FISH_FED_LUBE" = TRAIT_FISH_FED_LUBE, + ), )) /// value -> trait name, generated on use from trait_by_type global diff --git a/code/_onclick/hud/radial_persistent.dm b/code/_onclick/hud/radial_persistent.dm index 4e83f161a3b..58dfa0e07ef 100644 --- a/code/_onclick/hud/radial_persistent.dm +++ b/code/_onclick/hud/radial_persistent.dm @@ -32,6 +32,13 @@ /datum/radial_menu/persistent/element_chosen(choice_id, mob/user, params) select_proc_callback.Invoke(choices_values[choice_id], params) +///Version of wait used by persistent radial menus. +/datum/radial_menu/persistent/wait() + while(!QDELETED(src)) + if(custom_check_callback && next_check < world.time) + custom_check_callback.Invoke() + next_check = world.time + check_delay + stoplag(1) /datum/radial_menu/persistent/proc/change_choices(list/newchoices, tooltips = FALSE, animate = FALSE, keep_same_page = FALSE) if(!newchoices.len) @@ -55,7 +62,7 @@ Select_proc is the proc to be called each time an element on the menu is clicked, and should accept the chosen element as its final argument Clicking the center button will return a choice of null */ -/proc/show_radial_menu_persistent(mob/user, atom/anchor, list/choices, datum/callback/select_proc, uniqueid, radius, tooltips = FALSE, radial_slice_icon = "radial_slice") +/proc/show_radial_menu_persistent(mob/user, atom/anchor, list/choices, datum/callback/select_proc, uniqueid, radius, tooltips = FALSE, radial_slice_icon = "radial_slice", datum/callback/custom_check) if(!user || !anchor || !length(choices) || !select_proc) return if(!uniqueid) @@ -71,9 +78,13 @@ menu.radius = radius menu.radial_slice_icon = radial_slice_icon menu.select_proc_callback = select_proc + if(istype(custom_check)) + menu.custom_check_callback = custom_check menu.anchor = anchor menu.check_screen_border(user) //Do what's needed to make it look good near borders or on hud menu.set_choices(choices, tooltips) menu.show_to(user) + if(menu.custom_check_callback) + menu.next_check = world.time + menu.check_delay + INVOKE_ASYNC(menu, TYPE_PROC_REF(/datum/radial_menu, wait)) return menu - diff --git a/code/datums/achievements/skill_achievements.dm b/code/datums/achievements/skill_achievements.dm index 128b35a6a4e..6384b1b3db4 100644 --- a/code/datums/achievements/skill_achievements.dm +++ b/code/datums/achievements/skill_achievements.dm @@ -7,3 +7,8 @@ database_id = MEDAL_LEGENDARY_MINER icon = "mining" +/datum/award/achievement/skill/legendary_fisher + name = "Legendary fisher" + desc = "Give a spaceman a fish and you feed him for a while; teach a spaceman to fish and you feed him until the shuttle arrives." + database_id = MEDAL_LEGENDARY_FISHER + icon = "fishing_hat" diff --git a/code/datums/components/aquarium.dm b/code/datums/components/aquarium_content.dm similarity index 99% rename from code/datums/components/aquarium.dm rename to code/datums/components/aquarium_content.dm index 25a95105174..589b831aa72 100644 --- a/code/datums/components/aquarium.dm +++ b/code/datums/components/aquarium_content.dm @@ -95,6 +95,8 @@ aquarium_vc_color = fish.aquarium_vc_color if(fish.dedicated_in_aquarium_icon_state) + if(fish.dedicated_in_aquarium_icon) + icon = fish.dedicated_in_aquarium_icon icon_state = fish.dedicated_in_aquarium_icon_state base_transform = matrix() else diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm index 0064999d798..30c63ebd021 100644 --- a/code/datums/components/chasm.dm +++ b/code/datums/components/chasm.dm @@ -1,6 +1,3 @@ -/// List of weakrefs to containers for things which have fallen into chasms -GLOBAL_LIST_INIT(chasm_storage, list()) - // Used by /turf/open/chasm and subtypes to implement the "dropping" mechanic /datum/component/chasm var/turf/target_turf @@ -35,6 +32,8 @@ GLOBAL_LIST_INIT(chasm_storage, list()) )) /datum/component/chasm/Initialize(turf/target, mapload) + if(!isturf(parent)) + return COMPONENT_INCOMPATIBLE RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_CHASM_STOPPED), PROC_REF(on_chasm_stopped)) RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_CHASM_STOPPED), PROC_REF(on_chasm_no_longer_stopped)) target_turf = target @@ -45,24 +44,10 @@ GLOBAL_LIST_INIT(chasm_storage, list()) //otherwise don't do anything because turfs and areas are initialized before movables. if(!mapload) addtimer(CALLBACK(src, PROC_REF(drop_stuff)), 0) - src.parent.AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_CHASM) + parent.AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_CHASM) /datum/component/chasm/UnregisterFromParent() - remove_storage() - -/** - * Deletes the chasm storage object and removes empty weakrefs from global list - */ -/datum/component/chasm/proc/remove_storage() - if (!storage) - return - QDEL_NULL(storage) - var/list/chasm_storage = list() - for (var/datum/weakref/ref as anything in GLOB.chasm_storage) - if (!ref.resolve()) - continue - chasm_storage += ref - GLOB.chasm_storage = chasm_storage + storage = null /datum/component/chasm/proc/entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs) SIGNAL_HANDLER @@ -197,9 +182,7 @@ GLOBAL_LIST_INIT(chasm_storage, list()) return if(!storage) - storage = new(get_turf(parent)) - RegisterSignal(storage, COMSIG_ATOM_EXITED, PROC_REF(left_chasm)) - GLOB.chasm_storage += WEAKREF(storage) + storage = (locate() in parent) || new(parent) if(storage.contains(dropped_thing)) return @@ -209,14 +192,11 @@ GLOBAL_LIST_INIT(chasm_storage, list()) dropped_thing.transform = oldtransform dropped_thing.pixel_y = oldoffset - if(dropped_thing.forceMove(storage)) - if (isliving(dropped_thing)) - RegisterSignal(dropped_thing, COMSIG_LIVING_REVIVE, PROC_REF(on_revive)) - else + if(!dropped_thing.forceMove(storage)) parent.visible_message(span_boldwarning("[parent] spits out [dropped_thing]!")) dropped_thing.throw_at(get_edge_target_turf(parent, pick(GLOB.alldirs)), rand(1, 10), rand(1, 10)) - if(isliving(dropped_thing)) + else if(isliving(dropped_thing)) var/mob/living/fallen_mob = dropped_thing fallen_mob.notransform = FALSE if (fallen_mob.stat != DEAD) @@ -237,27 +217,8 @@ GLOBAL_LIST_INIT(chasm_storage, list()) SIGNAL_HANDLER UnregisterSignal(gone, COMSIG_LIVING_REVIVE) -#define CHASM_TRAIT "chasm trait" - -/** - * Called if something comes back to life inside the pit. Expected sources are badmins and changelings. - * Ethereals should take enough damage to be smashed and not revive. - * - * Arguments - * * escapee - Lucky guy who just came back to life at the bottom of a hole. - */ -/datum/component/chasm/proc/on_revive(mob/living/escapee) - SIGNAL_HANDLER - var/atom/parent = src.parent - parent.visible_message(span_boldwarning("After a long climb, [escapee] leaps out of [parent]!")) - ADD_TRAIT(escapee, TRAIT_MOVE_FLYING, CHASM_TRAIT) //Otherwise they instantly fall back in - escapee.forceMove(get_turf(parent)) - escapee.throw_at(get_edge_target_turf(parent, pick(GLOB.alldirs)), rand(1, 10), rand(1, 10)) - REMOVE_TRAIT(escapee, TRAIT_MOVE_FLYING, CHASM_TRAIT) - escapee.Paralyze(20 SECONDS, TRUE) - UnregisterSignal(escapee, COMSIG_LIVING_REVIVE) - -#undef CHASM_TRAIT +///Global list needed to let fishermen with a rescue hook fish fallen mobs from any place +GLOBAL_LIST_EMPTY(chasm_fallen_mobs) /** * An abstract object which is basically just a bag that the chasm puts people inside @@ -271,3 +232,40 @@ GLOBAL_LIST_INIT(chasm_storage, list()) /obj/effect/abstract/chasm_storage/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_SECLUDED_LOCATION, INNATE_TRAIT) + +/obj/effect/abstract/chasm_storage/Entered(atom/movable/arrived) + . = ..() + if (isliving(arrived)) + RegisterSignal(arrived, COMSIG_LIVING_REVIVE, PROC_REF(on_revive)) + GLOB.chasm_fallen_mobs += arrived + +/obj/effect/abstract/chasm_storage/Exited(atom/movable/gone) + . = ..() + if (isliving(gone)) + UnregisterSignal(gone, COMSIG_LIVING_REVIVE) + GLOB.chasm_fallen_mobs -= gone + +#define CHASM_TRAIT "chasm trait" +/** + * Called if something comes back to life inside the pit. Expected sources are badmins and changelings. + * Ethereals should take enough damage to be smashed and not revive. + * Arguments + * escapee - Lucky guy who just came back to life at the bottom of a hole. + */ +/obj/effect/abstract/chasm_storage/proc/on_revive(mob/living/escapee) + SIGNAL_HANDLER + var/turf/turf = get_turf(src) + if(turf.GetComponent(/datum/component/chasm)) + turf.visible_message(span_boldwarning("After a long climb, [escapee] leaps out of [turf]!")) + else + playsound(turf, 'sound/effects/bang.ogg', 50, TRUE) + turf.visible_message(span_boldwarning("[escapee] busts through [turf], leaping out of the chasm below")) + turf.ScrapeAway(2, flags = CHANGETURF_INHERIT_AIR) + ADD_TRAIT(escapee, TRAIT_MOVE_FLYING, CHASM_TRAIT) //Otherwise they instantly fall back in + escapee.forceMove(turf) + escapee.throw_at(get_edge_target_turf(turf, pick(GLOB.alldirs)), rand(1, 10), rand(1, 10)) + REMOVE_TRAIT(escapee, TRAIT_MOVE_FLYING, CHASM_TRAIT) + escapee.Paralyze(20 SECONDS, TRUE) + UnregisterSignal(escapee, COMSIG_LIVING_REVIVE) + +#undef CHASM_TRAIT diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm index a43aef04906..0c8b5caabce 100644 --- a/code/datums/components/fishing_spot.dm +++ b/code/datums/components/fishing_spot.dm @@ -51,14 +51,6 @@ /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_REF(fishing_completed)) + var/datum/fishing_challenge/challenge = new(src, result, rod, user) + fish_source.pre_challenge_started(rod, user) challenge.start(user) - -/datum/component/fishing_spot/proc/fishing_completed(datum/fishing_challenge/source, mob/user, success, perfect) - if(success) - var/obj/item/fish/caught = source.reward_path - user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = initial(caught.name)) - fish_source.dispense_reward(source.reward_path, user) diff --git a/code/datums/elements/skill_reward.dm b/code/datums/elements/skill_reward.dm new file mode 100644 index 00000000000..ac80c53449d --- /dev/null +++ b/code/datums/elements/skill_reward.dm @@ -0,0 +1,48 @@ +///An element that forbids mobs without a required skill level from equipping the item. +/datum/element/skill_reward + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + ///The required skill the user has to have to equip the item. + var/associated_skill + +/datum/element/skill_reward/Attach(datum/target, associated_skill) + . = ..() + if(!isitem(target)) + return ELEMENT_INCOMPATIBLE + + src.associated_skill = associated_skill + RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + RegisterSignal(target, COMSIG_ITEM_POST_EQUIPPED, PROC_REF(drop_if_unworthy)) + +/datum/element/skill_reward/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += span_notice("You notice a powerful aura about this item, suggesting that only the truly experienced may wield it.") + +/datum/element/skill_reward/proc/on_attack_hand(datum/source, mob/living/user, list/modifiers) + SIGNAL_HANDLER + if(!LAZYACCESS(modifiers, CTRL_CLICK) && !check_equippable(user)) //Allows other players to drag it around at least. + to_chat(user, span_warning("You feel completely and utterly unworthy to even touch \the [source].")) + return COMPONENT_CANCEL_ATTACK_CHAIN + +///We check if the item can be equipped, otherwise we drop it. +/datum/element/skill_reward/proc/drop_if_unworthy(datum/source, mob/living/user) + SIGNAL_HANDLER + if(check_equippable(user) | !(source in user.get_equipped_items(TRUE))) + return + to_chat(user, span_warning("You feel completely and utterly unworthy to even touch \the [source].")) + user.dropItemToGround(src, TRUE) + return COMPONENT_EQUIPPED_FAILED + +/datum/element/skill_reward/proc/check_equippable(mob/living/user) + return user.mind?.get_skill_level(associated_skill) >= SKILL_LEVEL_LEGENDARY + +/** + * Welp, the code is pretty much the same, except for one tiny detail, I suppose it's ok to make a subtype of this element. + * That tiny detail is that we don't check for skills, but if the player has played for thousands of hours. + */ +/datum/element/skill_reward/veteran + element_flags = NONE + +/datum/element/skill_reward/veteran/check_equippable(mob/user) + return user.client?.is_veteran() diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_items.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_items.dm index 9cfd47a4e38..a345aba0b81 100644 --- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_items.dm +++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_items.dm @@ -42,6 +42,21 @@ icon_file = 'icons/obj/clothing/belt_overlays.dmi' json_config = 'code/datums/greyscale/json_configs/screwdriver_worn.json' +/datum/greyscale_config/fish_analyzer_inhand_left + name = "Held Fish Analyzer, Left" + icon_file = 'icons/mob/inhands/items_lefthand.dmi' + json_config = 'code/datums/greyscale/json_configs/fish_analyzer_worn.json' + +/datum/greyscale_config/fish_analyzer_inhand_right + name = "Held Fish Analyzer, Right" + icon_file = 'icons/mob/inhands/items_righthand.dmi' + json_config = 'code/datums/greyscale/json_configs/fish_analyzer_worn.json' + +/datum/greyscale_config/fish_analyzer_worn + name = "Worn Fish Analyzer" + icon_file = 'icons/mob/clothing/belt.dmi' + json_config = 'code/datums/greyscale/json_configs/fish_analyzer_worn.json' + // // WEAPONS // diff --git a/code/datums/greyscale/json_configs/fish_analyzer_worn.json b/code/datums/greyscale/json_configs/fish_analyzer_worn.json new file mode 100644 index 00000000000..f2cc22bf1b5 --- /dev/null +++ b/code/datums/greyscale/json_configs/fish_analyzer_worn.json @@ -0,0 +1,15 @@ +{ + "fish_analyzer": [ + { + "type": "icon_state", + "icon_state": "fish_analyzer_outer", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "fish_analyzer_inner", + "blend_mode": "overlay" + } + ] +} \ No newline at end of file diff --git a/code/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm index a9c69bf223d..c0217d95774 100644 --- a/code/datums/helper_datums/teleport.dm +++ b/code/datums/helper_datums/teleport.dm @@ -131,23 +131,13 @@ return var/list/floor_gases = floor_gas_mixture.gases - var/list/gases_to_check = list(/datum/gas/oxygen, /datum/gas/nitrogen, /datum/gas/carbon_dioxide, /datum/gas/plasma) - var/trace_gases - for(var/id in floor_gases) - if(id in gases_to_check) - continue - trace_gases = TRUE - break - - // Can most things breathe? - if(trace_gases) - return - if(!(floor_gases[/datum/gas/oxygen] && floor_gases[/datum/gas/oxygen][MOLES] >= 16)) - return - if(floor_gases[/datum/gas/plasma]) - return - if(floor_gases[/datum/gas/carbon_dioxide] && floor_gases[/datum/gas/carbon_dioxide][MOLES] >= 10) - return + var/static/list/gases_to_check = list( + /datum/gas/oxygen = list(16, 100), + /datum/gas/nitrogen, + /datum/gas/carbon_dioxide = list(0, 10) + ) + if(!check_gases(floor_gases, gases_to_check)) + return FALSE // Aim for goldilocks temperatures and pressure if((floor_gas_mixture.temperature <= 270) || (floor_gas_mixture.temperature >= 360)) diff --git a/code/datums/skills/_skill.dm b/code/datums/skills/_skill.dm index c4ba1500e45..ec52ebd9e96 100644 --- a/code/datums/skills/_skill.dm +++ b/code/datums/skills/_skill.dm @@ -6,8 +6,8 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill)) var/desc = "the art of doing things" ///Dictionary of modifier type - list of modifiers (indexed by level). 7 entries in each list for all 7 skill levels. var/modifiers = list(SKILL_SPEED_MODIFIER = list(1, 1, 1, 1, 1, 1, 1)) //Dictionary of modifier type - list of modifiers (indexed by level). 7 entries in each list for all 7 skill levels. - ///List Path pointing to the skill cape reward that will appear when a user finishes leveling up a skill - var/skill_cape_path + ///List Path pointing to the skill item reward that will appear when a user finishes leveling up a skill + var/skill_item_path ///List associating different messages that appear on level up with different levels var/list/levelUpMessages = list() ///List associating different messages that appear on level up with different levels @@ -66,7 +66,7 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill)) /datum/skill/proc/try_skill_reward(datum/mind/mind, new_level) if (new_level != SKILL_LEVEL_LEGENDARY) return - if (!ispath(skill_cape_path)) + if (!ispath(skill_item_path)) to_chat(mind.current, span_nicegreen("My legendary [name] skill is quite impressive, though it seems the Professional [title] Association doesn't have any status symbols to commemorate my abilities with. I should let Centcom know of this travesty, maybe they can do something about it.")) return if (LAZYFIND(mind.skills_rewarded, src.type)) @@ -75,7 +75,7 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill)) podspawn(list( "target" = get_turf(mind.current), "style" = STYLE_BLUESPACE, - "spawn" = skill_cape_path, + "spawn" = skill_item_path, "delays" = list(POD_TRANSIT = 150, POD_FALLING = 4, POD_OPENING = 30, POD_LEAVING = 30) )) to_chat(mind.current, span_nicegreen("My legendary skill has attracted the attention of the Professional [title] Association. It seems they are sending me a status symbol to commemorate my abilities.")) diff --git a/code/datums/skills/cleaning.dm b/code/datums/skills/cleaning.dm index 6b797384d48..b3ccad7eb4c 100644 --- a/code/datums/skills/cleaning.dm +++ b/code/datums/skills/cleaning.dm @@ -3,4 +3,4 @@ title = "Cleaner" desc = "It’s not who I am underneath, but what I mop up that defines me." modifiers = list(SKILL_SPEED_MODIFIER = list(1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.36)) //speed also touches probability in using up a soap's charge - skill_cape_path = /obj/item/clothing/neck/cloak/skill_reward/cleaning + skill_item_path = /obj/item/clothing/neck/cloak/skill_reward/cleaning diff --git a/code/datums/skills/fishing.dm b/code/datums/skills/fishing.dm new file mode 100644 index 00000000000..d5ecff3c8f5 --- /dev/null +++ b/code/datums/skills/fishing.dm @@ -0,0 +1,10 @@ +/** + * skill associated with the fishing feature. It modifies the fishing minigame difficulty + * and is gained each time one is completed. + */ +/datum/skill/fishing + name = "Fishing" + title = "Fisher" + desc = "How empty and alone you are on this barren Earth." + modifiers = list(SKILL_VALUE_MODIFIER = list(1, 1, 0, -1, -2, -4, -6)) + skill_item_path = /obj/item/clothing/head/soft/fishing_hat diff --git a/code/datums/skills/gaming.dm b/code/datums/skills/gaming.dm index 794ab1fb864..75d90f3d8c0 100644 --- a/code/datums/skills/gaming.dm +++ b/code/datums/skills/gaming.dm @@ -4,7 +4,7 @@ desc = "My proficiency as a gamer. This helps me beat bosses with ease, powergame in Orion Trail, and makes me wanna slam some gamer fuel." modifiers = list(SKILL_PROBS_MODIFIER = list(0, 5, 10, 15, 15, 20, 25), SKILL_RANDS_MODIFIER = list(0, 1, 2, 3, 4, 5, 7)) - skill_cape_path = /obj/item/clothing/neck/cloak/skill_reward/gaming + skill_item_path = /obj/item/clothing/neck/cloak/skill_reward/gaming /datum/skill/gaming/New() . = ..() diff --git a/code/datums/skills/mining.dm b/code/datums/skills/mining.dm index e1b698a3d87..db79c2801a4 100644 --- a/code/datums/skills/mining.dm +++ b/code/datums/skills/mining.dm @@ -3,4 +3,4 @@ title = "Miner" desc = "A dwarf's biggest skill, after drinking." modifiers = list(SKILL_SPEED_MODIFIER = list(1, 0.95, 0.9, 0.85, 0.75, 0.6, 0.5),SKILL_PROBS_MODIFIER=list(10, 15, 20, 25, 30, 35, 40)) - skill_cape_path = /obj/item/clothing/neck/cloak/skill_reward/mining + skill_item_path = /obj/item/clothing/neck/cloak/skill_reward/mining diff --git a/code/datums/storage/subtypes/fish_case.dm b/code/datums/storage/subtypes/fish_case.dm new file mode 100644 index 00000000000..bc16f1607bf --- /dev/null +++ b/code/datums/storage/subtypes/fish_case.dm @@ -0,0 +1,33 @@ +/datum/storage/fish_case + max_slots = 1 + max_specific_storage = WEIGHT_CLASS_HUGE + can_hold_trait = TRAIT_FISH_CASE_COMPATIBILE + can_hold_description = "fish and aquarium equipment" + +/** + * Change the size of the storage item to match the inserted item's + * Because of that, we also check if conditions to keep it inside another storage or pockets are still met. + */ +/datum/storage/fish_case/handle_enter(obj/item/storage/fish_case/source, obj/item/arrived) + . = ..() + if(!istype(arrived) || arrived.w_class == source.w_class) + return + source.w_class = arrived.w_class + var/obj/item/resolve_parent = parent?.resolve() + if(resolve_parent?.item_flags & IN_STORAGE) + source.moveToNullspace() //temporarily remove source from its location so that attempt_insert may work correctly. + if(!resolve_parent.atom_storage?.attempt_insert(source, override = TRUE)) + source.forceMove(resolve_parent.drop_location()) + source.visible_message("[source] spills out of [resolve_parent] as it expands to hold [arrived]", vision_distance = 1) + else if(!isliving(source.loc)) + return + var/mob/living/living_loc = source.loc + var/equipped_slot = living_loc.get_slot_by_item(source) + if(equipped_slot & (ITEM_SLOT_RPOCKET|ITEM_SLOT_LPOCKET) && source.w_class > WEIGHT_CLASS_SMALL) + source.forceMove(living_loc.drop_location()) + to_chat(living_loc, "[source] drops out of your pockets as it expands to hold [arrived]") + +/datum/storage/fish_case/handle_exit(obj/item/storage/fish_case/source, obj/item/gone) + . = ..() + if(istype(gone)) + source.w_class = initial(source.w_class) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index bf1413dca16..dd1e2ee4ded 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -663,6 +663,22 @@ /obj/item/proc/on_found(mob/finder) return +/** + * Called after an item is placed in an equipment slot. Runs equipped(), then sends a signal. + * This should be called last or near-to-last, after all other inventory code stuff is handled. + * + * Arguments: + * * user is mob that equipped it + * * slot uses the slot_X defines found in setup.dm for items that can be placed in multiple slots + * * initial is used to indicate whether or not this is the initial equipment (job datums etc) or just a player doing it + */ +/obj/item/proc/on_equipped(mob/user, slot, initial = FALSE) + SHOULD_NOT_OVERRIDE(TRUE) + equipped(user, slot, initial) + if(SEND_SIGNAL(src, COMSIG_ITEM_POST_EQUIPPED, user, slot) && COMPONENT_EQUIPPED_FAILED) + return FALSE + return TRUE + /** * To be overwritten to only perform visual tasks; * this is directly called instead of `equipped` on visual-only features like human dummies equipping outfits. @@ -674,7 +690,7 @@ return /** - * Called after an item is placed in an equipment slot. + * Called by on_equipped. Don't call this directly, we want the ITEM_POST_EQUIPPED signal to be sent after everything else. * * Note that hands count as slots. * @@ -685,7 +701,7 @@ */ /obj/item/proc/equipped(mob/user, slot, initial = FALSE) SHOULD_CALL_PARENT(TRUE) - SEND_SIGNAL(user, COMSIG_HUMAN_EQUIPPING_ITEM, src, slot) + PROTECTED_PROC(TRUE) visual_equipped(user, slot, initial) SEND_SIGNAL(src, COMSIG_ITEM_EQUIPPED, user, slot) SEND_SIGNAL(user, COMSIG_MOB_EQUIPPED_ITEM, src, slot) diff --git a/code/game/objects/items/food/meatdish.dm b/code/game/objects/items/food/meatdish.dm index b08551fa5ae..ac2ec39974d 100644 --- a/code/game/objects/items/food/meatdish.dm +++ b/code/game/objects/items/food/meatdish.dm @@ -81,11 +81,11 @@ icon_state = "armorfish_fillet" food_reagents = list(/datum/reagent/consumable/nutriment/protein = 3) +///donkfish fillets. The yuck reagent is now added by the fish trait of the same name. /obj/item/food/fishmeat/donkfish name = "donkfillet" desc = "The dreaded donkfish fillet. No sane spaceman would eat this, and it does not get better when cooked." icon_state = "donkfillet" - food_reagents = list(/datum/reagent/yuck = 3) /obj/item/food/fishfingers name = "fish fingers" diff --git a/code/game/objects/items/storage/wallets.dm b/code/game/objects/items/storage/wallets.dm index 4294722be35..7a75ba88674 100644 --- a/code/game/objects/items/storage/wallets.dm +++ b/code/game/objects/items/storage/wallets.dm @@ -168,3 +168,12 @@ /obj/item/storage/wallet/random/PopulateContents() new /obj/item/holochip(src, rand(5, 30)) new /obj/effect/spawner/random/entertainment/wallet_storage(src) + +///Used by the toilet fish source. +/obj/item/storage/wallet/money + desc = "It can hold a few small and personal things. This one reeks of toilet water." + +/obj/item/storage/wallet/money/PopulateContents() + for(var/iteration in 1 to pick(3, 4)) + new /obj/item/holochip(src, rand(50, 450)) + diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm index b0ca7679915..dc95c046456 100644 --- a/code/game/objects/structures/watercloset.dm +++ b/code/game/objects/structures/watercloset.dm @@ -16,7 +16,8 @@ . = ..() open = round(rand(0, 1)) update_appearance() - + if(mapload && SSmapping.level_trait(z, ZTRAIT_STATION)) + AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_TOILET) /obj/structure/toilet/attack_hand(mob/living/user, list/modifiers) . = ..() @@ -120,8 +121,9 @@ return w_items += I.w_class to_chat(user, span_notice("You carefully place [I] into the cistern.")) + return - else if(is_reagent_container(I) && !user.combat_mode) + if(is_reagent_container(I) && !user.combat_mode) if (!open) return if(istype(I, /obj/item/food/monkeycube)) diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm index 6f734763e5c..f9f5d5c8fbb 100644 --- a/code/game/turfs/open/lava.dm +++ b/code/game/turfs/open/lava.dm @@ -38,10 +38,13 @@ var/mask_icon = 'icons/turf/floors.dmi' /// The icon state that covers the lava bits of our turf var/mask_state = "lava-lightmask" + /// The configuration key for the preset fishing spot for this type of turf. + var/fish_source_type = FISHING_SPOT_PRESET_LAVALAND_LAVA /turf/open/lava/Initialize(mapload) . = ..() - AddElement(/datum/element/lazy_fishing_spot, FISHING_SPOT_PRESET_LAVALAND_LAVA) + if(fish_source_type) + AddElement(/datum/element/lazy_fishing_spot, fish_source_type) refresh_light() if(!smoothing_flags) update_appearance() @@ -342,6 +345,7 @@ icon_state = "liquidplasma" initial_gas_mix = BURNING_COLD baseturfs = /turf/open/lava/plasma + fish_source_type = FISHING_SPOT_PRESET_ICEMOON_PLASMA light_range = 3 light_power = 0.75 @@ -423,8 +427,10 @@ initial_gas_mix = OPENTURF_DEFAULT_ATMOS baseturfs = /turf/open/lava/plasma/mafia slowdown = 0 + fish_source_type = null //basketball specific lava (normal atmos, no slowdown) /turf/open/lava/smooth/basketball initial_gas_mix = OPENTURF_DEFAULT_ATMOS slowdown = 0 + fish_source_type = null diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm index 56e9c757a2e..6fc900ce183 100644 --- a/code/modules/cargo/goodies.dm +++ b/code/modules/cargo/goodies.dm @@ -243,6 +243,12 @@ cost = PAYCHECK_CREW contains = list(/obj/item/bait_can/worm/premium) +/datum/supply_pack/goody/telescopic_fishing_rod + name = "Telescopic Fishing Rod" + desc = "A collapsible fishing rod that can fit within a backpack." + cost = PAYCHECK_CREW * 8 + contains = list(/obj/item/fishing_rod/telescopic) + /datum/supply_pack/goody/coffee_mug name = "Coffee Mug" desc = "A bog standard coffee mug, for drinking coffee." diff --git a/code/modules/cargo/markets/market_items/misc.dm b/code/modules/cargo/markets/market_items/misc.dm index d0ba6b26e9f..937bbc33232 100644 --- a/code/modules/cargo/markets/market_items/misc.dm +++ b/code/modules/cargo/markets/market_items/misc.dm @@ -95,6 +95,13 @@ stock_max = 3 availability_prob = 50 +/datum/market_item/misc/jawed_hook + name = "Jawed Fishing Hook" + desc = "The thing ya use if y'are strugglin' with fishes. Just rememeber to whoop yer rod before it's too late, 'cause this thing's gonna hurt them like an Arkansas toothpick." + price_min = CARGO_CRATE_VALUE * 0.75 + price_max = CARGO_CRATE_VALUE * 2 + stock_max = 3 + availability_prob = 70 /datum/market_item/misc/v8_engine name = "Genuine V8 Engine (Perserved)" @@ -104,3 +111,13 @@ price_max = CARGO_CRATE_VALUE * 6 stock_max = 1 availability_prob = 15 + +/datum/market_item/misc/fish + name = "Fish" + desc = "Fish! Fresh fish! Fish you can cut, grind and even keep in aquarium if you want to! Get some before the next fight at my village breaks out!" + price_min = PAYCHECK_CREW * 0.5 + price_max = PAYCHECK_CREW * 1.2 + item = /obj/item/storage/fish_case/blackmarket + stock_min = 3 + stock_max = 8 + availability_prob = 90 diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm index 14ff4643a6d..73f958c0144 100644 --- a/code/modules/cargo/packs/general.dm +++ b/code/modules/cargo/packs/general.dm @@ -67,6 +67,14 @@ cost = CARGO_CRATE_VALUE * 2 contains = list(/obj/item/storage/fish_case/tiziran = 2) crate_name = "tiziran fish crate" + +/datum/supply_pack/misc/fish_analyzers + name = "Fish Analyzers" + desc = "A pack containing three analyzers to monitor fish's status and traits with." + cost = CARGO_CRATE_VALUE * 2.5 + contains = list(/obj/item/fish_analyzer = 3) + crate_name = "fish analyzers crate" + /* SKYRAT REMOVAL /datum/supply_pack/misc/bicycle name = "Bicycle" @@ -76,6 +84,7 @@ crate_name = "bicycle crate" crate_type = /obj/structure/closet/crate/large SKYRAT REMOVAL END */ + /datum/supply_pack/misc/bigband name = "Big Band Instrument Collection" desc = "Get your sad station movin' and groovin' with this fine collection! \ diff --git a/code/modules/clothing/head/soft_caps.dm b/code/modules/clothing/head/soft_caps.dm index 33119b598be..08562481a44 100644 --- a/code/modules/clothing/head/soft_caps.dm +++ b/code/modules/clothing/head/soft_caps.dm @@ -6,13 +6,14 @@ icon_state = "cargosoft" inhand_icon_state = "greyscale_softcap" //todo wip var/soft_type = "cargo" + var/soft_suffix = "soft" dog_fashion = /datum/dog_fashion/head/cargo_tech var/flipped = FALSE /obj/item/clothing/head/soft/dropped() - icon_state = "[soft_type]soft" + icon_state = "[soft_type][soft_suffix]" flipped = FALSE ..() @@ -33,10 +34,10 @@ if(!user.incapacitated()) flipped = !flipped if(flipped) - icon_state = "[soft_type]soft_flipped" + icon_state = "[soft_type][soft_suffix]_flipped" to_chat(user, span_notice("You flip the hat backwards.")) else - icon_state = "[soft_type]soft" + icon_state = "[soft_type][soft_suffix]" to_chat(user, span_notice("You flip the hat back in normal position.")) usr.update_worn_head() //so our mob-overlays update @@ -139,3 +140,24 @@ icon_state = "paramedicsoft" soft_type = "paramedic" dog_fashion = null + +/obj/item/clothing/head/soft/fishing_hat + name = "legendary fishing hat" + desc = "An ancient relic of a bygone era of bountiful catches and endless rivers. Printed on the front is a poem:\n\ + Women Fear Me\n\ + Fish Fear Me\n\ + Men Turn Their Eyes Away From Me\n\ + As I Walk No Beast Dares Make A Sound In My Presence\n\ + I Am Alone On This Barren Earth." + icon_state = "fishing_hat" + soft_type = "fishing_hat" + inhand_icon_state = "fishing_hat" + soft_suffix = null + worn_y_offset = 5 + clothing_flags = SNUG_FIT + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE + dog_fashion = null + +/obj/item/clothing/head/soft/fishing_hat/Initialize(mapload) + . = ..() + AddElement(/datum/element/skill_reward, /datum/skill/fishing) diff --git a/code/modules/clothing/suits/cloaks.dm b/code/modules/clothing/suits/cloaks.dm index b061d4cb298..5d31ee29e4a 100644 --- a/code/modules/clothing/suits/cloaks.dm +++ b/code/modules/clothing/suits/cloaks.dm @@ -55,31 +55,12 @@ /obj/item/clothing/neck/cloak/skill_reward var/associated_skill_path = /datum/skill + var/element_type = /datum/element/skill_reward resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE -/obj/item/clothing/neck/cloak/skill_reward/examine(mob/user) +/obj/item/clothing/neck/cloak/skill_reward/Initialize(mapload) . = ..() - . += span_notice("You notice a powerful aura about this cloak, suggesting that only the truly experienced may wield it.") - -/obj/item/clothing/neck/cloak/skill_reward/proc/check_wearable(mob/user) - return user.mind?.get_skill_level(associated_skill_path) >= SKILL_LEVEL_LEGENDARY - -/obj/item/clothing/neck/cloak/skill_reward/proc/unworthy_unequip(mob/user) - to_chat(user, span_warning("You feel completely and utterly unworthy to even touch \the [src].")) - var/hand_index = user.get_held_index_of_item(src) - if (hand_index) - user.dropItemToGround(src, TRUE) - return FALSE - -/obj/item/clothing/neck/cloak/skill_reward/equipped(mob/user, slot) - if (!check_wearable(user)) - unworthy_unequip(user) - return ..() - -/obj/item/clothing/neck/cloak/skill_reward/attack_hand(mob/user, list/modifiers) - if (!check_wearable(user)) - unworthy_unequip(user) - return ..() + AddElement(element_type, associated_skill_path) /obj/item/clothing/neck/cloak/skill_reward/gaming name = "legendary gamer's cloak" @@ -103,6 +84,5 @@ name = "legendary veteran's cloak" desc = "Worn by the wisest of veteran employees, this legendary cloak is only attainable by maintaining a living employment agreement with Nanotrasen for over five thousand hours. This status symbol represents a being is better than you in nearly every quantifiable way, simple as that." icon_state = "playercloak" + element_type = /datum/element/skill_reward/veteran -/obj/item/clothing/neck/cloak/skill_reward/playing/check_wearable(mob/user) - return user.client?.is_veteran() diff --git a/code/modules/fishing/aquarium/aquarium.dm b/code/modules/fishing/aquarium/aquarium.dm index 56d3e1ca805..8d6baa732e5 100644 --- a/code/modules/fishing/aquarium/aquarium.dm +++ b/code/modules/fishing/aquarium/aquarium.dm @@ -37,22 +37,39 @@ ///Current layers in use by aquarium contents var/list/used_layers = list() - /// /obj/item/fish in the aquarium - does not include things with aquarium visuals that are not fish - var/list/tracked_fish = list() + /// /obj/item/fish in the aquarium, sorted by type - does not include things with aquarium visuals that are not fish + var/list/tracked_fish_by_type /obj/structure/aquarium/Initialize(mapload) . = ..() update_appearance() - RegisterSignal(src,COMSIG_ATOM_ATTACKBY, PROC_REF(feed_feedback)) + RegisterSignal(src, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(track_if_fish)) + AddElement(/datum/element/relay_attackers) + RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + +/obj/structure/aquarium/proc/track_if_fish(atom/source, atom/initialized) + SIGNAL_HANDLER + if(isfish(initialized)) + LAZYADDASSOCLIST(tracked_fish_by_type, initialized.type, initialized) /obj/structure/aquarium/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) . = ..() - if(istype(arrived,/obj/item/fish)) - tracked_fish += arrived + if(isfish(arrived)) + LAZYADDASSOCLIST(tracked_fish_by_type, arrived.type, arrived) /obj/structure/aquarium/Exited(atom/movable/gone, direction) . = ..() - tracked_fish -= gone + LAZYREMOVEASSOC(tracked_fish_by_type, gone.type, gone) + +/// Returns tracked_fish_by_type but flattened and without the items in the blacklist, also shuffled if shuffle is TRUE. +/obj/structure/aquarium/proc/get_fishes(shuffle = FALSE, blacklist) + . = list() + for(var/fish_type in tracked_fish_by_type) + . += tracked_fish_by_type[fish_type] + . -= blacklist + if(shuffle) + . = shuffle(.) + return . /obj/structure/aquarium/proc/request_layer(layer_type) /** @@ -111,9 +128,9 @@ default_unfasten_wrench(user, tool) return TOOL_ACT_TOOLTYPE_SUCCESS -/obj/structure/aquarium/attackby(obj/item/I, mob/living/user, params) +/obj/structure/aquarium/attackby(obj/item/item, mob/living/user, params) if(broken) - var/obj/item/stack/sheet/glass/glass = I + var/obj/item/stack/sheet/glass/glass = item if(istype(glass)) if(glass.get_amount() < 2) to_chat(user, span_warning("You need two glass sheets to fix the case!")) @@ -126,20 +143,27 @@ update_appearance() return TRUE else - var/datum/component/aquarium_content/content_component = I.GetComponent(/datum/component/aquarium_content) - if(content_component && content_component.is_ready_to_insert(src)) - if(user.transferItemToLoc(I,src)) - update_appearance() - return TRUE - else - return ..() + var/datum/component/aquarium_content/content_component = item.GetComponent(/datum/component/aquarium_content) + if(content_component && content_component.is_ready_to_insert(src) && user.transferItemToLoc(item, src)) + update_appearance() + return TRUE + + if(istype(item, /obj/item/fish_feed)) + if(!item.reagents.total_volume) + to_chat(user, span_warning("[item] is empty.")) + return TRUE + var/list/fishes = get_fishes() + for(var/obj/item/fish/fish as anything in fishes) + fish.feed(item.reagents) + to_chat(user, span_notice("You feed the fish.")) + return TRUE return ..() -/obj/structure/aquarium/proc/feed_feedback(datum/source, obj/item/thing, mob/user, params) - SIGNAL_HANDLER - if(istype(thing, /obj/item/fish_feed)) - to_chat(user,span_notice("You feed the fish.")) - return NONE +/obj/structure/aquarium/proc/on_attacked(datum/source, mob/attacker, attack_flags) + var/list/fishes = get_fishes() + //I wish this were an aquarium signal, but the aquarium_content component got in the way. + for(var/obj/item/fish/fish as anything in fishes) + SEND_SIGNAL(fish, COMSIG_FISH_STIRRED) /obj/structure/aquarium/interact(mob/user) if(!broken && user.pulling && isliving(user.pulling)) @@ -176,6 +200,7 @@ if(do_after(user, 5 SECONDS, target = src)) var/alive_fish = 0 var/dead_fish = 0 + var/list/tracked_fish = get_fishes() for(var/obj/item/fish/fish in tracked_fish) if(fish.status == FISH_ALIVE) alive_fish++ diff --git a/code/modules/fishing/aquarium/aquarium_kit.dm b/code/modules/fishing/aquarium/aquarium_kit.dm index 1522e212f92..2279b1c4ad3 100644 --- a/code/modules/fishing/aquarium/aquarium_kit.dm +++ b/code/modules/fishing/aquarium/aquarium_kit.dm @@ -1,7 +1,7 @@ ///Fish feed can /obj/item/fish_feed name = "fish feed can" - desc = "Autogenerates nutritious fish feed based on sample inside." + desc = "A refillable can that dispenses nutritious fish feed." icon = 'icons/obj/aquarium.dmi' icon_state = "fish_feed" w_class = WEIGHT_CLASS_TINY @@ -9,57 +9,85 @@ /obj/item/fish_feed/Initialize(mapload) . = ..() create_reagents(5, OPENCONTAINER) - reagents.add_reagent(/datum/reagent/consumable/nutriment, 1) //Default fish diet + reagents.add_reagent(/datum/reagent/consumable/nutriment, 2.5) //Default fish diet -///Stasis fish case container for moving fish between aquariums safely. +/** + * Stasis fish case container for moving fish between aquariums safely. + * Their w_class scales with that of the fish inside it. + * Most subtypes of this also start with a fish already inside. + */ /obj/item/storage/fish_case name = "stasis fish case" - desc = "A small case keeping the fish inside in stasis." + desc = "A resizable case keeping the fish inside in stasis." icon = 'icons/obj/storage/case.dmi' icon_state = "fishbox" inhand_icon_state = "syringe_kit" lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' + storage_type = /datum/storage/fish_case /obj/item/storage/fish_case/Initialize(mapload) ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC) // Before populate so fish instatiates in ready container already - . = ..() + return ..() - create_storage(max_slots = 1) - atom_storage.can_hold_trait = TRAIT_FISH_CASE_COMPATIBILE - atom_storage.can_hold_description = "fish and aquarium equipment" +/obj/item/storage/fish_case/PopulateContents() + var/fish_type = get_fish_type() + if(fish_type) + var/obj/item/fish/spawned_fish = new fish_type(null) + spawned_fish.forceMove(src) // trigger storage.handle_entered -///Fish case with single random fish inside. -/obj/item/storage/fish_case/random/PopulateContents() - . = ..() - var/fish_type = select_fish_type() - new fish_type(src) +/obj/item/storage/fish_case/proc/get_fish_type() + return -/obj/item/storage/fish_case/random/proc/select_fish_type() - return random_fish_type() +/obj/item/storage/fish_case/random -/obj/item/storage/fish_case/random/freshwater/select_fish_type() - return random_fish_type(required_fluid=AQUARIUM_FLUID_FRESHWATER) + var/fluid_type -/obj/item/storage/fish_case/random/saltwater/select_fish_type() - return random_fish_type(required_fluid=AQUARIUM_FLUID_SALTWATER) +/obj/item/storage/fish_case/random/get_fish_type() + return random_fish_type(required_fluid = fluid_type) + +/obj/item/storage/fish_case/random/freshwater + fluid_type = AQUARIUM_FLUID_FRESHWATER + +/obj/item/storage/fish_case/random/saltwater + fluid_type = AQUARIUM_FLUID_SALTWATER /obj/item/storage/fish_case/syndicate name = "ominous fish case" -/obj/item/storage/fish_case/syndicate/PopulateContents() - . = ..() - var/fish_type = pick(/obj/item/fish/donkfish, /obj/item/fish/emulsijack) - new fish_type(src) +/obj/item/storage/fish_case/syndicate/get_fish_type() + return pick(/obj/item/fish/donkfish, /obj/item/fish/emulsijack) /obj/item/storage/fish_case/tiziran name = "imported fish case" -/obj/item/storage/fish_case/tiziran/PopulateContents() +/obj/item/storage/fish_case/tiziran/get_fish_type() + return pick(/obj/item/fish/dwarf_moonfish, /obj/item/fish/gunner_jellyfish, /obj/item/fish/needlefish, /obj/item/fish/armorfish) + +///Subtype bought from the blackmarket at a gratuitously cheap price. The catch? The fish inside it is dead. +/obj/item/storage/fish_case/blackmarket + name = "ominous fish case" + desc = "A resizable case keeping the fish inside in stasis. This one holds a faint cadaverine smell." + +/obj/item/storage/fish_case/blackmarket/get_fish_type() + var/static/list/weighted_list = list( + /obj/item/fish/boned = 1, + /obj/item/fish/clownfish/lube = 3, + /obj/item/fish/emulsijack = 1, + /obj/item/fish/sludgefish/purple = 1, + /obj/item/fish/pufferfish = 3, + /obj/item/fish/slimefish = 2, + /obj/item/fish/ratfish = 2, + /obj/item/fish/chasm_crab/ice = 2, + /obj/item/fish/chasm_crab = 2, + ) + return pick_weight(weighted_list) + +/obj/item/storage/fish_cas/blackmarket/Initialize(mapload) . = ..() - 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) + for(var/obj/item/fish/fish as anything in contents) + fish.set_status(FISH_DEAD) /obj/item/aquarium_kit name = "DIY Aquarium Construction Kit" @@ -72,7 +100,6 @@ . = ..() to_chat(user,span_notice("There's instruction and tools necessary to build aquarium inside. All you need is to start crafting.")) - /obj/item/aquarium_prop name = "generic aquarium prop" desc = "very boring" diff --git a/code/modules/fishing/aquarium/fish_analyzer.dm b/code/modules/fishing/aquarium/fish_analyzer.dm new file mode 100644 index 00000000000..c7feab860ca --- /dev/null +++ b/code/modules/fishing/aquarium/fish_analyzer.dm @@ -0,0 +1,241 @@ +///An item that can be used to gather information on the fish, such as but not limited to: health, hunger and traits. +/obj/item/fish_analyzer + name = "fish analyzer" + icon = 'icons/obj/device.dmi' + icon_state = "fish_analyzer_map" + base_icon_state = "fish_analyzer" + inhand_icon_state = "fish_analyzer" + worn_icon_state = "fish_analyzer" + desc = "A fish-shaped scanner used to monitor fish's status and evolutionary traits." + flags_1 = CONDUCT_1 + item_flags = NOBLUDGEON + slot_flags = ITEM_SLOT_BELT + throwforce = 3 + w_class = WEIGHT_CLASS_TINY + throw_speed = 3 + throw_range = 7 + custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT *2) + greyscale_config_inhand_left = /datum/greyscale_config/fish_analyzer_inhand_left + greyscale_config_inhand_right = /datum/greyscale_config/fish_analyzer_inhand_right + greyscale_config_worn = /datum/greyscale_config/fish_analyzer_worn + ///The color of the case. Used by grayscale configs and update_overlays() + var/case_color + /** + * The radial menu shown when analyzing aquariums. Having a persistent one allows us + * to update it whenever fish come and go, and is also required since we have a select callback + * used to check right clicks for scanning traits instead of status. + */ + var/datum/radial_menu/persistent/fish_menu + /// A cached list of the current choices for the aforedefined radial menu. + var/list/radial_choices + +/obj/item/fish_analyzer/Initialize(mapload) + case_color = rgb(rand(16, 255), rand(16, 255), rand(16, 255)) + set_greyscale(colors = list(case_color)) + . = ..() + register_item_context() + update_appearance() + +/obj/item/fish_analyzer/Destroy() + if(fish_menu) + QDEL_NULL(fish_menu) + radial_choices = null + return ..() + +/obj/item/fish_analyzer/update_icon_state() + . = ..() + icon_state = base_icon_state + +/obj/item/fish_analyzer/update_overlays() + . = ..() + var/mutable_appearance/case = mutable_appearance(icon, "fish_analyzer_case") + case.color = case_color + . += case + . += emissive_appearance(icon, "fish_analyzer_emissive", src) + +/obj/item/fish_analyzer/add_item_context(obj/item/source, list/context, atom/target) + if (isfish(target)) + context[SCREENTIP_CONTEXT_LMB] = "Analyze status" + context[SCREENTIP_CONTEXT_RMB] = "Analyze traits" + return CONTEXTUAL_SCREENTIP_SET + else if(isaquarium(target)) + context[SCREENTIP_CONTEXT_LMB] = "Open radial menu" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/obj/item/fish_analyzer/afterattack(atom/target, mob/user, proximity) + . = ..() + if(!proximity || !user.can_read(src) || user.is_blind()) + return + + if(isfish(target)) + balloon_alert(user, "analyzing stats") + + user.visible_message(span_notice("[user] analyzes [target]."), span_notice("You analyze [target].")) + analyze_status(target, user) + else if(istype(target, /obj/structure/aquarium)) + scan_aquarium(target, user) + + +/obj/item/fish_analyzer/afterattack_secondary(atom/target, mob/user, proximity_flag, click_parameters) + if(!isfish(target)) + return + + if(!proximity_flag || !user.can_read(src) || user.is_blind()) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + balloon_alert(user, "analyzing traits") + + analyze_traits(target, user) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +///Instantiates the radial menu, populates the list of choices, shows it and register signals on the aquarium. +/obj/item/fish_analyzer/proc/scan_aquarium(obj/structure/aquarium/aquarium, mob/user) + if(fish_menu) + balloon_alert(user, "already scanning") + return + var/list/fishes = aquarium.get_fishes() + if(!length(fishes)) + balloon_alert(user, "no fish to scan") + return + radial_choices = list() + for(var/obj/item/fish/fish as anything in fishes) + radial_choices(fish) + fish_menu = show_radial_menu_persistent(user, aquarium, radial_choices, select_proc = CALLBACK(src, PROC_REF(choice_selected), user, aquarium), tooltips = TRUE, custom_check = CALLBACK(src, PROC_REF(can_select_fish), user, aquarium)) + RegisterSignal(aquarium, COMSIG_ATOM_ABSTRACT_ENTERED, PROC_REF(on_aquarium_entered)) + RegisterSignal(aquarium, COMSIG_ATOM_ABSTRACT_EXITED, PROC_REF(on_aquarium_exited)) + RegisterSignal(aquarium, COMSIG_QDELETING, PROC_REF(delete_radial)) + +///Instantiates a radial menu choice datum for the current fish and adds it to the list of choices. +/obj/item/fish_analyzer/proc/radial_choices(obj/item/fish/fish) + var/datum/radial_menu_choice/menu_choice = new + menu_choice.name = fish.name + menu_choice.info = "[fish.status == FISH_ALIVE ? "Alive" : "Dead"]\n[fish.size] cm\n[fish.weight] g\nProgenitors: [fish.progenitors]\nRight-click to analyze traits" + var/mutable_appearance/fish_appearance = new(fish) + fish_appearance.layer = FLOAT_LAYER + fish_appearance.plane = FLOAT_PLANE + menu_choice.image = fish_appearance + radial_choices[fish] = menu_choice + +///Called when the user has selected a choice. If it's a right click, analyze the traits, else the status +/obj/item/fish_analyzer/proc/choice_selected(mob/user, obj/structure/aquarium/aquarium, obj/item/fish/choice, params) + if(!choice || !can_select_fish(user, aquarium)) + delete_radial(aquarium) + return + var/is_right_clicking = LAZYACCESS(params2list(params), RIGHT_CLICK) + user.visible_message(span_notice("[user] analyzes [choice] inside [aquarium]."), span_notice("You analyze [choice] inside [aquarium].")) + if(is_right_clicking) + analyze_traits(choice, user) + else + analyze_status(choice, user) + +///Whether the item should continue to show its radial menu or delete it. +/obj/item/fish_analyzer/proc/can_select_fish(mob/user, obj/structure/aquarium/aquarium) + if(!user.is_holding(src) || !user?.CanReach(aquarium) || IS_DEAD_OR_INCAP(user)) + delete_radial(aquarium) + return FALSE + return TRUE + +///Called when something enters the aquarium. If it's a fish, update the choices. +/obj/item/fish_analyzer/proc/on_aquarium_entered(obj/structure/aquarium/source, atom/movable/arrived) + SIGNAL_HANDLER + if(isfish(arrived)) + radial_choices(arrived) + fish_menu.change_choices(radial_choices, tooltips = TRUE, animate = TRUE) + +///Called when something exits the aquarium. If it's a fish, update the choices. +/obj/item/fish_analyzer/proc/on_aquarium_exited(obj/structure/aquarium/source, atom/movable/gone) + SIGNAL_HANDLER + if(!isfish(gone)) + return + radial_choices -= gone + if(!length(radial_choices)) + delete_radial(source) + return + fish_menu.change_choices(radial_choices, tooltips = TRUE, animate = TRUE) + +///Unregisters signals, delete the radial menu, unsets the choices. +/obj/item/fish_analyzer/proc/delete_radial(obj/structure/aquarium/source) + SIGNAL_HANDLER + UnregisterSignal(source, list(COMSIG_ATOM_ABSTRACT_EXITED, COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_QDELETING)) + QDEL_NULL(fish_menu) + radial_choices = null + +/** + * Called when a fish or a menu choice is left-clicked. + * This returns the fish's status, size, weight, feed type, hunger, breeding timeout. + */ +/obj/item/fish_analyzer/proc/analyze_status(obj/item/fish/fish, mob/user) + + // the final list of strings to render + var/render_list = list() + + var/fish_status = fish.status == FISH_DEAD ? span_alert("Deceased") : "[PERCENT(fish.health/initial(fish.health))]% healthy" + + render_list += "[span_info("Analyzing status for [fish]:")]\nOverrall status: [fish_status]\n" + render_list += "Size: [fish.size] cm - Weight: [fish.weight] g\n" + render_list += "Required feed type: [initial(fish.food.name)]\n" + render_list += "Safe temperature: [fish.required_temperature_min] - [fish.required_temperature_max]K" + if(isaquarium(fish.loc)) + var/obj/structure/aquarium/aquarium = fish.loc + if(!ISINRANGE(aquarium.fluid_temp, fish.required_temperature_min, fish.required_temperature_max)) + render_list += span_alert("(OUT OF RANGE)") + render_list += "\n" + render_list += "Safe fluid type: [fish.required_fluid_type]" + if(isaquarium(fish.loc)) + var/obj/structure/aquarium/aquarium = fish.loc + if(!compatible_fluid_type(fish.required_fluid_type, aquarium.fluid_type)) + render_list += span_alert("(IN UNSAFE FLUID)") + render_list += "" + + if(fish.status != FISH_DEAD) + render_list += "\n" + var/hunger = PERCENT(min((world.time - fish.last_feeding) / fish.feeding_frequency, 1)) + var/hunger_string = "[hunger]%" + switch(hunger) + if(0 to 60) + hunger_string = span_info(hunger_string) + if(60 to 90) + hunger_string = span_warning(hunger_string) + if(90 to 100) + hunger_string = span_alert(hunger_string) + render_list += "Hunger: [hunger_string]\n" + var/time_left = round(max(fish.breeding_wait - world.time, 0)/10) + render_list += "Time until it can breed: [time_left] seconds" + + to_chat(user, examine_block(jointext(render_list, "")), type = MESSAGE_TYPE_INFO) + +/** + * Called when a fish or a menu choice is left-clicked. + * This returns the fish's progenitors, traits and their inheritability. + */ +/obj/item/fish_analyzer/proc/analyze_traits(obj/item/fish/fish, mob/user) + + // the final list of strings to render + var/render_list = list() + + render_list += "[span_info("Analyzing traits for [fish]:")]\nProgenitor species: [fish.progenitors]\n" + + if(!length(fish.fish_traits)) + render_list += "This fish has no trait to speak of...\n" + else + render_list += "Traits:\n" + for(var/trait_type in fish.fish_traits) + var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] + var/tooltipped_trait = span_tooltip(trait.catalog_description, trait.name) + render_list += "[tooltipped_trait] - Inheritabilities: [trait.inheritability]% - [trait.diff_traits_inheritability]%\n" + + var/evolution_len = length(fish.evolution_types) + if(!evolution_len) + render_list += "This fish has no evolution to speak of..." + for(var/index in 1 to evolution_len) + var/datum/fish_evolution/evolution = GLOB.fish_evolutions[fish.evolution_types[index]] + var/evolution_name = evolution.name + var/evolution_tooltip = evolution.get_evolution_tooltip() + if(evolution_tooltip) + evolution_name = span_tooltip(evolution_tooltip, evolution_name) + render_list += "[evolution_name] - Base Probability: [evolution.probability]%" + if(index != evolution_len) + render_list += "\n" + + to_chat(user, examine_block(jointext(render_list, "")), type = MESSAGE_TYPE_INFO) diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm index b896b44ef5c..a5149b29e3d 100644 --- a/code/modules/fishing/fish/_fish.dm +++ b/code/modules/fishing/fish/_fish.dm @@ -4,8 +4,16 @@ desc = "very bland" icon = 'icons/obj/aquarium.dmi' icon_state = "bugfish" - - w_class = WEIGHT_CLASS_TINY + lefthand_file = 'icons/mob/inhands/fish_lefthand.dmi' + righthand_file = 'icons/mob/inhands/fish_righthand.dmi' + inhand_icon_state = "fish_normal" + force = 6 + attack_verb_continuous = list("slaps", "whacks") + attack_verb_simple = list("slap", "whack") + hitsound = 'sound/weapons/slap.ogg' + ///The grind results of the fish. They scale with the weight of the fish. + grind_results = list(/datum/reagent/blood = 20, /datum/reagent/consumable/liquidgibs = 5) + obj_flags = UNIQUE_RENAME /// Resulting width of aquarium visual icon - default size of "fish_greyscale" state var/sprite_width = 3 @@ -17,6 +25,11 @@ /// Original height of aquarium visual icon - used to calculate scaledown factor var/source_height = 32 + /** + * If present and it also has a dedicated icon state, this icon file will + * be used for in-aquarium visual for the fish instead of its icon + */ + var/dedicated_in_aquarium_icon /// If present this icon will be used for in-aquarium visual for the fish instead of icon_state var/dedicated_in_aquarium_icon_state @@ -31,7 +44,7 @@ var/required_temperature_max = MAX_AQUARIUM_TEMP /// What type of reagent this fish needs to be fed. - var/food = /datum/reagent/consumable/nutriment + var/datum/reagent/food = /datum/reagent/consumable/nutriment /// How often the fish needs to be fed var/feeding_frequency = 5 MINUTES /// Time of last feedeing @@ -39,9 +52,15 @@ /// Fish status var/status = FISH_ALIVE + ///icon used when the fish is dead, ifset. + var/icon_state_dead + ///If this fish should do the flopping animation + var/do_flop_animation = TRUE /// Current fish health. Dies at 0. var/health = 100 + /// The message shown when the fish dies. + var/death_text = "%SRC dies." /// Should this fish type show in fish catalog var/show_in_catalog = TRUE @@ -52,13 +71,21 @@ /// Fish autogenerated from this behaviour will be processable into this var/fillet_type = /obj/item/food/fishmeat + /// number of fillets given by the fish. It scales with its size. + var/num_fillets = 1 /// Won't breed more than this amount in single aquarium. var/stable_population = 1 - /// Last time new fish was created - var/last_breeding + /// The time limit before new fish can be created + var/breeding_wait /// How long it takes to produce new fish var/breeding_timeout = 2 MINUTES + /// If set, the fish can also breed with these fishes types + var/list/compatible_types + /// A list of possible evolutions. If set, offsprings may be of a different, new fish type if conditions are met. + var/list/evolution_types + /// The species' name(s) of the parents of the fish. Shown by the fish analyzer. + var/progenitors var/flopping = FALSE @@ -66,8 +93,11 @@ // Fishing related properties - /// List of fishing trait types, these modify probabilty/difficulty depending on rod/user properties - var/list/fishing_traits = list() + /** + * List of fish trait types, these may modify probabilty/difficulty depending on rod/user properties + * or dictate how the fish behaves or some of its qualities. + */ + var/list/fish_traits = list() /// Fishing behaviour var/fish_ai_type = FISH_AI_DUMB @@ -87,32 +117,44 @@ */ var/list/disliked_bait = list() - /// Size in centimeters + /// Size in centimeters. Item size class scales with it. 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 + /// Weight in grams. number of fillets and grind results scale with it, just don't think too hard how someone could manage to fit a trout in a blender. var/weight = 1000 /// Average weight for this fish type in grams var/average_weight = 1000 - - -/obj/item/fish/Initialize(mapload) +/obj/item/fish/Initialize(mapload, apply_qualities = TRUE) . = ..() - if(fillet_type) - AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, 1, 5, screentip_verb = "Cut") AddComponent(/datum/component/aquarium_content, PROC_REF(get_aquarium_animation), list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED)) - RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, PROC_REF(on_temp_animation)) - RegisterSignal(src, COMSIG_ATOM_ON_LAZARUS_INJECTOR, PROC_REF(use_lazarus)) - check_environment_after_movement() + RegisterSignal(src, COMSIG_ATOM_ON_LAZARUS_INJECTOR, PROC_REF(use_lazarus)) + if(do_flop_animation) + RegisterSignal(src, COMSIG_ATOM_TEMPORARY_ANIMATION_START, PROC_REF(on_temp_animation)) + check_environment() if(status != FISH_DEAD) START_PROCESSING(SSobj, src) - size = average_size - weight = average_weight + //stops new fish from being able to reproduce right away. + breeding_wait = world.time + (breeding_timeout * NEW_FISH_BREEDING_TIMEOUT_MULT) + last_feeding = world.time - (feeding_frequency * NEW_FISH_LAST_FEEDING_MULT) + + if(apply_qualities) + apply_traits() //Make sure traits are applied before size and weight. + update_size_and_weight(first_run = TRUE) + progenitors = full_capitalize(name) //default value + + register_evolutions() + +/obj/item/fish/update_icon_state() + if(status == FISH_DEAD && icon_state_dead) + icon_state = icon_state_dead + else + icon_state = initial(icon_state) + return ..() /obj/item/fish/examine(mob/user) . = ..() @@ -120,18 +162,111 @@ . += 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)) +///Randomizes weight and size. +/obj/item/fish/proc/randomize_size_and_weight(avg_size = average_size, avg_weight = average_weight, deviation = 0.2, first_run = FALSE) + var/size_deviation = 0.2 * avg_size + var/new_size = round(max(1,gaussian(avg_size, size_deviation)), 1) - var/weight_deviation = 0.2 * average_weight - var/weight_mod = modifier * average_weight - weight = max(1,gaussian(average_weight + weight_mod, weight_deviation)) + var/weight_deviation = 0.2 * avg_weight + var/new_weight = round(max(1,gaussian(avg_weight, weight_deviation)), 1) + + update_size_and_weight(new_size, new_weight, first_run) + +///Updates weight and size, along with weight class, number of fillets you can get and grind results. +/obj/item/fish/proc/update_size_and_weight(new_size = average_size, new_weight = average_weight, first_run = FALSE) + if(!first_run && fillet_type) + RemoveElement(/datum/element/processable, TOOL_KNIFE, fillet_type, num_fillets, 0.5 SECONDS, screentip_verb = "Cut") + size = new_size + switch(size) + if(0 to FISH_SIZE_TINY_MAX) + w_class = WEIGHT_CLASS_TINY + inhand_icon_state = "fish_small" + if(FISH_SIZE_TINY_MAX to FISH_SIZE_SMALL_MAX) + inhand_icon_state = "fish_small" + w_class = WEIGHT_CLASS_SMALL + if(FISH_SIZE_SMALL_MAX to FISH_SIZE_NORMAL_MAX) + inhand_icon_state = "fish_normal" + w_class = WEIGHT_CLASS_NORMAL + if(FISH_SIZE_NORMAL_MAX to FISH_SIZE_BULKY_MAX) + inhand_icon_state = "fish_bulky" + w_class = WEIGHT_CLASS_BULKY + if(FISH_SIZE_BULKY_MAX to INFINITY) + inhand_icon_state = "fish_huge" + w_class = WEIGHT_CLASS_HUGE + if(fillet_type) + var/init_fillets = initial(num_fillets) + var/amount = max(round(init_fillets * size / FISH_FILLET_NUMBER_SIZE_DIVISOR, 1), 1) + num_fillets = amount + AddElement(/datum/element/processable, TOOL_KNIFE, fillet_type, num_fillets, 0.5 SECONDS, screentip_verb = "Cut") + + if(!first_run) + for(var/reagent_type in grind_results) + grind_results[reagent_type] /= FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1) + weight = new_weight + for(var/reagent_type in grind_results) + grind_results[reagent_type] *= FLOOR(weight/FISH_GRIND_RESULTS_WEIGHT_DIVISOR, 0.1) + +/** + * This proc has fish_traits list populated with fish_traits paths from three different lists: + * traits from x_traits and y_traits are compared, and inserted if conditions are met; + * traits from fixed_traits are inserted unconditionally. + * traits from removed_traits will be removed from the for loop. + * + * This proc should only be called if the fish was spawned with the apply_qualities arg set to FALSE + * and hasn't had inherited traits already. + */ +/obj/item/fish/proc/inherit_traits(list/x_traits, list/y_traits, list/fixed_traits, list/removed_traits) + + fish_traits = fixed_traits?.Copy() || list() + + var/list/same_traits = x_traits & y_traits + var/list/all_traits = (x_traits|y_traits)-removed_traits + /** + * Traits that the fish is guaranteed to inherit will be inherited, + * with the assertion that they're compatible anyway. + */ + for(var/trait_type in all_traits) + var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] + if(type in trait.guaranteed_inheritance_types) + fish_traits |= trait_type + all_traits -= trait_type + + ///Build a list of incompatible traits. Don't let any such trait pass onto the fish. + var/list/incompatible_traits = list() + for(var/trait_type in fish_traits) + var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] + incompatible_traits |= trait.incompatible_traits + /** + * shuffle the traits, so, in the case of incompatible traits, we don't have to choose which to discard. + * Instead we let the random numbers do it for us in a first come, first served basis. + */ + for(var/trait_type in shuffle(all_traits)) + if(trait_type in fish_traits) + continue //likely a fixed trait + if(trait_type in incompatible_traits) + continue + var/datum/fish_trait/trait = GLOB.fish_traits[trait_type] + if(length(fish_traits & trait.incompatible_traits)) + continue + if((trait_type in same_traits) ? prob(trait.inheritability) : prob(trait.diff_traits_inheritability)) + fish_traits |= trait_type + incompatible_traits |= trait.incompatible_traits + + apply_traits() + +/obj/item/fish/proc/apply_traits() + for(var/fish_trait_type in fish_traits) + var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait_type] + trait.apply_to_fish(src) + +/obj/item/fish/proc/register_evolutions() + for(var/evolution_type in evolution_types) + var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type] + evolution.register_fish(src) /obj/item/fish/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) . = ..() - check_environment_after_movement() + check_environment() /obj/item/fish/proc/enter_stasis() in_stasis = TRUE @@ -144,49 +279,35 @@ if(status != FISH_DEAD) START_PROCESSING(SSobj, src) -/obj/item/fish/proc/on_aquarium_insertion(obj/structure/aquarium) - if(isnull(last_feeding)) //Fish start fed. - last_feeding = world.time - RegisterSignal(aquarium, COMSIG_ATOM_EXITED, PROC_REF(aquarium_exited)) - RegisterSignal(aquarium, COMSIG_ATOM_ATTACKBY, PROC_REF(attack_reaction)) - -/obj/item/fish/proc/aquarium_exited(datum/source, atom/movable/gone, direction) - SIGNAL_HANDLER - if(src != gone) +///Feed the fishes with the contents of the fish feed +/obj/item/fish/proc/feed(datum/reagents/fed_reagents) + if(status != FISH_ALIVE) return - UnregisterSignal(source,list(COMSIG_ATOM_EXITED,COMSIG_ATOM_ATTACKBY)) - -/// Our aquarium is hit with stuff -/obj/item/fish/proc/attack_reaction(datum/source, obj/item/thing, mob/user, params) - SIGNAL_HANDLER - if(is_food(thing)) - on_feeding(thing.reagents) - return COMPONENT_NO_AFTERATTACK - else - //stirred effect - SEND_SIGNAL(src, COMSIG_FISH_STIRRED) - -/obj/item/fish/proc/is_food(obj/item/thing) - return istype(thing, /obj/item/fish_feed) - -/obj/item/fish/proc/on_feeding(datum/reagents/feed_reagents) - if(feed_reagents.has_reagent(food)) + var/fed_reagent_type + if(fed_reagents.remove_reagent(food, 0.1)) + fed_reagent_type = food last_feeding = world.time + else + var/datum/reagent/wrong_reagent = pick(fed_reagents.reagent_list) + fed_reagent_type = wrong_reagent.type + fed_reagents.remove_reagent(fed_reagent_type, 0.1) + SEND_SIGNAL(src, COMSIG_FISH_FED, fed_reagents, fed_reagent_type) -/obj/item/fish/proc/check_environment_after_movement() +/obj/item/fish/proc/check_environment(stasis_check = TRUE) if(QDELETED(src)) //we don't care anymore return - // Apply/remove stasis as needed - if(loc && HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE)) - enter_stasis() - else if(in_stasis) - exit_stasis() + if(stasis_check) + // Apply/remove stasis as needed + if(loc && HAS_TRAIT(loc, TRAIT_FISH_SAFE_STORAGE)) + enter_stasis() + else if(in_stasis) + exit_stasis() + + if(!do_flop_animation) + return // Do additional stuff - var/in_aquarium = istype(loc,/obj/structure/aquarium) - if(in_aquarium) - on_aquarium_insertion(loc) - + var/in_aquarium = isaquarium(loc) // Start flopping if outside of fish container var/should_be_flopping = status == FISH_ALIVE && loc && !HAS_TRAIT(loc,TRAIT_FISH_SAFE_STORAGE) && !in_aquarium @@ -203,22 +324,28 @@ if(ready_to_reproduce()) try_to_reproduce() + SEND_SIGNAL(src, COMSIG_FISH_LIFE, seconds_per_tick) + /obj/item/fish/proc/set_status(new_status) + if(status == new_status) + return switch(new_status) if(FISH_ALIVE) status = FISH_ALIVE health = initial(health) // since the fishe has been revived - start_flopping() + last_feeding = world.time //reset hunger + check_environment(FALSE) START_PROCESSING(SSobj, src) if(FISH_DEAD) status = FISH_DEAD STOP_PROCESSING(SSobj, src) stop_flopping() - var/message = span_notice("\The [name] dies.") - if(istype(loc,/obj/structure/aquarium)) + var/message = span_notice(replacetext(death_text, "%SRC", "[src]")) + if(isaquarium(loc)) loc.visible_message(message) else visible_message(message) + update_appearance() SEND_SIGNAL(src, COMSIG_FISH_STATUS_CHANGED) /obj/item/fish/proc/use_lazarus(datum/source, obj/item/lazarus_injector/injector, mob/user) @@ -243,16 +370,31 @@ /// Checks if our current environment lets us live. /obj/item/fish/proc/proper_environment() var/obj/structure/aquarium/aquarium = loc - if(!istype(aquarium)) - return FALSE + if(istype(aquarium)) + if(!compatible_fluid_type(required_fluid_type, aquarium.fluid_type)) + if(aquarium.fluid_type != AQUARIUM_FLUID_AIR || !HAS_TRAIT(src, TRAIT_FISH_AMPHIBIOUS)) + return FALSE + if(!ISINRANGE(aquarium.fluid_temp, required_temperature_min, required_temperature_max)) + return FALSE + return TRUE - if(required_fluid_type != AQUARIUM_FLUID_ANADROMOUS) - if(aquarium.fluid_type != required_fluid_type) - return FALSE - else - if(aquarium.fluid_type != AQUARIUM_FLUID_SALTWATER && aquarium.fluid_type != AQUARIUM_FLUID_FRESHWATER) - return FALSE - if(aquarium.fluid_temp < required_temperature_min || aquarium.fluid_temp > required_temperature_max) + if(required_fluid_type != AQUARIUM_FLUID_AIR && !HAS_TRAIT(src, TRAIT_FISH_AMPHIBIOUS)) + return FALSE + var/datum/gas_mixture/mixture = loc.return_air() + if(!mixture) + return FALSE + var/static/list/gases_to_check = list( + /datum/gas/oxygen = list(12, 100), + /datum/gas/nitrogen, + /datum/gas/carbon_dioxide = list(0, 10), + /datum/gas/water_vapor, + ) + if(!check_gases(mixture.gases, gases_to_check)) + return FALSE + if(!ISINRANGE(mixture.temperature, required_temperature_min, required_temperature_max)) + return FALSE + var/pressure = mixture.return_pressure() + if((pressure <= 20) || (pressure >= 550)) return FALSE return TRUE @@ -272,37 +414,109 @@ set_status(FISH_DEAD) -/obj/item/fish/proc/ready_to_reproduce() +//Fish breeding stops if fish count exceeds this. +#define AQUARIUM_MAX_BREEDING_POPULATION 20 + +/obj/item/fish/proc/ready_to_reproduce(being_targetted = FALSE) var/obj/structure/aquarium/aquarium = loc if(!istype(aquarium)) return FALSE - return aquarium.allow_breeding && health == initial(health) && stable_population > 1 && world.time - last_breeding >= breeding_timeout + if(being_targetted && HAS_TRAIT(src, TRAIT_FISH_NO_MATING)) + return FALSE + if(!being_targetted && length(aquarium.get_fishes()) >= AQUARIUM_MAX_BREEDING_POPULATION) + return FALSE + return aquarium.allow_breeding && health >= initial(health) * 0.8 && stable_population > 1 && world.time >= breeding_wait + +#undef AQUARIUM_MAX_BREEDING_POPULATION -//Fish breeding stops if fish count exceeds this. -#define AQUARIUM_MAX_BREEDING_POPULATION 20 /obj/item/fish/proc/try_to_reproduce() var/obj/structure/aquarium/aquarium = loc if(!istype(aquarium)) - return - if(length(aquarium.tracked_fish) >= AQUARIUM_MAX_BREEDING_POPULATION) //so aquariums full of fish don't need to do these expensive checks - return - var/list/other_fish_of_same_type = list() - for(var/obj/item/fish/fish_in_aquarium in aquarium) - if(fish_in_aquarium == src || fish_in_aquarium.type != type) - continue - other_fish_of_same_type += fish_in_aquarium - if(length(other_fish_of_same_type) >= stable_population) - return + return FALSE + var/obj/item/fish/second_fish - for(var/obj/item/fish/other_fish in other_fish_of_same_type) - if(other_fish.ready_to_reproduce()) - second_fish = other_fish - break - if(second_fish) - new type(loc) //could use child_type var - last_breeding = world.time - second_fish.last_breeding = world.time -#undef AQUARIUM_MAX_BREEDING_POPULATION + + /** + * Fishes with this trait cannot mate, but could still reproduce asexually, so don't early return. + * Also mating takes priority over that. + */ + if(!HAS_TRAIT(src, TRAIT_FISH_NO_MATING)) + var/list/available_fishes = list() + var/types_to_mate_with = aquarium.tracked_fish_by_type + if(!HAS_TRAIT(src, TRAIT_FISH_CROSSBREEDER)) + var/list/types_to_check = list(src) + if(compatible_types) + types_to_check |= compatible_types + types_to_mate_with = types_to_mate_with & types_to_check + + for(var/obj/item/fish/fish_type as anything in types_to_mate_with) + var/list/type_fishes = types_to_mate_with[fish_type] + if(length(type_fishes) >= initial(fish_type.stable_population)) + continue + available_fishes += type_fishes + + available_fishes -= src //no self-mating. + if(length(available_fishes)) + for(var/obj/item/fish/other_fish as anything in shuffle(available_fishes)) + if(other_fish.ready_to_reproduce(TRUE)) + second_fish = other_fish + break + + if(!second_fish && !HAS_TRAIT(src, TRAIT_FISH_SELF_REPRODUCE)) + return FALSE + + var/chosen_type + var/datum/fish_evolution/chosen_evolution + if(PERFORM_ALL_TESTS(fish_breeding) && second_fish && !length(evolution_types)) + chosen_type = second_fish.type + else + var/list/possible_evolutions = list() + for(var/evolution_type in evolution_types) + var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type] + if(evolution.check_conditions(src, second_fish, aquarium)) + possible_evolutions += evolution + if(second_fish?.evolution_types) + var/secondary_evolutions = (second_fish.evolution_types - evolution_types) + for(var/evolution_type in secondary_evolutions) + var/datum/fish_evolution/evolution = GLOB.fish_evolutions[evolution_type] + if(evolution.check_conditions(second_fish, src, aquarium)) + possible_evolutions += evolution + + if(length(possible_evolutions)) + chosen_evolution = pick(possible_evolutions) + chosen_type = chosen_evolution.new_fish_type + else if(second_fish) + if(length(aquarium.tracked_fish_by_type[type]) >= stable_population) + chosen_type = second_fish.type + else + chosen_type = pick(second_fish.type, type) + else + chosen_type = type + + return create_offspring(chosen_type, second_fish, chosen_evolution) + +/obj/item/fish/proc/create_offspring(chosen_type, obj/item/fish/partner, datum/fish_evolution/evolution) + var/obj/item/fish/new_fish = new chosen_type (loc, FALSE) + //Try to pass down compatible traits based on inheritability + new_fish.inherit_traits(fish_traits, partner?.fish_traits, evolution?.new_traits, evolution?.removed_traits) + + if(partner) + var/mean_size = (size + partner.size)/2 + var/mean_weight = (weight + partner.weight)/2 + new_fish.randomize_size_and_weight(mean_size, mean_weight, 0.3, TRUE) + partner.breeding_wait = world.time + breeding_timeout + else //Make a close of this fish. + new_fish.update_size_and_weight(size, weight, TRUE) + new_fish.progenitors = initial(name) + if(partner && type != partner.type) + var/string = "[initial(name)] - [initial(partner.name)]" + new_fish.progenitors = full_capitalize(string) + else + new_fish.progenitors = full_capitalize(initial(name)) + + breeding_wait = world.time + breeding_timeout + + return new_fish #define PAUSE_BETWEEN_PHASES 15 #define PAUSE_BETWEEN_FLOPS 2 @@ -345,9 +559,10 @@ /// Starts flopping animation /obj/item/fish/proc/start_flopping() - if(!flopping) //Requires update_transform/animate_wrappers to be less restrictive. - flopping = TRUE - flop_animation(src) + if(flopping) //Requires update_transform/animate_wrappers to be less restrictive. + return + flopping = TRUE + flop_animation(src) /// Stops flopping animation /obj/item/fish/proc/stop_flopping() @@ -374,11 +589,20 @@ var/chance_table = list() for(var/_fish_type in subtypesof(/obj/item/fish)) var/obj/item/fish/fish = _fish_type - if(required_fluid && initial(fish.required_fluid_type) != required_fluid) - continue + if(required_fluid) + var/init_fish_fluid_type = initial(fish.required_fluid_type) + if(!compatible_fluid_type(init_fish_fluid_type, required_fluid)) + continue if(initial(fish.available_in_random_cases) || !case_fish_only) chance_table[fish] = initial(fish.random_case_rarity) probability_table[argkey] = chance_table return pick_weight(probability_table[argkey]) - +/proc/compatible_fluid_type(fish_fluid_type, fluid_type) + switch(fish_fluid_type) + if(AQUARIUM_FLUID_ANY_WATER) + return fluid_type != AQUARIUM_FLUID_AIR + if(AQUARIUM_FLUID_ANADROMOUS) + return fluid_type == AQUARIUM_FLUID_SALTWATER || fluid_type == AQUARIUM_FLUID_FRESHWATER + else + return fish_fluid_type == fluid_type diff --git a/code/modules/fishing/fish/chasm_detritus.dm b/code/modules/fishing/fish/chasm_detritus.dm index 69105daeba4..a5102b97f64 100644 --- a/code/modules/fishing/fish/chasm_detritus.dm +++ b/code/modules/fishing/fish/chasm_detritus.dm @@ -40,15 +40,7 @@ if (prob(default_contents_chance)) create_default_object() return - - var/list/chasm_stuff = find_chasm_contents() - if (!chasm_stuff.len) - create_default_object() - return - - var/atom/movable/detritus = determine_detritus(chasm_stuff) - detritus.forceMove(get_turf(src)) - qdel(src) + RegisterSignal(src, COMSIG_ATOM_FISHING_REWARD, PROC_REF(find_chasm_contents)) /// Returns the chosen detritus from the given list of things to choose from /obj/item/chasm_detritus/proc/determine_detritus(list/chasm_stuff) @@ -60,19 +52,24 @@ new contents_type(get_turf(src)) qdel(src) -/// Returns a list of every object which is currently inside of a chasm. -/obj/item/chasm_detritus/proc/find_chasm_contents() - var/list/chasm_contents = list() - if (!GLOB.chasm_storage.len) - return chasm_contents +/// Returns an objected which is currently inside of a nearby chasm. +/obj/item/chasm_detritus/proc/find_chasm_contents(datum/source, turf/fishing_spot) + SIGNAL_HANDLER + var/list/chasm_contents = get_chasm_contents(fishing_spot) - var/list/chasm_storage_resolved = recursive_list_resolve(GLOB.chasm_storage) - for (var/obj/storage as anything in chasm_storage_resolved) + if (length(chasm_contents)) + create_default_object() + return + + var/atom/movable/detritus = determine_detritus(chasm_contents) + detritus.forceMove(get_turf(src)) + qdel(src) + +/obj/item/chasm_detritus/proc/get_chasm_contents(turf/fishing_spot) + . = list() + for (var/obj/effect/abstract/chasm_storage/storage in range(5, fishing_spot)) for (var/thing as anything in storage.contents) - chasm_contents += thing - - return chasm_contents - + . += thing /// Variant of the chasm detritus that allows for an easier time at fishing out /// bodies, and sometimes less desireable monsters too. @@ -81,21 +78,27 @@ /// contained in the `GLOB.chasm_storage` global list in `find_chasm_contents()`. var/chasm_storage_restricted_type = /obj - -/obj/item/chasm_detritus/restricted/find_chasm_contents() - var/list/chasm_contents = list() - if (!GLOB.chasm_storage.len) - return chasm_contents - - var/list/chasm_storage_resolved = recursive_list_resolve(GLOB.chasm_storage) - for (var/obj/storage as anything in chasm_storage_resolved) +/obj/item/chasm_detritus/restricted/get_chasm_contents(turf/fishing_spot) + . = list() + for (var/obj/effect/abstract/chasm_storage/storage in range(5, fishing_spot)) for (var/thing as anything in storage.contents) if(!istype(thing, chasm_storage_restricted_type)) continue + . += thing - chasm_contents += thing +/obj/item/chasm_detritus/restricted/objects + default_contents_chance = 12.5 + default_contents_key = NO_CORPSES - return chasm_contents +/obj/item/chasm_detritus/restricted/bodies + default_contents_chance = 12.5 + default_contents_key = BODIES_ONLY + chasm_storage_restricted_type = /mob + +/// This also includes all mobs fallen into chasms, regardless of distance +/obj/item/chasm_detritus/restricted/bodies/get_chasm_contents(turf/fishing_spot) + . = ..() + . |= GLOB.chasm_fallen_mobs /// Body detritus is selected in favor of bodies belonging to sentient mobs /// The first sentient body found in the list of contents is returned, otherwise @@ -106,17 +109,6 @@ return fallen_mob return ..() -/obj/item/chasm_detritus/restricted/objects - default_contents_chance = 12.5 - default_contents_key = NO_CORPSES - - -/obj/item/chasm_detritus/restricted/bodies - default_contents_chance = 12.5 - default_contents_key = BODIES_ONLY - chasm_storage_restricted_type = /mob - - #undef NORMAL_CONTENTS #undef BODIES_ONLY #undef NO_CORPSES diff --git a/code/modules/fishing/fish/fish_evolution.dm b/code/modules/fishing/fish/fish_evolution.dm new file mode 100644 index 00000000000..ccdf50f9e0e --- /dev/null +++ b/code/modules/fishing/fish/fish_evolution.dm @@ -0,0 +1,113 @@ +GLOBAL_LIST_INIT(fish_evolutions, init_fish_evolutions()) + +/proc/init_fish_evolutions() + . = list() + for(var/datum/fish_evolution/evolution as anything in subtypesof(/datum/fish_evolution)) + //Skip abstract types. + if(!initial(evolution.probability) || !initial(evolution.new_fish_type)) + continue + .[evolution] = new evolution + +/** + * Fish evolution datums + * + * If present in a fish's evolution_types list, and other conditions are met in check_conditions() + * then there's a chance the offspring may be of a new type rather than the same as its source or mate (if any). + */ +/datum/fish_evolution + var/name + var/probability = 0 + ///The obj/item/fish path of the new fish + var/obj/item/fish/new_fish_type = /obj/item/fish + ///The minimum required temperature for the evolved fish to spawn + var/required_temperature_min = MIN_AQUARIUM_TEMP + ///The maximum required temperature for the evolved fish to spawn + var/required_temperature_max = MAX_AQUARIUM_TEMP + ///A list of traits added to the new fish. These take priority over the parents' traits. + var/list/new_traits + ///If set, these traits will be removed from the new fish. + var/list/removed_traits + ///A text string shown in the catalog, containing information on conditions specific to this evolution. + var/conditions_note + +/datum/fish_evolution/New() + if(!ispath(new_fish_type, /obj/item/fish)) + stack_trace("[type] instantiated with a new fish type of [new_fish_type]. That's not a fish, hun, things will break.") + if(!name) + name = full_capitalize(initial(new_fish_type.name)) +/** + * The main proc that checks whether this can happen or not. + * Please do keep in mind a mate may not be present for fish with the + * self-reproductive trait. + */ +/datum/fish_evolution/proc/check_conditions(obj/item/fish/source, obj/item/fish/mate, obj/structure/aquarium/aquarium) + SHOULD_CALL_PARENT(TRUE) + //chances are halved if only one parent has this evolution. + var/real_probability = (mate && (type in mate.evolution_types)) ? probability : probability/2 + if(!prob(real_probability)) + return FALSE + if(!ISINRANGE(aquarium.fluid_temp, required_temperature_min, required_temperature_max)) + return FALSE + return TRUE + +///Called by the fish analyzer right click function. Returns a text string used as tooltip. +/datum/fish_evolution/proc/get_evolution_tooltip() + . = "" + if(required_temperature_min != MIN_AQUARIUM_TEMP || required_temperature_max != MAX_AQUARIUM_TEMP) + . = "An aquarium temperature between [required_temperature_min] and [required_temperature_max] is required." + if(conditions_note) + . += " [conditions_note]" + return . + +///Proc called to let evolution register signals that are needed for various conditions. +/datum/fish_evolution/proc/register_fish(obj/item/fish/fish) + return + +/datum/fish_evolution/lubefish + probability = 25 + new_fish_type = /obj/item/fish/clownfish/lube + new_traits = list(/datum/fish_trait/lubed) + conditions_note = "The fish must be fed lube beforehand." + +/datum/fish_evolution/lubefish/register_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_FED, PROC_REF(check_for_lube)) + +/datum/fish_evolution/lubefish/proc/check_for_lube(obj/item/fish/source, datum/reagents/fed_reagents, wrong_reagent_type) + SIGNAL_HANDLER + if((wrong_reagent_type == /datum/reagent/lube) || fed_reagents.remove_reagent(/datum/reagent/lube, 0.1)) + ADD_TRAIT(source, TRAIT_FISH_FED_LUBE, FISH_EVOLUTION) + addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_FISH_FED_LUBE, FISH_EVOLUTION), source.feeding_frequency) + +/datum/fish_evolution/lubefish/check_conditions(obj/item/fish/source, obj/item/fish/mate, obj/structure/aquarium/aquarium) + if(!HAS_TRAIT(source, TRAIT_FISH_FED_LUBE)) + return FALSE + return ..() + +/datum/fish_evolution/purple_sludgefish + probability = 5 + new_fish_type = /obj/item/fish/sludgefish/purple + removed_traits = list(/datum/fish_trait/no_mating) + +/datum/fish_evolution/mastodon + name = "???" //The resulting fish is not shown on the catalog. + probability = 40 + new_fish_type = /obj/item/fish/mastodon + new_traits = list(/datum/fish_trait/heavy, /datum/fish_trait/amphibious, /datum/fish_trait/predator, /datum/fish_trait/aggressive) + conditions_note = "The fish (and its mate) need to be unusually big both in size and weight." + +/datum/fish_evolution/mastodon/check_conditions(obj/item/fish/source, obj/item/fish/mate, obj/structure/aquarium/aquarium) + if((source.size < 144 || source.weight < 4000) || (mate && (mate.size < 144 || mate.weight < 4000))) + return FALSE + return ..() + +/datum/fish_evolution/chasm_chrab + probability = 50 + new_fish_type = /obj/item/fish/chasm_crab + required_temperature_min = MIN_AQUARIUM_TEMP+14 + required_temperature_max = MIN_AQUARIUM_TEMP+15 + +/datum/fish_evolution/ice_chrab + probability = 50 + new_fish_type = /obj/item/fish/chasm_crab/ice + required_temperature_min = MIN_AQUARIUM_TEMP+9 + required_temperature_max = MIN_AQUARIUM_TEMP+10 diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm new file mode 100644 index 00000000000..efe7f8f9a32 --- /dev/null +++ b/code/modules/fishing/fish/fish_traits.dm @@ -0,0 +1,338 @@ +GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list())) + +/datum/fish_trait + var/name = "Unnamed Trait" + /// Description of the trait in the fishing catalog and scanner + var/catalog_description = "Uh uh, someone has forgotten to set description to this trait. Yikes!" + ///A list of traits fish cannot have in conjunction with this trait. + var/list/incompatible_traits + /// The probability this trait can be inherited by offsprings when both mates have it + var/inheritability = 100 + /// Same as above, but for when only one has it. + var/diff_traits_inheritability = 50 + /// fishes of types within this list are granted to have this trait, no matter the probability + var/list/guaranteed_inheritance_types + +/// Difficulty modifier from this mod, needs to return a list with two values +/datum/fish_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/fish_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/fish_trait/proc/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman) + return list() + +/// Applies some special qualities to the fish that has been spawned +/datum/fish_trait/proc/apply_to_fish(obj/item/fish/fish) + return + +/// Proc used by both the predator and necrophage traits. +/datum/fish_trait/proc/eat_fish(obj/item/fish/predator, obj/item/fish/prey) + predator.last_feeding = world.time + var/message = prey.status == FISH_DEAD ? "[src] eats [prey]'s carcass." : "[src] hunts down and eats [prey]." + predator.loc.visible_message(span_warning(message)) + SEND_SIGNAL(prey, COMSIG_FISH_EATEN_BY_OTHER_FISH, predator) + qdel(prey) + +/datum/fish_trait/wary + name = "Wary" + catalog_description = "This fish will avoid visible fish lines, cloaked line recommended." + +/datum/fish_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/fish_trait/shiny_lover + name = "Shiny Lover" + catalog_description = "This fish loves shiny things, shiny lure recommended." + +/datum/fish_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/fish_trait/picky_eater + name = "Picky Eater" + catalog_description = "This fish is very picky and will ignore low quality bait." + +/datum/fish_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/fish_trait/nocturnal + name = "Nocturnal" + catalog_description = "This fish avoids bright lights, fishing and storing in darkness recommended." + +/datum/fish_trait/nocturnal/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman) + . = ..() + var/turf/turf = get_turf(fisherman) + var/light_amount = turf.get_lumcount() + if(light_amount > SHADOW_SPECIES_LIGHT_THRESHOLD) + .[MULTIPLICATIVE_FISHING_MOD] = 0 + +/datum/fish_trait/nocturnal/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(check_light)) + +/datum/fish_trait/nocturnal/proc/check_light(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + if(isturf(source.loc) || isaquarium(source)) + var/turf/turf = get_turf(source) + var/light_amount = turf.get_lumcount() + if(light_amount > SHADOW_SPECIES_LIGHT_THRESHOLD) + source.adjust_health(source.health - 0.5 * seconds_per_tick) + +/datum/fish_trait/heavy + name = "Heavy" + catalog_description = "This fish tends to stay near the waterbed."; + +/datum/fish_trait/heavy/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman) + return list(FISHING_MINIGAME_RULE_HEAVY_FISH) + +/datum/fish_trait/carnivore + name = "Carnivore" + catalog_description = "This fish can only be baited with meat." + incompatible_traits = list(/datum/fish_trait/vegan) + +/datum/fish_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/fish_trait/vegan + name = "Herbivore" + catalog_description = "This fish can only be baited with fresh produce." + incompatible_traits = list(/datum/fish_trait/carnivore, /datum/fish_trait/predator, /datum/fish_trait/necrophage) + +/datum/fish_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 + +/datum/fish_trait/emulsijack + name = "Emulsifier" + catalog_description = "This fish emits an invisible toxin that emulsifies other fish for it to feed on." + +/datum/fish_trait/emulsijack/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(emulsify)) + ADD_TRAIT(fish, TRAIT_RESIST_EMULSIFY, FISH_TRAIT_DATUM) + +/datum/fish_trait/emulsijack/proc/emulsify(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + if(!isaquarium(source.loc)) + return + var/emulsified = FALSE + for(var/obj/item/fish/victim in source.loc) + if(HAS_TRAIT(victim, TRAIT_RESIST_EMULSIFY) || HAS_TRAIT(victim, TRAIT_FISH_TOXIN_IMMUNE)) //no team killing + continue + victim.adjust_health(victim.health - 3 * seconds_per_tick) //the victim may heal a bit but this will quickly kill + emulsified = TRUE + if(emulsified) + source.adjust_health(source.health + 3 * seconds_per_tick) + source.last_feeding = world.time //it feeds on the emulsion! + +/datum/fish_trait/necrophage + name = "Necrophage" + catalog_description = "This fish will eat the carcasses of dead fishes when hungry." + incompatible_traits = list(/datum/fish_trait/vegan) + +/datum/fish_trait/necrophage/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(eat_dead_fishes)) + +/datum/fish_trait/necrophage/proc/eat_dead_fishes(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + if(world.time - source.last_feeding >= source.feeding_frequency || !isaquarium(source.loc)) + return + for(var/obj/item/fish/victim in source.loc) + if(victim.status != FISH_DEAD || victim == source || HAS_TRAIT(victim, TRAIT_YUCKY_FISH)) + continue + eat_fish(source, victim) + return + +/datum/fish_trait/parthenogenesis + name = "Parthenogenesis" + catalog_description = "This fish can reproduce asexually, without the need of a mate." + inheritability = 80 + diff_traits_inheritability = 25 + +/datum/fish_trait/parthenogenesis/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_SELF_REPRODUCE, FISH_TRAIT_DATUM) + +/** + * Useful for those species with the parthenogenesis trait if you don't want them to mate with each other, + * or for similar shenanigeans, I don't know. + * Otherwise you could just set the stable_population to 1. + */ +/datum/fish_trait/no_mating + name = "Mateless" + catalog_description = "This fish cannot reproduce with other fishes." + incompatible_traits = list(/datum/fish_trait/crossbreeder) + +/datum/fish_trait/no_mating/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_NO_MATING, FISH_TRAIT_DATUM) + +/datum/fish_trait/revival + diff_traits_inheritability = 15 + name = "Self-Revival" + catalog_description = "This fish shows a peculiar ability of reviving itself a minute or two after death." + guaranteed_inheritance_types = list(/obj/item/fish/boned, /obj/item/fish/mastodon) + +/datum/fish_trait/revival/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_STATUS_CHANGED, PROC_REF(check_status)) + +/datum/fish_trait/revival/proc/check_status(obj/item/fish/source) + SIGNAL_HANDLER + if(source.status == FISH_DEAD) + addtimer(CALLBACK(src, PROC_REF(revive), source), rand(1 MINUTES, 2 MINUTES)) + +/datum/fish_trait/revival/proc/revive(obj/item/fish/source) + if(QDELETED(source) || source.status != FISH_DEAD) + return + source.set_status(FISH_ALIVE) + var/message = span_nicegreen("[source] twitches. It's alive!") + if(isaquarium(source.loc)) + source.loc.visible_message(message) + else + source.visible_message(message) + +/datum/fish_trait/predator + name = "Predator" + catalog_description = "It's a predatory fish. It'll hunt down and eat live fishes of smaller size when hungry." + incompatible_traits = list(/datum/fish_trait/vegan) + +/datum/fish_trait/predator/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(eat_fishes)) + +/datum/fish_trait/predator/proc/eat_fishes(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + if(world.time - source.last_feeding >= source.feeding_frequency || !isaquarium(source.loc)) + return + var/obj/structure/aquarium/aquarium = source.loc + for(var/obj/item/fish/victim in aquarium.get_fishes(TRUE, source)) + if(victim.size < source.size * 0.75) // It's a big fish eat small fish world + continue + if(victim.status != FISH_ALIVE || victim == source || HAS_TRAIT(victim, TRAIT_YUCKY_FISH) || SPT_PROB(80, seconds_per_tick)) + continue + eat_fish(source, victim) + return + +/datum/fish_trait/yucky + name = "Yucky" + catalog_description = "This fish tastes so repulsive, other fishes won't try to eat it." + +/datum/fish_trait/yucky/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_ATOM_PROCESSED, PROC_REF(add_yuck)) + ADD_TRAIT(fish, TRAIT_YUCKY_FISH, FISH_TRAIT_DATUM) + LAZYSET(fish.grind_results, /datum/reagent/yuck, 3) + +/datum/fish_trait/yucky/proc/add_yuck(obj/item/fish/source, mob/living/user, obj/item/process_item, list/results) + var/amount = source.grind_results[/datum/reagent/yuck] / length(results) + for(var/atom/result as anything in results) + result.reagents?.add_reagent(/datum/reagent/yuck, amount) + +/datum/fish_trait/toxic + name = "Toxic" + catalog_description = "This fish contains toxins in its liver. Feeding it to predatory fishes or people is not reccomended." + diff_traits_inheritability = 25 + +/datum/fish_trait/toxic/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_ATOM_PROCESSED, PROC_REF(add_toxin)) + RegisterSignal(fish, COMSIG_FISH_EATEN_BY_OTHER_FISH, PROC_REF(on_eaten)) + LAZYSET(fish.grind_results, /datum/reagent/toxin/tetrodotoxin, 0.5) + +/datum/fish_trait/toxic/proc/add_toxin(obj/item/fish/source, mob/living/user, obj/item/process_item, list/results) + var/amount = source.grind_results[ /datum/reagent/toxin/tetrodotoxin] / length(results) + for(var/atom/result as anything in results) + result.reagents?.add_reagent( /datum/reagent/toxin/tetrodotoxin, amount) + +/datum/fish_trait/toxic/proc/on_eaten(obj/item/fish/source, obj/item/fish/predator) + if(HAS_TRAIT(predator, TRAIT_FISH_TOXIN_IMMUNE)) + return + RegisterSignal(predator, COMSIG_FISH_LIFE, PROC_REF(damage_predator), TRUE) + RegisterSignal(predator, COMSIG_FISH_STATUS_CHANGED, PROC_REF(stop_damaging), TRUE) + +/datum/fish_trait/toxic/proc/damage_predator(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + source.adjust_health(source.health - 3 * seconds_per_tick) + +/datum/fish_trait/toxic/proc/stop_damaging(obj/item/fish/source) + SIGNAL_HANDLER + if(source.status == FISH_DEAD) + UnregisterSignal(source, list(COMSIG_FISH_LIFE, COMSIG_FISH_STATUS_CHANGED)) + +/datum/fish_trait/toxin_immunity + name = "Toxin Immunity" + catalog_description = "This fish has developed an ample-spected immunity to toxins." + diff_traits_inheritability = 40 + +/datum/fish_trait/toxin_immunity/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_TOXIN_IMMUNE, FISH_TRAIT_DATUM) + +/datum/fish_trait/crossbreeder + name = "Crossbreeder" + catalog_description = "This fish's adaptive genetics allows it to crossbreed with other fish species." + inheritability = 80 + diff_traits_inheritability = 20 + incompatible_traits = list(/datum/fish_trait/no_mating) + +/datum/fish_trait/crossbreeder/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_CROSSBREEDER, FISH_TRAIT_DATUM) + +/datum/fish_trait/aggressive + name = "Aggressive" + inheritability = 80 + diff_traits_inheritability = 40 + catalog_description = "This fish is agressively territorial, and may attack fish that come close to it." + +/datum/fish_trait/aggressive/apply_to_fish(obj/item/fish/fish) + RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(try_attack_fish)) + +/datum/fish_trait/aggressive/proc/try_attack_fish(obj/item/fish/source, seconds_per_tick) + SIGNAL_HANDLER + if(!isaquarium(source.loc) || !SPT_PROB(1, seconds_per_tick)) + return + var/obj/structure/aquarium/aquarium = source.loc + for(var/obj/item/fish/victim in aquarium.get_fishes(TRUE, source)) + if(victim.status != FISH_ALIVE) + continue + aquarium.visible_message(span_warning("[source] violently [pick("whips", "bites", "attacks", "slams")] [victim]")) + var/damage = round(rand(4, 20) * (source.size / victim.size)) //smaller fishes take extra damage. + victim.adjust_health(victim.health - damage) + return + +/datum/fish_trait/lubed + name = "Lubed" + inheritability = 90 + diff_traits_inheritability = 45 + guaranteed_inheritance_types = list(/obj/item/fish/clownfish/lube) + catalog_description = "This fish exudes a viscous, slippery lubrificant. It's reccomended not to step on it." + +/datum/fish_trait/lubed/apply_to_fish(obj/item/fish/fish) + fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP) + +/datum/fish_trait/lubed/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman) + return list(FISHING_MINIGAME_RULE_LUBED_FISH) + +/datum/fish_trait/amphibious + name = "Amphibious" + inheritability = 80 + diff_traits_inheritability = 40 + catalog_description = "This fish has developed a primitive adaptation to life on both land and water." + +/datum/fish_trait/amphibious/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS, FISH_TRAIT_DATUM) + if(fish.required_fluid_type == AQUARIUM_FLUID_AIR) + fish.required_fluid_type = AQUARIUM_FLUID_FRESHWATER diff --git a/code/modules/fishing/fish/fish_types.dm b/code/modules/fishing/fish/fish_types.dm index fe6f634fce8..34c945241da 100644 --- a/code/modules/fishing/fish/fish_types.dm +++ b/code/modules/fishing/fish/fish_types.dm @@ -10,6 +10,8 @@ average_size = 30 average_weight = 500 favorite_bait = list(/obj/item/food/bait/worm) + required_temperature_min = MIN_AQUARIUM_TEMP+18 + required_temperature_max = MIN_AQUARIUM_TEMP+26 /obj/item/fish/angelfish name = "angelfish" @@ -21,6 +23,9 @@ average_size = 30 average_weight = 500 stable_population = 3 + fish_traits = list(/datum/fish_trait/aggressive) + required_temperature_min = MIN_AQUARIUM_TEMP+22 + required_temperature_max = MIN_AQUARIUM_TEMP+30 /obj/item/fish/guppy name = "guppy" @@ -33,6 +38,8 @@ average_size = 30 average_weight = 500 stable_population = 6 + required_temperature_min = MIN_AQUARIUM_TEMP+20 + required_temperature_max = MIN_AQUARIUM_TEMP+28 /obj/item/fish/plasmatetra name = "plasma tetra" @@ -43,6 +50,8 @@ average_size = 30 average_weight = 500 stable_population = 3 + required_temperature_min = MIN_AQUARIUM_TEMP+20 + required_temperature_max = MIN_AQUARIUM_TEMP+28 /obj/item/fish/catfish name = "cory catfish" @@ -59,6 +68,8 @@ "Value" = JUNKFOOD ) ) + required_temperature_min = MIN_AQUARIUM_TEMP+12 + required_temperature_max = MIN_AQUARIUM_TEMP+30 // Saltwater fish below @@ -73,8 +84,23 @@ average_size = 30 average_weight = 500 stable_population = 4 + grind_results = list(/datum/reagent/blood = 20, /datum/reagent/consumable/liquidgibs = 2) + fish_traits = list(/datum/fish_trait/picky_eater) + evolution_types = list(/datum/fish_evolution/lubefish) + compatible_types = list(/obj/item/fish/clownfish/lube) + required_temperature_min = MIN_AQUARIUM_TEMP+22 + required_temperature_max = MIN_AQUARIUM_TEMP+30 - fishing_traits = list(/datum/fishing_trait/picky_eater) +/obj/item/fish/clownfish/lube + name = "lubefish" + desc = "A clownfish exposed to cherry-flavored lube for far too long. First discovered the days following a cargo incident around the seas of Europa, when thousands of thousands of thousands..." + icon_state = "lubefish" + random_case_rarity = FISH_RARITY_VERY_RARE + dedicated_in_aquarium_icon_state = "lubefish_small" + fish_traits = list(/datum/fish_trait/picky_eater, /datum/fish_trait/lubed) + evolution_types = null + compatible_types = list(/obj/item/fish/clownfish) + food = /datum/reagent/lube /obj/item/fish/cardinal name = "cardinalfish" @@ -85,7 +111,9 @@ average_size = 30 average_weight = 500 stable_population = 4 - fishing_traits = list(/datum/fishing_trait/vegan) + fish_traits = list(/datum/fish_trait/vegan) + required_temperature_min = MIN_AQUARIUM_TEMP+22 + required_temperature_max = MIN_AQUARIUM_TEMP+30 /obj/item/fish/greenchromis name = "green chromis" @@ -97,6 +125,8 @@ average_size = 30 average_weight = 500 stable_population = 5 + required_temperature_min = MIN_AQUARIUM_TEMP+23 + required_temperature_max = MIN_AQUARIUM_TEMP+28 fishing_difficulty_modifier = 5 // Bit harder @@ -112,10 +142,12 @@ stable_population = 3 disliked_bait = list(/obj/item/food/bait/worm, /obj/item/food/bait/doughball) fish_ai_type = FISH_AI_ZIPPY + required_temperature_min = MIN_AQUARIUM_TEMP+23 + required_temperature_max = MIN_AQUARIUM_TEMP+28 /obj/item/fish/pufferfish name = "pufferfish" - desc = "One Pufferfish contains enough toxins in its liver to kill 30 people." + desc = "They say that one pufferfish contains enough toxins to kill 30 people, although in the last few decades they've been genetically engineered en masse to be less poisonous." icon_state = "pufferfish" required_fluid_type = AQUARIUM_FLUID_SALTWATER sprite_width = 8 @@ -123,8 +155,10 @@ average_size = 60 average_weight = 1000 stable_population = 3 + required_temperature_min = MIN_AQUARIUM_TEMP+23 + required_temperature_max = MIN_AQUARIUM_TEMP+28 - fishing_traits = list(/datum/fishing_trait/heavy) + fish_traits = list(/datum/fish_trait/heavy, /datum/fish_trait/toxic) /obj/item/fish/lanternfish name = "lanternfish" @@ -139,8 +173,9 @@ average_size = 100 average_weight = 1500 stable_population = 3 - - fishing_traits = list(/datum/fishing_trait/nocturnal) + fish_traits = list(/datum/fish_trait/nocturnal) + required_temperature_min = MIN_AQUARIUM_TEMP+2 //My source is that the water at a depth 6600 feet is pretty darn cold. + required_temperature_max = MIN_AQUARIUM_TEMP+18 //Tiziran Fish /obj/item/fish/dwarf_moonfish @@ -152,6 +187,8 @@ fillet_type = /obj/item/food/fishmeat/moonfish average_size = 100 average_weight = 2000 + required_temperature_min = MIN_AQUARIUM_TEMP+20 + required_temperature_max = MIN_AQUARIUM_TEMP+30 /obj/item/fish/gunner_jellyfish name = "gunner jellyfish" @@ -160,6 +197,8 @@ required_fluid_type = AQUARIUM_FLUID_SALTWATER stable_population = 4 fillet_type = /obj/item/food/fishmeat/gunner_jellyfish + required_temperature_min = MIN_AQUARIUM_TEMP+24 + required_temperature_max = MIN_AQUARIUM_TEMP+32 /obj/item/fish/needlefish name = "needlefish" @@ -168,9 +207,11 @@ required_fluid_type = AQUARIUM_FLUID_SALTWATER stable_population = 12 fillet_type = null - average_size = 30 + average_size = 20 average_weight = 300 - fishing_traits = list(/datum/fishing_trait/carnivore) + fish_traits = list(/datum/fish_trait/carnivore) + required_temperature_min = MIN_AQUARIUM_TEMP+10 + required_temperature_max = MIN_AQUARIUM_TEMP+32 /obj/item/fish/armorfish name = "armorfish" @@ -180,6 +221,8 @@ stable_population = 10 fillet_type = /obj/item/food/fishmeat/armorfish fish_ai_type = FISH_AI_SLOW + required_temperature_min = MIN_AQUARIUM_TEMP+10 + required_temperature_max = MIN_AQUARIUM_TEMP+32 //Chasm fish /obj/item/fish/chasm_crab @@ -195,6 +238,20 @@ feeding_frequency = 15 MINUTES random_case_rarity = FISH_RARITY_RARE fillet_type = /obj/item/food/meat/slab/rawcrab + required_temperature_min = MIN_AQUARIUM_TEMP+9 + required_temperature_max = MAX_AQUARIUM_TEMP+150 + evolution_types = list(/datum/fish_evolution/ice_chrab) + compatible_types = list(/obj/item/fish/chasm_crab/ice) + +/obj/item/fish/chasm_crab/ice + name = "arctic chrab" + desc = "A subspecies of chasm chrabs that has adapted to the cold climate and lack of abysmal holes of the icemoon." + icon_state = "arctic_chrab" + dedicated_in_aquarium_icon_state = "ice_chrab_small" + required_temperature_min = MIN_AQUARIUM_TEMP-150 + required_temperature_max = MIN_AQUARIUM_TEMP+15 + evolution_types = list(/datum/fish_evolution/chasm_chrab) + compatible_types = list(/obj/item/fish/chasm_crab) /obj/item/storage/box/fish_debug name = "box full of fish" @@ -211,28 +268,20 @@ required_fluid_type = AQUARIUM_FLUID_FRESHWATER stable_population = 4 fillet_type = /obj/item/food/fishmeat/donkfish + fish_traits = list(/datum/fish_trait/yucky) + required_temperature_min = MIN_AQUARIUM_TEMP+15 + required_temperature_max = MIN_AQUARIUM_TEMP+28 /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." + desc = "Ah, the terrifying emulsijack. Created in a laboratory, the only real use of this slimey, scaleless fish 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(seconds_per_tick) - 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) * seconds_per_tick) //the victim may heal a bit but this will quickly kill - emulsified = TRUE - if(emulsified) - adjust_health((health + 3) * seconds_per_tick) - last_feeding = world.time //emulsijack feeds on the emulsion! - ..() + fish_traits = list(/datum/fish_trait/emulsijack) + required_temperature_min = MIN_AQUARIUM_TEMP+5 + required_temperature_max = MIN_AQUARIUM_TEMP+40 /obj/item/fish/ratfish name = "ratfish" @@ -242,6 +291,9 @@ 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_traits = list(/datum/fish_trait/necrophage) + required_temperature_min = MIN_AQUARIUM_TEMP+15 + required_temperature_max = MIN_AQUARIUM_TEMP+35 fish_ai_type = FISH_AI_ZIPPY favorite_bait = list( @@ -255,3 +307,186 @@ . = ..() //stable pop reflects the config for how many mice migrate. powerful... stable_population = CONFIG_GET(number/mice_roundstart) + +/obj/item/fish/sludgefish + name = "sludgefish" + desc = "A misshapen, fragile, loosely fish-like living goop, the only thing that'd ever thrive in the acidic and claustrophobic cavities of the station's organic waste disposal system." + icon_state = "sludgefish" + dedicated_in_aquarium_icon_state = "sludgefish_small" + sprite_width = 7 + sprite_height = 6 + required_fluid_type = AQUARIUM_FLUID_SULPHWATEVER + stable_population = 8 + average_size = 20 + average_weight = 400 + health = 50 + breeding_timeout = 5 MINUTES + fish_traits = list(/datum/fish_trait/parthenogenesis, /datum/fish_trait/no_mating) + required_temperature_min = MIN_AQUARIUM_TEMP+10 + required_temperature_max = MIN_AQUARIUM_TEMP+40 + evolution_types = list(/datum/fish_evolution/purple_sludgefish) + +/obj/item/fish/sludgefish/purple + name = "purple sludgefish" + desc = "A misshapen, fragile, loosely fish-like living goop. This one has developed sexual reproduction mechanisms, and a purple tint to boot." + icon_state = "sludgefish_purple" + dedicated_in_aquarium_icon_state = "sludgefish_purple_small" + available_in_random_cases = FALSE + random_case_rarity = FISH_RARITY_VERY_RARE + fish_traits = list(/datum/fish_trait/parthenogenesis) + +/obj/item/fish/slimefish + name = "acquatic slime" + desc = "Kids, this is what happens when a slime overcomes its hydrophobic nature. It goes glug glug." + icon_state = "slimefish" + icon_state_dead = "slimefish_dead" + dedicated_in_aquarium_icon_state = "slimefish_small" + sprite_width = 7 + sprite_height = 7 + do_flop_animation = FALSE //it already has our cute bouncy wiggle. :3 + random_case_rarity = FISH_RARITY_VERY_RARE + required_fluid_type = AQUARIUM_FLUID_ANADROMOUS + stable_population = 4 + health = 150 + fillet_type = /obj/item/slime_extract/grey + grind_results = list(/datum/reagent/toxin/slimejelly = 10) + fish_traits = list(/datum/fish_trait/toxin_immunity, /datum/fish_trait/crossbreeder) + favorite_bait = list( + list( + "Type" = "Foodtype", + "Value" = TOXIC, + ), + list( + "Type" = "Reagent", + "Value" = /datum/reagent/toxin, + "Amount" = 5, + ), + ) + required_temperature_min = MIN_AQUARIUM_TEMP+20 + +/obj/item/fish/boned + name = "unmarine bonemass" + desc = "What one could mistake for fish remains, is in reality a species that chose to discard its weak flesh a long time ago. A living fossil, in its most literal sense." + icon_state = "bonemass" + dedicated_in_aquarium_icon_state = "bonemass_small" + sprite_width = 10 + sprite_height = 7 + fish_ai_type = FISH_AI_ZIPPY + random_case_rarity = FISH_RARITY_GOOD_LUCK_FINDING_THIS + required_fluid_type = AQUARIUM_FLUID_ANY_WATER + health = 150 + stable_population = 3 + grind_results = list(/datum/reagent/bone_dust = 20) + fillet_type = /obj/item/stack/sheet/bone + num_fillets = 2 + fish_traits = list(/datum/fish_trait/revival, /datum/fish_trait/carnivore) + average_size = 70 + average_weight = 2000 + death_text = "%SRC stops moving." //It's dead... or is it? + evolution_types = list(/datum/fish_evolution/mastodon) + +/obj/item/fish/mastodon + name = "unmarine mastodon" + desc = "A monster of exposed muscles and innards, wrapped in a fish-like skeleton. You don't remember ever seeing it on the catalog." + icon = 'icons/obj/aquarium_wide.dmi' + icon_state = "mastodon" + dedicated_in_aquarium_icon = 'icons/obj/aquarium.dmi' + dedicated_in_aquarium_icon_state = "mastodon_small" + base_pixel_x = -16 + pixel_x = -16 + sprite_width = 12 + sprite_height = 7 + show_in_catalog = FALSE + available_in_random_cases = FALSE + random_case_rarity = FISH_RARITY_GOOD_LUCK_FINDING_THIS + fishing_difficulty_modifier = 5 + required_fluid_type = AQUARIUM_FLUID_ANY_WATER + health = 300 + stable_population = 2 //This means they can only crossbreed. + grind_results = list(/datum/reagent/bone_dust = 15, /datum/reagent/consumable/liquidgibs = 5) + fillet_type = /obj/item/stack/sheet/bone + num_fillets = 2 + feeding_frequency = 2 MINUTES + breeding_timeout = 10 MINUTES + average_size = 180 + average_weight = 5000 + death_text = "%SRC stops moving." + fish_traits = list(/datum/fish_trait/heavy, /datum/fish_trait/amphibious, /datum/fish_trait/revival, /datum/fish_trait/carnivore, /datum/fish_trait/predator, /datum/fish_trait/aggressive) + +/obj/item/fish/holo + name = "holographic goldfish" + desc = "A holographic representation of a common goldfish, slowly flickering out, removed from its holo-habitat." + icon_state = "goldfish" + show_in_catalog = FALSE + available_in_random_cases = FALSE + sprite_width = 8 + sprite_height = 8 + stable_population = 1 + average_size = 30 + average_weight = 500 + required_fluid_type = AQUARIUM_FLUID_ANADROMOUS + grind_results = null + fillet_type = null + death_text = "%SRC disappears." + fish_traits = list(/datum/fish_trait/no_mating) //just to be sure, these shouldn't reproduce + +/obj/item/fish/holo/Initialize(mapload) + . = ..() + var/area/station/holodeck/holo_area = get_area(src) + if(!istype(holo_area)) + return + holo_area.linked.add_to_spawned(src) + +/obj/item/fish/holo/set_status(new_status) + . = ..() + if(status == FISH_DEAD) + qdel(src) + +/obj/item/fish/holo/crab + name = "holographic crab" + desc = "A holographic represantion of a soul-crushingly soulless crab, unlike the cuter ones occasionally roaming around. It stares at you, with empty, beady eyes." + icon_state = "crab" + dedicated_in_aquarium_icon_state = "crab_small" + average_weight = 1000 + sprite_height = 6 + sprite_width = 10 + +/obj/item/fish/holo/puffer + name = "holographic pufferfish" + desc ="A holographic representation of 100% safe-to-eat pufferfish... that is, if holographic fishes were even edible." + sprite_width = 8 + sprite_height = 8 + average_size = 60 + average_weight = 1000 + +/obj/item/fish/holo/angel + name = "holographic angelfish" + desc = "A holographic representation of a angelfish. I got nothing snarky to say about this one." + icon_state = "angelfish" + dedicated_in_aquarium_icon_state = "bigfish" + sprite_height = 7 + +/obj/item/fish/holo/clown + name = "holographic clownfish" + icon_state = "holo_clownfish" + desc = "A holographic representation of a clownfish, or at least how they used to look like five centuries ago." + dedicated_in_aquarium_icon_state = "holo_clownfish_small" + required_fluid_type = AQUARIUM_FLUID_SALTWATER + sprite_width = 8 + sprite_height = 5 + +/obj/item/fish/holo/checkered + name = "unrendered holographic fish" //it's a meta joke, buddy. + desc = "A checkered silhoutte of searing purple and pitch black presents itself before your eyes, like a tear in fabric of reality. It hurts to watch." + icon_state = "checkered" + dedicated_in_aquarium_icon_state = "checkered_small" + sprite_width = 4 + +/obj/item/fish/holo/halffish + name = "holographic half-fish" + desc = "A holographic representation of... a fish reduced to all bones, except for its head. Isn't it supposed to be dead? Ehr, holo-dead?" + icon_state = "half_fish" + dedicated_in_aquarium_icon_state = "half_fish_small" + sprite_height = 4 + sprite_width = 10 + average_size = 50 diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm index 854a4080b89..962486d6d1d 100644 --- a/code/modules/fishing/fish_catalog.dm +++ b/code/modules/fishing/fish_catalog.dm @@ -91,10 +91,10 @@ .["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)] + var/list/fish_traits = fish_list_properties[fishy][NAMEOF(fishy, fish_traits)] for(var/fish_trait in fish_traits) - var/datum/fishing_trait/trait = fish_trait - trait_descriptions += initial(trait.catalog_description) + var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait] + trait_descriptions += trait.catalog_description if(!length(trait_descriptions)) trait_descriptions += "This fish exhibits no special behavior." .["traits"] = trait_descriptions diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm index a5c06197921..0a5e697e271 100644 --- a/code/modules/fishing/fishing_equipment.dm +++ b/code/modules/fishing/fishing_equipment.dm @@ -150,7 +150,7 @@ /obj/item/fishing_hook/bone name = "bone hook" - desc = "a simple hook carved from sharpened bone" + desc = "A simple hook carved from sharpened bone" icon_state = "hook_bone" /datum/crafting_recipe/bone_hook @@ -160,6 +160,24 @@ time = 2 SECONDS category = CAT_TOOLS +/obj/item/fishing_hook/stabilized + name = "gyro-stabilized hook" + desc = "A quirky hook that grants the user a better control of the tool, allowing them to move the hook both and up and down when reeling in, otherwise keeping it stabilized." + icon_state = "gyro" + fishing_hook_traits = FISHING_HOOK_BIDIRECTIONAL + rod_overlay_icon_state = "hook_gyro_overlay" + +/obj/item/fishing_hook/stabilized/examine(mob/user) + . = ..() + . += span_notice("While fishing, you can press the Ctrl key down to move the bait down, rather than up.") + +/obj/item/fishing_hook/jaws + name = "jawed hook" + desc = "Despite hints of rust, this gritty beartrap-like hook hybrid manages to look even more threating than the real thing. May neptune have mercy of whatever is caught by its jaws." + icon_state = "jaws" + fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_ENSNARE|FISHING_HOOK_KILL + rod_overlay_icon_state = "hook_jaws_overlay" + /obj/item/storage/toolbox/fishing name = "fishing toolbox" desc = "Contains everything you need for your fishing trip." @@ -197,6 +215,5 @@ new /obj/item/fishing_line/reinforced(src) new /obj/item/fishing_line/cloaked(src) - #undef MAGNET_HOOK_BONUS_MULTIPLIER #undef RESCUE_HOOK_FISH_MULTIPLIER diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm index 1811c4d2f17..b2e021533ad 100644 --- a/code/modules/fishing/fishing_minigame.dm +++ b/code/modules/fishing/fishing_minigame.dm @@ -33,34 +33,51 @@ /// 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) + var/experience_multiplier = 1 + +/datum/fishing_challenge/New(datum/component/fishing_spot/comp, reward_path, obj/item/fishing_rod/rod, mob/user) src.user = user src.reward_path = reward_path src.used_rod = rod - lure = new(get_turf(spot)) + var/atom/spot = comp.parent + lure = new(get_turf(spot), spot) + RegisterSignal(spot, COMSIG_QDELETING, PROC_REF(on_spot_gone)) + RegisterSignal(comp.fish_source, COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE, PROC_REF(interrupt_challenge)) + comp.fish_source.RegisterSignal(src, COMSIG_FISHING_CHALLENGE_COMPLETED, TYPE_PROC_REF(/datum/fish_source, on_challenge_completed)) + background = comp.fish_source.background /// 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 + // Apply fish trait modifiers var/list/fish_list_properties = collect_fish_properties() - var/list/fish_traits = fish_list_properties[fish][NAMEOF(fish, fishing_traits)] + var/list/fish_traits = fish_list_properties[fish][NAMEOF(fish, fish_traits)] for(var/fish_trait in fish_traits) - var/datum/fishing_trait/trait = new fish_trait + var/datum/fish_trait/trait = GLOB.fish_traits[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 + 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 + special_effects |= FISHING_MINIGAME_RULE_WEIGHTED_BAIT + if(rod.hook.fishing_hook_traits & FISHING_HOOK_BIDIRECTIONAL) + special_effects |= FISHING_MINIGAME_RULE_BIDIRECTIONAL + if(rod.hook.fishing_hook_traits & FISHING_HOOK_NO_ESCAPE) + special_effects |= FISHING_MINIGAME_RULE_NO_ESCAPE + if(rod.hook.fishing_hook_traits & FISHING_HOOK_ENSNARE) + special_effects |= FISHING_MINIGAME_RULE_LIMIT_LOSS + if(rod.hook.fishing_hook_traits & FISHING_HOOK_KILL) + special_effects |= FISHING_MINIGAME_RULE_KILL + + if((FISHING_MINIGAME_RULE_KILL in special_effects) && ispath(reward_path,/obj/item/fish)) + RegisterSignal(user, COMSIG_MOB_FISHING_REWARD_DISPENSED, PROC_REF(hurt_fish)) + + difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src) /datum/fishing_challenge/Destroy(force, ...) if(!completed) @@ -69,13 +86,29 @@ QDEL_NULL(fishing_line) if(lure) QDEL_NULL(lure) - . = ..() + user = null + used_rod = null + return ..() + +/datum/fishing_challenge/proc/send_alert(message) + var/turf/lure_turf = get_turf(lure) + lure_turf?.balloon_alert(user, message) + +/datum/fishing_challenge/proc/on_spot_gone(datum/source) + send_alert("fishing spot gone!") + interrupt(balloon_alert = FALSE) + +/datum/fishing_challenge/proc/interrupt_challenge(datum/source, reason) + if(reason) + send_alert(reason) + interrupt(balloon_alert = FALSE) /datum/fishing_challenge/proc/start(mob/living/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_REF(interrupt)) + RegisterSignal(used_rod, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self)) ADD_TRAIT(user, TRAIT_GONE_FISHING, REF(src)) user.add_mood_event("fishing", /datum/mood_event/fishing) RegisterSignal(user, COMSIG_MOB_CLICKON, PROC_REF(handle_click)) @@ -83,40 +116,53 @@ to_chat(user, span_notice("You start fishing...")) playsound(lure, 'sound/effects/splash.ogg', 100) -/datum/fishing_challenge/proc/handle_click() +/datum/fishing_challenge/proc/handle_click(mob/source, atom/target, modifiers) + SIGNAL_HANDLER + //You need to be holding the rod to use it. + if(!source.get_active_held_item(used_rod) || LAZYACCESS(modifiers, SHIFT_CLICK)) + return if(phase == WAIT_PHASE) //Reset wait - lure.balloon_alert(user, "miss!") + send_alert("miss!") start_baiting_phase() else if(phase == BITING_PHASE) - start_minigame_phase() + INVOKE_ASYNC(src, PROC_REF(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() +/datum/fishing_challenge/proc/interrupt(datum/source, balloon_alert = TRUE) SIGNAL_HANDLER if(!completed) + experience_multiplier *= 0.5 + if(balloon_alert) + send_alert(user.is_holding(used_rod) ? "line snapped" : "tool dropped") + complete(FALSE) + +/datum/fishing_challenge/proc/on_attack_self(obj/item/source, mob/user) + SIGNAL_HANDLER + INVOKE_ASYNC(src, PROC_REF(stop_fishing), source, user) + +/datum/fishing_challenge/proc/stop_fishing(obj/item/rod, mob/user) + if((phase != MINIGAME_PHASE || do_after(user, 3 SECONDS, rod)) && !QDELETED(src) && !completed) + experience_multiplier *= 0.5 + send_alert("stopped fishing") 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(start_time) + var/seconds_spent = (world.time - start_time)/10 + if(!(FISHING_MINIGAME_RULE_NO_EXP in special_effects)) + user.mind?.adjust_experience(/datum/skill/fishing, min(round(seconds_spent * FISHING_SKILL_EXP_PER_SECOND * experience_multiplier), FISHING_SKILL_EXP_CAP_PER_GAME)) + if(win && user.mind?.get_skill_level(/datum/skill/fishing) >= SKILL_LEVEL_LEGENDARY) + user.client?.give_award(/datum/award/achievement/skill/legendary_fisher, user) + if(used_rod && phase == MINIGAME_PHASE) + used_rod.consume_bait() 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) @@ -134,19 +180,41 @@ phase = BITING_PHASE // Trashing animation playsound(lure, 'sound/effects/fish_splash.ogg', 100) - lure.balloon_alert(user, "!!!") + send_alert("!!!") 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_REF(start_baiting_phase)), wait_time, TIMER_STOPPABLE) +///The damage dealt per second to the fish when FISHING_MINIGAME_RULE_KILL is active. +#define FISH_DAMAGE_PER_SECOND 2 + /datum/fishing_challenge/proc/start_minigame_phase() phase = MINIGAME_PHASE deltimer(next_phase_timer) + if((FISHING_MINIGAME_RULE_KILL in special_effects) && ispath(reward_path,/obj/item/fish)) + var/obj/item/fish/fish = reward_path + var/wait_time = (initial(fish.health) / FISH_DAMAGE_PER_SECOND) SECONDS + addtimer(CALLBACK(src, PROC_REF(win_anyway)), wait_time) start_time = world.time + experience_multiplier += difficulty * FISHING_SKILL_DIFFIULTY_EXP_MULT ui_interact(user) +/datum/fishing_challenge/proc/win_anyway() + if(!completed) + //winning by timeout or idling around shouldn't give as much experience. + experience_multiplier *= 0.5 + complete(TRUE) + +/datum/fishing_challenge/proc/hurt_fish(datum/source, obj/item/fish/reward) + SIGNAL_HANDLER + if(istype(reward)) + var/damage = CEILING((world.time - start_time)/10 * FISH_DAMAGE_PER_SECOND, 1) + reward.adjust_health(reward.health - damage) + +#undef FISH_DAMAGE_PER_SECOND + /datum/fishing_challenge/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) @@ -162,11 +230,12 @@ /datum/fishing_challenge/ui_close(mob/user) . = ..() if(!completed) + send_alert("stopped fishing") complete(FALSE) /datum/fishing_challenge/ui_static_data(mob/user) . = ..() - .["difficulty"] = max(1,min(difficulty,100)) + .["difficulty"] = clamp(difficulty, 1, 100) .["fish_ai"] = fish_ai .["special_effects"] = special_effects .["background_image"] = background @@ -176,7 +245,7 @@ /datum/fishing_challenge/ui_status(mob/user, datum/ui_state/state) return min( - get_dist(user, lure) > max_distance ? UI_CLOSE : UI_INTERACTIVE, + get_dist(user, lure) > 5 ? UI_CLOSE : UI_INTERACTIVE, ui_status_user_has_free_hands(user), ui_status_user_is_abled(user, lure), ) @@ -193,6 +262,7 @@ if("win") complete(win = TRUE, perfect_win = params["perfect"]) if("lose") + send_alert("it got away") complete(win = FALSE) /// The visual that appears over the fishing spot @@ -200,6 +270,15 @@ icon = 'icons/obj/fishing.dmi' icon_state = "lure_idle" +/obj/effect/fishing_lure/Initialize(mapload, atom/spot) + . = ..() + if(ismovable(spot)) // we want the lure and therefore the fishing line to stay connected with the fishing spot. + RegisterSignal(spot, COMSIG_MOVABLE_MOVED, PROC_REF(follow_movable)) + +/obj/effect/fishing_lure/proc/follow_movable(atom/movable/source) + set_glide_size(source.glide_size) + forceMove(source.loc) + #undef WAIT_PHASE #undef BITING_PHASE #undef MINIGAME_PHASE diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm index d34c04ac118..04455485768 100644 --- a/code/modules/fishing/fishing_rod.dm +++ b/code/modules/fishing/fishing_rod.dm @@ -38,12 +38,17 @@ /// List of fishing line beams var/list/fishing_lines = list() + /// The default color for the reel overlay if no line is equipped. var/default_line_color = "gray" + ///The name of the icon state of the reel overlay + var/reel_overlay = "reel_overlay" + /obj/item/fishing_rod/Initialize(mapload) . = ..() register_context() register_item_context() + update_appearance() /obj/item/fishing_rod/add_context(atom/source, list/context, obj/item/held_item, mob/user) if(src == held_item) @@ -96,10 +101,7 @@ * * target_fish_source - The /datum/fish_source we're trying to fish in. */ /obj/item/fishing_rod/proc/reason_we_cant_fish(datum/fish_source/target_fish_source) - if(!hook) - return null - - return hook.reason_we_cant_fish(target_fish_source) + return hook?.reason_we_cant_fish(target_fish_source) /obj/item/fishing_rod/proc/consume_bait() @@ -201,23 +203,27 @@ 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() + cast_line(target, user, proximity_flag) return . +///Called by afterattack(). If the line to whatever that is is clear and we're not already busy, try fishing in it +/obj/item/fishing_rod/proc/cast_line(atom/target, mob/user, proximity_flag) + if(casting || currently_hooked_item || proximity_flag || !CheckToolReach(user, target, cast_range)) + return + /// 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 @@ -237,11 +243,15 @@ /obj/item/fishing_rod/update_overlays() . = ..() + . += get_fishing_overlays() + +/obj/item/fishing_rod/proc/get_fishing_overlays() + . = list() 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 + var/mutable_appearance/reel_appearance = mutable_appearance(icon, reel_overlay) + reel_appearance.color = line_color; + . += reel_appearance // Line & hook is also visible when only bait is equipped but it uses default appearances then if(hook || bait) @@ -260,6 +270,10 @@ /obj/item/fishing_rod/worn_overlays(mutable_appearance/standing, isinhands, icon_file) . = ..() + . += get_fishing_worn_overlays(standing, isinhands, icon_file) + +/obj/item/fishing_rod/proc/get_fishing_worn_overlays(mutable_appearance/standing, isinhands, icon_file) + . = list() 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 @@ -401,6 +415,8 @@ name = "bone fishing rod" desc = "A humble rod, made with whatever happened to be on hand." icon_state = "fishing_rod_bone" + reel_overlay = "reel_bone" + default_line_color = "red" /datum/crafting_recipe/bone_rod name = "Bone Fishing Rod" @@ -411,19 +427,84 @@ /obj/item/stack/sheet/bone = 2) category = CAT_TOOLS -/obj/item/fishing_rod/master +/obj/item/fishing_rod/telescopic + icon_state = "fishing_rod_telescopic" + desc = "A lightweight, ergonomic, easy to store telescopic fishing rod. " + inhand_icon_state = null + force = 0 + w_class = WEIGHT_CLASS_NORMAL + ui_description = "A collapsible fishing rod that can fit within a backpack." + reel_overlay = "reel_telescopic" + ///Whether the rod is exteded or not. Tied to the transforming element. + var/active = FALSE + ///The force of the item when extended. + var/active_force = 8 + +/obj/item/fishing_rod/telescopic/Initialize(mapload) + . = ..() + AddComponent(/datum/component/transforming, force_on = 8, hitsound_on = hitsound, w_class_on = WEIGHT_CLASS_HUGE, clumsy_check = FALSE) + RegisterSignal(src, COMSIG_TRANSFORMING_PRE_TRANSFORM, PROC_REF(pre_transform)) + RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform)) + +/obj/item/fishing_rod/telescopic/reason_we_cant_fish(datum/fish_source/target_fish_source) + if(!active) + return "You need to extend your fishing rod before you can cast the line." + return ..() + +/obj/item/fishing_rod/telescopic/cast_line(atom/target, mob/user, proximity_flag) + if(!active) + to_chat(user, "You need to extend your fishing rod before you can cast the line.") + return + return ..() + +/obj/item/fishing_rod/telescopic/get_fishing_overlays() + if(!active) + return list() + return ..() + +/obj/item/fishing_rod/telescopic/get_fishing_worn_overlays(mutable_appearance/standing, isinhands, icon_file) + if(!active) + return list() + return ..() + +///Stops the fishing rod from being collapsed while fishing. +/obj/item/fishing_rod/telescopic/proc/pre_transform(obj/item/source, mob/user, active) + SIGNAL_HANDLER + if(active) + return + //the fishing minigame uses the attack_self signal to let the user end it early without having to drop the rod. + if(HAS_TRAIT(user, TRAIT_GONE_FISHING)) + return COMPONENT_BLOCK_TRANSFORM + +///Gives feedback to the user, makes it show up inhand, toggles whether it can be used for fishing. +/obj/item/fishing_rod/telescopic/proc/on_transform(obj/item/source, mob/user, active) + SIGNAL_HANDLER + + src.active = active + inhand_icon_state = active ? "rod" : null // When inactive, there is no inhand icon_state. + if(user) + balloon_alert(user, active ? "extended" : "collapsed") + playsound(src, 'sound/weapons/batonextend.ogg', 50, TRUE) + update_appearance(UPDATE_OVERLAYS) + if(currently_hooked_item) + clear_hooked_item() + return COMPONENT_NO_DEFAULT_MESSAGE + +/obj/item/fishing_rod/telescopic/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" - + reel_overlay = "reel_master" + active_force = 13 //It's that sturdy /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" + reel_overlay = "reel_science" /obj/item/fishing_rod/tech/Initialize(mapload) . = ..() diff --git a/code/modules/fishing/fishing_traits.dm b/code/modules/fishing/fishing_traits.dm deleted file mode 100644 index 3a58ae5157c..00000000000 --- a/code/modules/fishing/fishing_traits.dm +++ /dev/null @@ -1,82 +0,0 @@ -/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 index 4f153eb9ff4..057f4f12280 100644 --- a/code/modules/fishing/sources/_fish_source.dm +++ b/code/modules/fishing/sources/_fish_source.dm @@ -11,9 +11,15 @@ GLOBAL_LIST_INIT(preset_fish_sources,init_fishing_configurations()) var/datum/fish_source/lavaland/lava_preset = new .[FISHING_SPOT_PRESET_LAVALAND_LAVA] = lava_preset + var/datum/fish_source/lavaland/icemoon/plasma_preset = new + .[FISHING_SPOT_PRESET_ICEMOON_PLASMA] = plasma_preset + var/datum/fish_source/chasm/chasm_preset = new .[FISHING_SPOT_PRESET_CHASM] = chasm_preset + var/datum/fish_source/toilet/toilet_preset = new + .[FISHING_SPOT_PRESET_TOILET] = toilet_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 @@ -35,7 +41,7 @@ GLOBAL_LIST_INIT(preset_fish_sources,init_fishing_configurations()) /// 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) +/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge) . = fishing_difficulty if(!ispath(result,/obj/item/fish)) @@ -55,21 +61,22 @@ GLOBAL_LIST_INIT(preset_fish_sources,init_fishing_configurations()) 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 + + if(!challenge || !(FISHING_MINIGAME_RULE_NO_EXP in challenge.special_effects)) + . += fisherman.mind?.get_skill_modifier(/datum/skill/fishing, SKILL_VALUE_MODIFIER) // Matching/not matching fish traits and equipment - var/list/fish_traits = fish_list_properties[caught_fish][NAMEOF(caught_fish, fishing_traits)] + var/list/fish_traits = fish_list_properties[caught_fish][NAMEOF(caught_fish, fish_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/datum/fish_trait/trait = GLOB.fish_traits[fish_trait] var/list/mod = trait.difficulty_mod(rod, fisherman) additive_mod += mod[ADDITIVE_FISHING_MOD] multiplicative_mod *= mod[MULTIPLICATIVE_FISHING_MOD] @@ -81,29 +88,57 @@ GLOBAL_LIST_INIT(preset_fish_sources,init_fishing_configurations()) /datum/fish_source/proc/roll_reward(obj/item/fishing_rod/rod, mob/fisherman) return pick_weight(get_modified_fish_table(rod,fisherman)) +/** + * Used to register signals or add traits and the such right after conditions have been cleared + * and before the minigame starts. + */ +/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user) + return + +///Proc called when the challenge is interrupted within the fish source code. +/datum/fish_source/proc/interrupt_challenge(reason) + SEND_SIGNAL(src, COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE, reason) + +/** + * Proc called when the COMSIG_FISHING_CHALLENGE_COMPLETED signal is sent. + * Check if we've succeeded. If so, write into memory and dispense the reward. + */ +/datum/fish_source/proc/on_challenge_completed(datum/fishing_challenge/source, mob/user, success, perfect) + SIGNAL_HANDLER + SHOULD_CALL_PARENT(TRUE) + if(!success) + return + var/obj/item/fish/caught = source.reward_path + user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = initial(caught.name)) + var/turf/fishing_spot = get_turf(source.lure) + dispense_reward(source.reward_path, user, fishing_spot) + /// Gives out the reward if possible -/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman) +/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, fishing_spot) 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) + var/atom/movable/reward if(ispath(reward_path)) + reward = new reward_path(get_turf(fisherman)) if(ispath(reward_path,/obj/item)) - var/obj/item/reward = new reward_path(get_turf(fisherman)) if(ispath(reward_path,/obj/item/fish)) var/obj/item/fish/caught_fish = reward - caught_fish.randomize_weight_and_size() + caught_fish.randomize_size_and_weight() //fish caught signal if needed goes here and/or fishing achievements //Try to put it in hand - fisherman.put_in_hands(reward) + INVOKE_ASYNC(fisherman, TYPE_PROC_REF(/mob, 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)) + SEND_SIGNAL(fisherman, COMSIG_MOB_FISHING_REWARD_DISPENSED, reward) + if(reward) + SEND_SIGNAL(reward, COMSIG_ATOM_FISHING_REWARD, fishing_spot) /// Cached fish list properties so we don't have to initalize fish every time, init deffered GLOBAL_LIST(fishing_property_cache) @@ -113,11 +148,11 @@ GLOBAL_LIST(fishing_property_cache) 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) + var/obj/item/fish/fish = new fish_type(null, FALSE) 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() + fish_property_table[fish_type][NAMEOF(fish, fish_traits)] = fish.fish_traits.Copy() QDEL_NULL(fish) GLOB.fishing_property_cache = fish_property_table return GLOB.fishing_property_cache @@ -132,6 +167,8 @@ GLOBAL_LIST(fishing_property_cache) if("Foodtype") var/obj/item/food/food_bait = bait return istype(food_bait) && food_bait.foodtypes & special_identifier["Value"] + if("Reagent") + return bait.reagents?.has_reagent(special_identifier["Value"], special_identifier["Amount"], check_subtypes = TRUE) else CRASH("Unknown bait identifier in fish favourite/disliked list") else @@ -173,20 +210,18 @@ GLOBAL_LIST(fishing_property_cache) 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)] + // Apply fish trait modifiers + var/list/fish_traits = fish_list_properties[caught_fish][NAMEOF(caught_fish, fish_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/datum/fish_trait/trait = GLOB.fish_traits[fish_trait] var/list/mod = trait.catch_weight_mod(rod, fisherman) additive_mod += mod[ADDITIVE_FISHING_MOD] multiplicative_mod *= mod[MULTIPLICATIVE_FISHING_MOD] diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm index d4ff7cd9856..7d82a5d4b46 100644 --- a/code/modules/fishing/sources/source_types.dm +++ b/code/modules/fishing/sources/source_types.dm @@ -8,6 +8,9 @@ /obj/item/fish/greenchromis = 15, /obj/item/fish/lanternfish = 5 ) + fish_counts = list( + /obj/item/fish/clownfish/lube = 2, + ) fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 5 /datum/fish_source/ocean/beach @@ -32,7 +35,6 @@ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 5 - /datum/fish_source/chasm/roll_reward(obj/item/fishing_rod/rod, mob/fisherman) var/rolled_reward = ..() @@ -41,7 +43,6 @@ return rod.hook.chasm_detritus_type - /datum/fish_source/lavaland catalog_description = "Lava vents" background = "fishing_background_lavaland" @@ -65,11 +66,86 @@ 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/lavaland/icemoon + catalog_description = "Liquid plasma vents" + fish_table = list( + FISHING_DUD = 5, + /obj/item/fish/chasm_crab/ice = 15, + /mob/living/simple_animal/hostile/asteroid/lobstrosity = 1, + /obj/effect/decal/remains/plasma = 1, + /obj/item/stack/ore/plasma = 3, + /obj/item/coin/plasma = 3, + + ) + fish_counts = list( + /obj/item/stack/sheet/mineral/adamantine = 3, + /obj/item/stack/sheet/mineral/mythril = 2, + ) /datum/fish_source/moisture_trap - catalog_description = "moisture trap basins" + catalog_description = "Moisture trap basins" fish_table = list( FISHING_DUD = 20, - /obj/item/fish/ratfish = 10 + /obj/item/fish/ratfish = 10, + /obj/item/fish/slimefish = 4 ) fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10 + +/datum/fish_source/toilet + catalog_description = "Station toilets" + duds = list("ewww... nothing", "it was nothing", "it was toilet paper", "it was flushed away", "the hook is empty", "where's the damn money?!") + fish_table = list( + FISHING_DUD = 18, + /obj/item/fish/sludgefish = 18, + /obj/item/fish/slimefish = 2, + ) + fish_counts = list( + /obj/item/storage/wallet/money = 2, + ) + fishing_difficulty = FISHING_DEFAULT_DIFFICULTY - 5 //For beginners + +/datum/fish_source/holographic + catalog_description = "Holographic water" + fish_table = list( + /obj/item/fish/holo = 2, + /obj/item/fish/holo/crab = 2, + /obj/item/fish/holo/puffer = 2, + /obj/item/fish/holo/angel = 2, + /obj/item/fish/holo/clown = 2, + ) + fish_counts = list( + /obj/item/fish/holo/checkered = 1, + /obj/item/fish/holo/halffish = 1, + ) + fishing_difficulty = FISHING_DEFAULT_DIFFICULTY - 5 + +/datum/fish_source/holographic/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman) + . = ..() + if(!istype(get_area(fisherman), /area/station/holodeck)) + return "You need to be inside the Holodeck to catch holographic fish." + +/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user) + RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_area)) + +/datum/fish_source/holographic/proc/check_area(mob/user) + SIGNAL_HANDLER + if(!istype(get_area(user), /area/station/holodeck)) + interrupt_challenge("exited holodeck") + +/datum/fish_source/holographic/on_challenge_completed(datum/fishing_challenge/source, mob/user, success, perfect) + . = ..() + UnregisterSignal(user, COMSIG_MOVABLE_MOVED) + +/datum/fish_source/oil_well + catalog_description = "Oil wells" + fish_table = list( + FISHING_DUD = 5, + /obj/item/fish/boned = 10, + /obj/item/stack/sheet/bone = 2, + ) + fish_counts = list( + /obj/item/clothing/gloves/bracer = 1, + /obj/effect/decal/remains/human = 1, + /obj/item/fish/mastodon = 1, + ) + fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15 diff --git a/code/modules/holodeck/computer.dm b/code/modules/holodeck/computer.dm index cf7778bcdf7..085225e34f3 100644 --- a/code/modules/holodeck/computer.dm +++ b/code/modules/holodeck/computer.dm @@ -269,42 +269,54 @@ GLOBAL_LIST_INIT(typecache_holodeck_linked_floorcheck_ok, typecacheof(list(/turf if(QDELETED(holo_atom)) spawned -= holo_atom continue - - RegisterSignal(holo_atom, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) - holo_atom.flags_1 |= HOLOGRAM_1 - - if(isholoeffect(holo_atom))//activates holo effects and transfers them from the spawned list into the effects list - var/obj/effect/holodeck_effect/holo_effect = holo_atom - effects += holo_effect - spawned -= holo_effect - var/atom/holo_effect_product = holo_effect.activate(src)//change name - if(istype(holo_effect_product)) - spawned += holo_effect_product // we want mobs or objects spawned via holoeffects to be tracked as objects - RegisterSignal(holo_effect_product, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) - if(islist(holo_effect_product)) - for(var/atom/atom_product as anything in holo_effect_product) - RegisterSignal(atom_product, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) - continue - - if(isobj(holo_atom)) - var/obj/holo_object = holo_atom - holo_object.resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - - if(isstructure(holo_object)) - holo_object.flags_1 |= NODECONSTRUCT_1 - continue - - if(ismachinery(holo_object)) - var/obj/machinery/holo_machine = holo_object - holo_machine.flags_1 |= NODECONSTRUCT_1 - holo_machine.power_change() - - if(istype(holo_machine, /obj/machinery/button)) - var/obj/machinery/button/holo_button = holo_machine - holo_button.setup_device() - + finalize_spawned(holo_atom) spawning_simulation = FALSE +/obj/machinery/computer/holodeck/proc/finalize_spawned(atom/holo_atom) + RegisterSignal(holo_atom, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) + holo_atom.flags_1 |= HOLOGRAM_1 + + if(isholoeffect(holo_atom))//activates holo effects and transfers them from the spawned list into the effects list + var/obj/effect/holodeck_effect/holo_effect = holo_atom + effects += holo_effect + spawned -= holo_effect + var/atom/holo_effect_product = holo_effect.activate(src)//change name + if(istype(holo_effect_product)) + spawned += holo_effect_product // we want mobs or objects spawned via holoeffects to be tracked as objects + RegisterSignal(holo_effect_product, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) + if(islist(holo_effect_product)) + for(var/atom/atom_product as anything in holo_effect_product) + RegisterSignal(atom_product, COMSIG_QDELETING, PROC_REF(remove_from_holo_lists)) + return + + if(isobj(holo_atom)) + var/obj/holo_object = holo_atom + holo_object.resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + + if(isstructure(holo_object)) + holo_object.flags_1 |= NODECONSTRUCT_1 + return + + if(ismachinery(holo_object)) + var/obj/machinery/holo_machine = holo_object + holo_machine.flags_1 |= NODECONSTRUCT_1 + holo_machine.power_change() + + if(istype(holo_machine, /obj/machinery/button)) + var/obj/machinery/button/holo_button = holo_machine + holo_button.setup_device() + +/** + * A separate proc for objects that weren't loaded by the template nor spawned by holo effects + * yet need to be added to the list of spawned objects. (e.g. holographic fishes) + */ +/obj/machinery/computer/holodeck/proc/add_to_spawned(atom/holo_atom) + spawned |= holo_atom + if(!(obj_flags & EMAGGED) && isitem(holo_atom)) + var/obj/item/to_be_nerfed = holo_atom + to_be_nerfed.damtype = STAMINA + finalize_spawned(holo_atom) + ///this qdels holoitems that should no longer exist for whatever reason /obj/machinery/computer/holodeck/proc/derez(atom/movable/holo_atom, silent = TRUE, forced = FALSE) spawned -= holo_atom diff --git a/code/modules/holodeck/turfs.dm b/code/modules/holodeck/turfs.dm index 2f0b415d075..28482873aeb 100644 --- a/code/modules/holodeck/turfs.dm +++ b/code/modules/holodeck/turfs.dm @@ -91,6 +91,10 @@ icon_state = "water" bullet_sizzle = TRUE +/turf/open/floor/holofloor/beach/water/Initialize(mapload) + . = ..() + AddComponent(/datum/component/fishing_spot, /datum/fish_source/holographic) + /turf/open/floor/holofloor/asteroid gender = PLURAL name = "asteroid sand" diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm index 435b863868f..80b81d988cb 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm @@ -90,6 +90,9 @@ .=..() create_reagents(20) reagents.add_reagent(dispensedreagent, 20) + //I'm pretty much aware that, because how oil wells and sinks work, attackby() won't work unless in combat mode. + //Thankfully, the user can cast the line from a distance. + AddComponent(/datum/component/fishing_spot, /datum/fish_source/oil_well) /obj/structure/sink/oil_well/attack_hand(mob/user, list/modifiers) flick("puddle-oil-splash",src) diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index 38a889f6418..f772c4756cf 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -163,15 +163,16 @@ I.forceMove(src) held_items[hand_index] = I SET_PLANE_EXPLICIT(I, ABOVE_HUD_PLANE, src) - I.equipped(src, ITEM_SLOT_HANDS) - if(QDELETED(I)) // this is here because some ABSTRACT items like slappers and circle hands could be moved from hand to hand then delete, which meant you'd have a null in your hand until you cleared it (say, by dropping it) - held_items[hand_index] = null - return FALSE if(I.pulledby) I.pulledby.stop_pulling() + if(!I.on_equipped(src, ITEM_SLOT_HANDS)) + return FALSE update_held_items() I.pixel_x = I.base_pixel_x I.pixel_y = I.base_pixel_y + if(QDELETED(I)) // this is here because some ABSTRACT items like slappers and circle hands could be moved from hand to hand then delete, which meant you'd have a null in your hand until you cleared it (say, by dropping it) + held_items[hand_index] = null + return FALSE return hand_index //Puts the item into the first available left hand if possible and calls all necessary triggers/updates. returns 1 on success. diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm index 93e27cc869f..fc96815045d 100644 --- a/code/modules/mob/living/carbon/inventory.dm +++ b/code/modules/mob/living/carbon/inventory.dm @@ -130,7 +130,7 @@ /// This proc is called after an item has been successfully handled and equipped to a slot. /mob/living/carbon/proc/has_equipped(obj/item/item, slot, initial = FALSE) - return item.equipped(src, slot, initial) + return item.on_equipped(src, slot, initial) /mob/living/carbon/doUnEquip(obj/item/I, force, newloc, no_move, invdrop = TRUE, silent = FALSE) . = ..() //Sets the default return value to what the parent returns. diff --git a/code/modules/mob/living/silicon/robot/inventory.dm b/code/modules/mob/living/silicon/robot/inventory.dm index 1d55351b02f..7df2e7d3390 100644 --- a/code/modules/mob/living/silicon/robot/inventory.dm +++ b/code/modules/mob/living/silicon/robot/inventory.dm @@ -64,7 +64,6 @@ item_module.screen_loc = inv3.screen_loc held_items[module_num] = item_module - item_module.equipped(src, ITEM_SLOT_HANDS) item_module.mouse_opacity = initial(item_module.mouse_opacity) SET_PLANE_EXPLICIT(item_module, ABOVE_HUD_PLANE, src) item_module.forceMove(src) @@ -78,6 +77,7 @@ if(storage_was_closed) hud_used.toggle_show_robot_modules() + item_module.on_equipped(src, ITEM_SLOT_HANDS) return TRUE /** diff --git a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm index c8d82f69957..9c620b8a089 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm @@ -80,7 +80,7 @@ return //Call back for item being equipped to drone - equipping.equipped(src, slot) + equipping.on_equipped(src, slot) /mob/living/simple_animal/drone/getBackSlot() return ITEM_SLOT_DEX_STORAGE diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm index c26ba8fc27d..995d9f432c1 100644 --- a/code/modules/mob/living/simple_animal/guardian/guardian.dm +++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm @@ -450,8 +450,8 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians equipping.screen_loc = null // will get moved if inventory is visible equipping.forceMove(src) - equipping.equipped(src, slot) SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src) + equipping.on_equipped(src, slot) /mob/living/simple_animal/hostile/guardian/proc/apply_overlay(cache_index) if((. = guardian_overlays[cache_index])) diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm index a0e8d240452..f9b5a2b18de 100644 --- a/code/modules/reagents/chemistry/holder.dm +++ b/code/modules/reagents/chemistry/holder.dm @@ -449,20 +449,27 @@ * Reagent takes a PATH to a reagent. * Amount checks for having a specific amount of that chemical. * Needs matabolizing takes into consideration if the chemical is matabolizing when it's checked. + * Check subtypes controls whether it should it should also include subtypes: ispath(type, reagent) versus type == reagent. */ -/datum/reagents/proc/has_reagent(reagent, amount = -1, needs_metabolizing = FALSE) +/datum/reagents/proc/has_reagent(reagent, amount = -1, needs_metabolizing = FALSE, check_subtypes = FALSE) var/list/cached_reagents = reagent_list for(var/datum/reagent/holder_reagent as anything in cached_reagents) - if (holder_reagent.type == reagent) + if (check_subtypes ? ispath(holder_reagent.type, reagent) : holder_reagent.type == reagent) if(!amount) if(needs_metabolizing && !holder_reagent.metabolizing) + if(check_subtypes) + continue return FALSE return holder_reagent else if(round(holder_reagent.volume, CHEMICAL_QUANTISATION_LEVEL) >= amount) if(needs_metabolizing && !holder_reagent.metabolizing) + if(check_subtypes) + continue return FALSE return holder_reagent + else if(!check_subtypes) + return FALSE return FALSE /** diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index f7fc8dcf83a..fcc3c365b50 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -1264,3 +1264,106 @@ /datum/reagent/toxin/viperspider/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) affected_mob.adjust_hallucinations(10 SECONDS * REM * seconds_per_tick) return ..() + +/datum/reagent/toxin/tetrodotoxin + name = "Tetrodotoxin" + description = "A colorless, oderless, tasteless neurotoxin usually carried by livers of animals of the Tetraodontiformes order." + silent_toxin = TRUE + reagent_state = SOLID + color = "#EEEEEE" + metabolization_rate = 0.1 * REAGENTS_METABOLISM + toxpwr = 0 + taste_mult = 0 + chemical_flags = REAGENT_NO_RANDOM_RECIPE + var/list/traits_not_applied = list( + TRAIT_PARALYSIS_L_ARM = BODY_ZONE_L_ARM, + TRAIT_PARALYSIS_R_ARM = BODY_ZONE_R_ARM, + TRAIT_PARALYSIS_L_LEG = BODY_ZONE_L_LEG, + TRAIT_PARALYSIS_R_LEG = BODY_ZONE_R_LEG, + ) + +/datum/reagent/toxin/tetrodotoxin/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) + //be ready for a cocktail of symptoms, including: + //numbness, nausea, vomit, breath loss, weakness, paralysis and nerve damage/impairment and eventually a heart attack if enough time passes. + switch(current_cycle) + if(6 to 12) + if(SPT_PROB(20, seconds_per_tick)) + affected_mob.set_jitter_if_lower(rand(2 SECONDS, 3 SECONDS) * REM * seconds_per_tick) + if(SPT_PROB(5, seconds_per_tick)) + var/obj/item/organ/internal/tongue/tongue = affected_mob.get_organ_slot(ORGAN_SLOT_TONGUE) + if(tongue) + to_chat(affected_mob, span_warning("your [tongue.name] feels numb...")) + affected_mob.set_slurring_if_lower(5 SECONDS * REM * seconds_per_tick) + affected_mob.adjust_disgust(3.5 * REM * seconds_per_tick) + if(12 to 20) + silent_toxin = FALSE + toxpwr = 0.5 + affected_mob.adjustStaminaLoss(2.5 * REM * seconds_per_tick, 0) + if(SPT_PROB(20, seconds_per_tick)) + affected_mob.losebreath += 1 * REM * seconds_per_tick + if(SPT_PROB(40, seconds_per_tick)) + affected_mob.set_jitter_if_lower(rand(2 SECONDS, 3 SECONDS) * REM * seconds_per_tick) + affected_mob.adjust_disgust(3 * REM * seconds_per_tick) + affected_mob.set_slurring_if_lower(1 SECONDS * REM * seconds_per_tick) + affected_mob.adjustStaminaLoss(2 * REM * seconds_per_tick, 0) + if(SPT_PROB(4, seconds_per_tick)) + paralyze_limb(affected_mob) + if(SPT_PROB(10, seconds_per_tick)) + affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS)) + if(20 to 28) + toxpwr = 1 + affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 0.5) + if(SPT_PROB(40, seconds_per_tick)) + affected_mob.losebreath += 2 * REM * seconds_per_tick + affected_mob.adjust_disgust(3 * REM * seconds_per_tick) + affected_mob.set_slurring_if_lower(3 SECONDS * REM * seconds_per_tick) + if(SPT_PROB(5, seconds_per_tick)) + to_chat(affected_mob, span_danger("you feel horribly weak.")) + affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, 0) + if(SPT_PROB(8, seconds_per_tick)) + paralyze_limb(affected_mob) + if(SPT_PROB(10, seconds_per_tick)) + affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS)) + if(28 to INFINITY) + toxpwr = 1.5 + affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 1, BRAIN_DAMAGE_DEATH) + affected_mob.set_silence_if_lower(3 SECONDS * REM * seconds_per_tick) + affected_mob.adjustStaminaLoss(5 * REM * seconds_per_tick, 0) + affected_mob.adjust_disgust(2 * REM * seconds_per_tick) + if(SPT_PROB(15, seconds_per_tick)) + paralyze_limb(affected_mob) + if(SPT_PROB(10, seconds_per_tick)) + affected_mob.adjust_confusion(rand(6 SECONDS, 8 SECONDS)) + + if(current_cycle >= 38 && !length(traits_not_applied) && SPT_PROB(5, seconds_per_tick) && !affected_mob.undergoing_cardiac_arrest()) + affected_mob.set_heartattack(TRUE) + to_chat(affected_mob, span_danger("you feel a burning pain spread throughout your chest, oh no...")) + + return ..() + +/datum/reagent/toxin/tetrodotoxin/proc/paralyze_limb(mob/living/affected_mob) + if(!length(traits_not_applied)) + return + var/added_trait = pick(traits_not_applied) + ADD_TRAIT(affected_mob, added_trait, REF(src)) + traits_not_applied -= added_trait + +/datum/reagent/toxin/tetrodotoxin/on_mob_metabolize(mob/living/affected_mob) + RegisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath)) + +/datum/reagent/toxin/tetrodotoxin/on_mob_end_metabolize(mob/living/affected_mob) + UnregisterSignal(affected_mob, COMSIG_CARBON_ATTEMPT_BREATHE, PROC_REF(block_breath)) + // the initial() proc doesn't work for lists. + var/list/initial_list = list( + TRAIT_PARALYSIS_L_ARM = BODY_ZONE_L_ARM, + TRAIT_PARALYSIS_R_ARM = BODY_ZONE_R_ARM, + TRAIT_PARALYSIS_L_LEG = BODY_ZONE_L_LEG, + TRAIT_PARALYSIS_R_LEG = BODY_ZONE_R_LEG, + ) + affected_mob.remove_traits(initial_list, REF(src)) + traits_not_applied = initial_list + +/datum/reagent/toxin/tetrodotoxin/proc/block_breath(mob/living/source) + SIGNAL_HANDLER + if(current_cycle >= 28) + return COMSIG_CARBON_BLOCK_BREATH diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index ef2844bb132..8dbfeb65c17 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -979,6 +979,7 @@ /datum/design/fishing_rod_tech name = "Advanced Fishing Rod" + desc = "A fishing rod with an embedded generator dispensing an infinite supply of fishing baits." id = "fishing_rod_tech" build_type = PROTOLATHE | AWAY_LATHE materials = list(/datum/material/uranium =HALF_SHEET_MATERIAL_AMOUNT, /datum/material/plastic =SHEET_MATERIAL_AMOUNT) @@ -988,6 +989,30 @@ ) departmental_flags = DEPARTMENT_BITFLAG_SERVICE +/datum/design/stabilized_hook + name = "Gyro-Stabilized Hook" + desc = "An advanced fishing hook that gives the user a tighter control on the fish when reeling in." + id = "stabilized_hook" + build_type = PROTOLATHE | AWAY_LATHE + materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 5, /datum/material/gold = SMALL_MATERIAL_AMOUNT * 3, /datum/material/titanium = SMALL_MATERIAL_AMOUNT * 2) + build_path = /obj/item/fishing_hook/stabilized + category = list( + RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SERVICE + ) + departmental_flags = DEPARTMENT_BITFLAG_SERVICE + +/datum/design/fish_analyzer + name = "Fish Analyzer" + desc = "An analyzer used to monitor fish's status and traits with." + id = "fish_analyzer" + build_type = PROTOLATHE | AWAY_LATHE + materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 0.5) + build_path = /obj/item/fish_analyzer + category = list( + RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SERVICE + ) + departmental_flags = DEPARTMENT_BITFLAG_SERVICE + ///////////////////////////////////////// /////////Coffeemaker Stuff/////////////// ///////////////////////////////////////// diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index faa95ac7da6..69e8e821286 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -2424,7 +2424,9 @@ description = "Cutting edge fishing advancements." prereq_ids = list("base") design_ids = list( - "fishing_rod_tech" + "fishing_rod_tech", + "stabilized_hook", + "fish_analyzer", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) hidden = TRUE diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 674016bcafa..0f831dfd926 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -26,6 +26,18 @@ var/initial_inline_css var/mouse_event_macro_set = FALSE + /** + * Static list used to map in macros that will then emit execute events to the tgui window + * A small disclaimer though I'm no tech wiz: I don't think it's possible to map in right or middle + * clicks in the current state, as they're keywords rather than modifiers. + */ + var/static/list/byondToTguiEventMap = list( + "MouseDown" = "byond/mousedown", + "MouseUp" = "byond/mouseup", + "Ctrl" = "byond/ctrldown", + "Ctrl+UP" = "byond/ctrlup", + ) + /** * public * @@ -381,11 +393,6 @@ 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) @@ -406,10 +413,6 @@ /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/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 8d69bd3b7bf..46af8987bca 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -125,6 +125,7 @@ #include "egg_glands.dm" #include "emoting.dm" #include "explosion_action.dm" +#include "fish_unit_tests.dm" #include "focus_only_tests.dm" #include "font_awesome_icons.dm" #include "food_edibility_check.dm" diff --git a/code/modules/unit_tests/fish_unit_tests.dm b/code/modules/unit_tests/fish_unit_tests.dm new file mode 100644 index 00000000000..6c4a278a396 --- /dev/null +++ b/code/modules/unit_tests/fish_unit_tests.dm @@ -0,0 +1,127 @@ +#define TRAIT_FISH_TESTING "made_you_read_this" + +///Checks that things associated with fish size and weight work correctly. +/datum/unit_test/fish_size_weight + +/datum/unit_test/fish_size_weight/Run() + var/obj/item/fish/fish = allocate(/obj/item/fish/testdummy) + TEST_ASSERT_EQUAL(fish.grind_results[/datum/reagent], 20, "the test fish has [fish.grind_results[/datum/reagent]] units of reagent when it should have 20") + TEST_ASSERT_EQUAL(fish.w_class, WEIGHT_CLASS_BULKY, "the test fish has w_class of [fish.w_class] when it should have been [WEIGHT_CLASS_BULKY]") + var/expected_num_fillets = round(FISH_SIZE_BULKY_MAX / FISH_FILLET_NUMBER_SIZE_DIVISOR * 2, 1) + TEST_ASSERT_EQUAL(fish.num_fillets, expected_num_fillets, "the test fish has [fish.num_fillets] number of fillets when it should have [expected_num_fillets]") + +///Checks that fish breeding works correctly. +/datum/unit_test/fish_breeding + +/datum/unit_test/fish_breeding/Run() + var/obj/item/fish/fish = allocate(/obj/item/fish/testdummy) + ///Check if the fishes can generate offsprings at all. + var/obj/item/fish/fish_two = allocate(/obj/item/fish/testdummy/two) + var/obj/item/fish/new_fish = fish.create_offspring(fish_two.type, fish_two) + TEST_ASSERT(new_fish, "the two test fishes couldn't generate an offspring") + var/traits_len = length(new_fish.fish_traits) + TEST_ASSERT_NOTEQUAL(traits_len, 2, "the offspring of the test fishes has both parents' traits, which are incompatible with each other") + TEST_ASSERT_NOTEQUAL(traits_len, 0, "the offspring has neither of the parents' traits") + TEST_ASSERT(HAS_TRAIT(new_fish, TRAIT_FISH_TESTING), "The offspring doesn't have the relative datum trait associated with its fish trait") + + ///Check that crossbreeder, no-mating and self-reproductive fish traits work correctly. + var/obj/structure/aquarium/traits/aquarium = allocate(/obj/structure/aquarium/traits) + TEST_ASSERT(!aquarium.sterile.try_to_reproduce(), "The test aquarium's sterile fish managed to reproduce when it shouldn't have") + var/obj/item/fish/crossbreeder_jr = aquarium.crossbreeder.try_to_reproduce() + TEST_ASSERT(crossbreeder_jr, "The test aquarium's crossbreeder fish didn't manage to reproduce when it should have.") + TEST_ASSERT_EQUAL(crossbreeder_jr.type, aquarium.cloner.type, "The test aquarium's crossbreeder fish mated with the wrong type of fish") + var/obj/item/fish/cloner_jr = aquarium.cloner.try_to_reproduce() + TEST_ASSERT(cloner_jr, "The test aquarium's cloner fish didn't manage to reproduce when it should have.") + TEST_ASSERT_NOTEQUAL(cloner_jr.type, aquarium.sterile.type, "The test aquarium's cloner fish mated with the sterile fish") + +///Checks that fish evolutions work correctly. +/datum/unit_test/fish_evolution + +/datum/unit_test/fish_evolution/Run() + var/obj/structure/aquarium/evolution/aquarium = allocate(/obj/structure/aquarium/evolution) + var/obj/item/fish/evolve_jr = aquarium.evolve.try_to_reproduce() + TEST_ASSERT(evolve_jr, "The test aquarium's evolution fish didn't manage to reproduce when it should have") + TEST_ASSERT_NOTEQUAL(evolve_jr.type, /obj/item/fish/goldfish, "The test aquarium's evolution fish managed to pass the conditions of an impossible evolution.") + TEST_ASSERT_EQUAL(evolve_jr.type, /obj/item/fish/clownfish, "The test aquarium's evolution fish's offspring isn't of the expected type") + TEST_ASSERT(!(/datum/fish_trait/dummy in evolve_jr.fish_traits), "The test aquarium's evolution fish's offspring still has the old trait that ought to be removed by the evolution datum") + TEST_ASSERT(/datum/fish_trait/dummy/two in evolve_jr.fish_traits, "The test aquarium's evolution fish's offspring doesn't have the evolution trait") + +///dummy fish item used for the tests, as well with related subtypes and datums. +/obj/item/fish/testdummy + grind_results = list() + average_weight = FISH_GRIND_RESULTS_WEIGHT_DIVISOR * 2 + average_size = FISH_SIZE_BULKY_MAX + num_fillets = 2 + fish_traits = list(/datum/fish_trait/dummy) + stable_population = INFINITY + breeding_timeout = 0 + +/obj/item/fish/testdummy/two + fish_traits = list(/datum/fish_trait/dummy/two) + +/datum/fish_trait/dummy + incompatible_traits = list(/datum/fish_trait/dummy/two) + inheritability = 100 + diff_traits_inheritability = 100 + +/datum/fish_trait/dummy/apply_to_fish(obj/item/fish/fish) + ADD_TRAIT(fish, TRAIT_FISH_TESTING, FISH_TRAIT_DATUM) + fish.grind_results[/datum/reagent] = 10 + +/datum/fish_trait/dummy/two + incompatible_traits = list(/datum/fish_trait/dummy) + +/obj/structure/aquarium/traits + allow_breeding = TRUE + var/obj/item/fish/testdummy/crossbreeder/crossbreeder + var/obj/item/fish/testdummy/cloner/cloner + var/obj/item/fish/testdummy/sterile/sterile + +/obj/structure/aquarium/traits/Initialize(mapload) + . = ..() + crossbreeder = new(src) + cloner = new(src) + sterile = new(src) + +/obj/item/fish/testdummy/crossbreeder + fish_traits = list(/datum/fish_trait/crossbreeder) + +/obj/item/fish/testdummy/cloner + fish_traits = list(/datum/fish_trait/parthenogenesis) + +/obj/item/fish/testdummy/sterile + fish_traits = list(/datum/fish_trait/no_mating) + +/obj/structure/aquarium/evolution + allow_breeding = TRUE + var/obj/item/fish/testdummy/evolve/evolve + var/obj/item/fish/testdummy/evolve_two/evolve_two + +/obj/structure/aquarium/evolution/Initialize(mapload) + . = ..() + evolve = new(src) + evolve_two = new(src) + +/obj/item/fish/testdummy/evolve + compatible_types = list(/obj/item/fish/testdummy/evolve_two) + evolution_types = list(/datum/fish_evolution/dummy) + +/obj/item/fish/testdummy/evolve_two + compatible_types = list(/obj/item/fish/testdummy/evolve) + evolution_types = list(/datum/fish_evolution/dummy/two) + +/datum/fish_evolution/dummy + probability = 200 //Guaranteed chance even if halved. + new_fish_type = /obj/item/fish/clownfish + new_traits = list(/datum/fish_trait/dummy/two) + removed_traits = list(/datum/fish_trait/dummy) + +/datum/fish_evolution/dummy/two + new_fish_type = /obj/item/fish/goldfish + +/datum/fish_evolution/dummy/two/New() + . = ..() + probability = 0 //works around the global list initialization skipping abstract/impossible evolutions. + +#undef TRAIT_FISH_TESTING + diff --git a/icons/mob/clothing/belt.dmi b/icons/mob/clothing/belt.dmi index a37c28fc737..29b75b5f336 100644 Binary files a/icons/mob/clothing/belt.dmi and b/icons/mob/clothing/belt.dmi differ diff --git a/icons/mob/clothing/head/hats.dmi b/icons/mob/clothing/head/hats.dmi index aa7f9fe6c8e..3676c8f5485 100644 Binary files a/icons/mob/clothing/head/hats.dmi and b/icons/mob/clothing/head/hats.dmi differ diff --git a/icons/mob/inhands/clothing/hats_lefthand.dmi b/icons/mob/inhands/clothing/hats_lefthand.dmi index 4133f3d1ad9..7111ee4d748 100644 Binary files a/icons/mob/inhands/clothing/hats_lefthand.dmi and b/icons/mob/inhands/clothing/hats_lefthand.dmi differ diff --git a/icons/mob/inhands/clothing/hats_righthand.dmi b/icons/mob/inhands/clothing/hats_righthand.dmi index a7d3c60f5f3..96756fc44db 100644 Binary files a/icons/mob/inhands/clothing/hats_righthand.dmi and b/icons/mob/inhands/clothing/hats_righthand.dmi differ diff --git a/icons/mob/inhands/fish_lefthand.dmi b/icons/mob/inhands/fish_lefthand.dmi new file mode 100644 index 00000000000..f5aeef62027 Binary files /dev/null and b/icons/mob/inhands/fish_lefthand.dmi differ diff --git a/icons/mob/inhands/fish_righthand.dmi b/icons/mob/inhands/fish_righthand.dmi new file mode 100644 index 00000000000..d9c23c3d1b1 Binary files /dev/null and b/icons/mob/inhands/fish_righthand.dmi differ diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi index 113fd372586..f544eeb753a 100644 Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi index 344568b8e22..85c6accc80c 100644 Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ diff --git a/icons/obj/aquarium.dmi b/icons/obj/aquarium.dmi index b5de3de4e92..3a27c83c906 100644 Binary files a/icons/obj/aquarium.dmi and b/icons/obj/aquarium.dmi differ diff --git a/icons/obj/aquarium_wide.dmi b/icons/obj/aquarium_wide.dmi new file mode 100644 index 00000000000..7c5f941a1db Binary files /dev/null and b/icons/obj/aquarium_wide.dmi differ diff --git a/icons/obj/clothing/head/hats.dmi b/icons/obj/clothing/head/hats.dmi index e8211e53a19..f420521a0aa 100644 Binary files a/icons/obj/clothing/head/hats.dmi and b/icons/obj/clothing/head/hats.dmi differ diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi index 4bcb2b1bfab..b6ccf1b8c41 100644 Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi index 1b7d03dd13d..39bcc853442 100644 Binary files a/icons/obj/fishing.dmi and b/icons/obj/fishing.dmi differ diff --git a/icons/ui_icons/achievements/achievements.dmi b/icons/ui_icons/achievements/achievements.dmi index ba4ee706431..4975509a10f 100644 Binary files a/icons/ui_icons/achievements/achievements.dmi and b/icons/ui_icons/achievements/achievements.dmi differ diff --git a/modular_skyrat/modules/primitive_production/code/production_skill.dm b/modular_skyrat/modules/primitive_production/code/production_skill.dm index 3630065359d..50fc0d30ca5 100644 --- a/modular_skyrat/modules/primitive_production/code/production_skill.dm +++ b/modular_skyrat/modules/primitive_production/code/production_skill.dm @@ -6,7 +6,7 @@ SKILL_SPEED_MODIFIER = list(1, 0.95, 0.9, 0.85, 0.75, 0.6, 0.5), SKILL_PROBS_MODIFIER = list(10, 15, 20, 25, 30, 35, 40) ) - skill_cape_path = /obj/item/clothing/neck/cloak/skill_reward/production + skill_item_path = /obj/item/clothing/neck/cloak/skill_reward/production /obj/item/clothing/neck/cloak/skill_reward/production name = "legendary producer's cloak" diff --git a/modular_skyrat/modules/reagent_forging/code/smith_skill.dm b/modular_skyrat/modules/reagent_forging/code/smith_skill.dm index 7fe379f0cbf..1170f957fc0 100644 --- a/modular_skyrat/modules/reagent_forging/code/smith_skill.dm +++ b/modular_skyrat/modules/reagent_forging/code/smith_skill.dm @@ -6,7 +6,7 @@ SKILL_SPEED_MODIFIER = list(1, 0.95, 0.9, 0.85, 0.75, 0.6, 0.5), SKILL_PROBS_MODIFIER = list(10, 15, 20, 25, 30, 35, 40) ) - skill_cape_path = /obj/item/clothing/neck/cloak/skill_reward/smithing + skill_item_path = /obj/item/clothing/neck/cloak/skill_reward/smithing /obj/item/clothing/neck/cloak/skill_reward/smithing name = "legendary smithy's cloak" diff --git a/tgstation.dme b/tgstation.dme index 9545f8bb116..6d81f08638c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -964,7 +964,7 @@ #include "code\datums\components\aggro_emote.dm" #include "code\datums\components\ai_retaliate_advanced.dm" #include "code\datums\components\anti_magic.dm" -#include "code\datums\components\aquarium.dm" +#include "code\datums\components\aquarium_content.dm" #include "code\datums\components\area_based_godmode.dm" #include "code\datums\components\area_sound_manager.dm" #include "code\datums\components\areabound.dm" @@ -1367,6 +1367,7 @@ #include "code\datums\elements\shatters_when_thrown.dm" #include "code\datums\elements\sideway_movement.dm" #include "code\datums\elements\simple_flying.dm" +#include "code\datums\elements\skill_reward.dm" #include "code\datums\elements\skittish.dm" #include "code\datums\elements\snail_crawl.dm" #include "code\datums\elements\soft_landing.dm" @@ -1542,6 +1543,7 @@ #include "code\datums\screentips\item_context.dm" #include "code\datums\skills\_skill.dm" #include "code\datums\skills\cleaning.dm" +#include "code\datums\skills\fishing.dm" #include "code\datums\skills\gaming.dm" #include "code\datums\skills\mining.dm" #include "code\datums\station_traits\_station_trait.dm" @@ -1589,6 +1591,7 @@ #include "code\datums\storage\subtypes\cards.dm" #include "code\datums\storage\subtypes\duffel_bag.dm" #include "code\datums\storage\subtypes\extract_inventory.dm" +#include "code\datums\storage\subtypes\fish_case.dm" #include "code\datums\storage\subtypes\implant.dm" #include "code\datums\storage\subtypes\organ_box.dm" #include "code\datums\storage\subtypes\pockets.dm" @@ -3587,11 +3590,13 @@ #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\aquarium\fish_analyzer.dm" #include "code\modules\fishing\fish\_fish.dm" #include "code\modules\fishing\fish\chasm_detritus.dm" +#include "code\modules\fishing\fish\fish_evolution.dm" +#include "code\modules\fishing\fish\fish_traits.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" diff --git a/tgui/packages/tgui/backend.ts b/tgui/packages/tgui/backend.ts index fb77a61e3cd..dd4cae9e622 100644 --- a/tgui/packages/tgui/backend.ts +++ b/tgui/packages/tgui/backend.ts @@ -146,6 +146,14 @@ export const backendMiddleware = (store) => { globalEvents.emit('byond/mouseup'); } + if (type === 'byond/ctrldown') { + globalEvents.emit('byond/ctrldown'); + } + + if (type === 'byond/ctrlup') { + globalEvents.emit('byond/ctrlup'); + } + 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.tsx b/tgui/packages/tgui/interfaces/FishCatalog.tsx index 4718e2a094b..0c9ba7ceb3c 100644 --- a/tgui/packages/tgui/interfaces/FishCatalog.tsx +++ b/tgui/packages/tgui/interfaces/FishCatalog.tsx @@ -96,7 +96,7 @@ export const FishCatalog = (props, context) => { {currentFish.weight} g - + {currentFish.fishing_tips.spots} diff --git a/tgui/packages/tgui/interfaces/Fishing.tsx b/tgui/packages/tgui/interfaces/Fishing.tsx index ba0196735f7..7e5e0f3ea50 100644 --- a/tgui/packages/tgui/interfaces/Fishing.tsx +++ b/tgui/packages/tgui/interfaces/Fishing.tsx @@ -1,11 +1,12 @@ import { clamp } from 'common/math'; +import { KEY_CTRL } from 'common/keycodes'; import { randomInteger, randomNumber, randomPick, randomProb } from 'common/random'; import { useDispatch } from 'common/redux'; import { Component } from 'inferno'; import { resolveAsset } from '../assets'; import { backendSuspendStart, useBackend } from '../backend'; -import { Icon } from '../components'; -import { globalEvents } from '../events'; +import { Icon, KeyListener } from '../components'; +import { globalEvents, KeyEvent } from '../events'; import { Window } from '../layouts'; type Bait = { @@ -26,6 +27,7 @@ type FishAI = 'dumb' | 'zippy' | 'slow'; enum ReelingState { Idle, Reeling, + ReelingDown, } type FishingMinigameProps = { @@ -43,7 +45,13 @@ type FishingMinigameState = { fish: Fish; }; -type SpecialRule = 'weighted' | 'limit_loss' | 'heavy'; +type SpecialRule = + | 'weighted' + | 'limit_loss' + | 'heavy' + | 'bidirectional' + | 'no_escape' + | 'lubed'; class FishingMinigame extends Component< FishingMinigameProps, @@ -63,6 +71,9 @@ class FishingMinigame extends Component< longJumpVelocityLimit: number = 200; shortJumpVelocityLimit: number = 400; idleVelocity: number = 0; + accel_up_coeff: number = 1; + bidirectional: boolean = false; + no_escape: boolean = false; baseLongJumpChancePerSecond: number = 0.0075; baseShortJumpChancePerSecond: number = 0.255; @@ -82,6 +93,9 @@ class FishingMinigame extends Component< : -6; this.baitBounceCoeff = props.special_rules.includes('weighted') ? 0.1 : 0.6; this.idleVelocity = props.special_rules.includes('heavy') ? 10 : 0; + this.bidirectional = props.special_rules.includes('bidirectional'); + this.no_escape = props.special_rules.includes('no_escape'); + this.accel_up_coeff = props.special_rules.includes('lubed') ? 1.4 : 1; switch (props.fish_ai) { case 'dumb': @@ -117,6 +131,10 @@ class FishingMinigame extends Component< this.handle_mousedown = this.handle_mousedown.bind(this); this.handle_mouseup = this.handle_mouseup.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + this.handle_ctrldown = this.handle_ctrldown.bind(this); + this.handle_ctrlup = this.handle_ctrlup.bind(this); this.updateAnimation = this.updateAnimation.bind(this); this.moveFish = this.moveFish.bind(this); this.moveBait = this.moveBait.bind(this); @@ -130,6 +148,8 @@ class FishingMinigame extends Component< this.animation_id = window.requestAnimationFrame(this.updateAnimation); globalEvents.on('byond/mousedown', this.handle_mousedown); globalEvents.on('byond/mouseup', this.handle_mouseup); + globalEvents.on('byond/ctrldown', this.handle_ctrldown); + globalEvents.on('byond/ctrlup', this.handle_ctrlup); } componentWillUnmount() { @@ -138,6 +158,8 @@ class FishingMinigame extends Component< window.cancelAnimationFrame(this.animation_id); globalEvents.off('byond/mousedown', this.handle_mousedown); globalEvents.off('byond/mouseup', this.handle_mouseup); + globalEvents.off('byond/ctrldown', this.handle_ctrldown); + globalEvents.off('byond/ctrlup', this.handle_ctrlup); } updateAnimation(timestamp: DOMHighResTimeStamp) { @@ -275,7 +297,7 @@ class FishingMinigame extends Component< const { fish, bait } = this.state; // Speedup when reeling - const acceleration_up = -1500; + const acceleration_up = -1500 * this.accel_up_coeff; // Gravity const acceleration_down = 1000; // Velocity is multiplied by this when bouncing off the bottom/top @@ -298,17 +320,31 @@ class FishingMinigame extends Component< // Bottom bound if (newPosition + bait.height > this.area_height) { newPosition = this.area_height - bait.height; - newVelocity = -bait.velocity * bounce_coeff; + if (this.reeling === ReelingState.ReelingDown) { + newVelocity = 0; + } else { + newVelocity = -bait.velocity * bounce_coeff; + } } const acceleration = this.reeling === ReelingState.Reeling ? acceleration_up - : acceleration_down; - const velocity_change = acceleration * seconds; + : this.reeling === ReelingState.ReelingDown + ? -acceleration_up + : acceleration_down; // Slowdown both ways when on fish - if (this.fishOnBait(fish, bait)) { - newVelocity += on_point_coeff * velocity_change; + const velocity_change = + acceleration * + seconds * + (this.fishOnBait(fish, bait) ? on_point_coeff : 1); + + if (this.bidirectional && this.reeling === ReelingState.Idle) { + if (newVelocity < 0) { + newVelocity = Math.min(newVelocity + velocity_change, 0); + } else { + newVelocity = Math.max(newVelocity - velocity_change, 0); + } } else { newVelocity += velocity_change; } @@ -351,7 +387,7 @@ class FishingMinigame extends Component< const dispatch = useDispatch(this.context); - if (newCompletion <= 0) { + if (newCompletion <= 0 && !this.no_escape) { this.props.lose(); dispatch(backendSuspendStart()); } else if (newCompletion >= 100) { @@ -377,7 +413,33 @@ class FishingMinigame extends Component< } handle_mouseup(event: MouseEvent) { - this.reeling = ReelingState.Idle; + if (this.reeling === ReelingState.Reeling) { + this.reeling = ReelingState.Idle; + } + } + + handleKeyDown(keyEvent: KeyEvent) { + if (keyEvent.code === KEY_CTRL) { + this.handle_ctrldown(); + } + } + + handleKeyUp(keyEvent: KeyEvent) { + if (keyEvent.code === KEY_CTRL) { + this.handle_ctrlup(); + } + } + + handle_ctrldown() { + if (this.bidirectional && this.reeling === ReelingState.Idle) { + this.reeling = ReelingState.ReelingDown; + } + } + + handle_ctrlup() { + if (this.bidirectional && this.reeling === ReelingState.ReelingDown) { + this.reeling = ReelingState.Idle; + } } render() { @@ -386,6 +448,10 @@ class FishingMinigame extends Component< const background_image = resolveAsset(this.props.background); return (
+