diff --git a/code/__DEFINES/aquarium.dm b/code/__DEFINES/aquarium.dm
deleted file mode 100644
index 6e0a39f5040..00000000000
--- a/code/__DEFINES/aquarium.dm
+++ /dev/null
@@ -1,44 +0,0 @@
-#define AQUARIUM_ANIMATION_FISH_SWIM "fish"
-#define AQUARIUM_ANIMATION_FISH_DEAD "dead"
-
-#define AQUARIUM_PROPERTIES_PX_MIN "px_min"
-#define AQUARIUM_PROPERTIES_PX_MAX "px_max"
-#define AQUARIUM_PROPERTIES_PY_MIN "py_min"
-#define AQUARIUM_PROPERTIES_PY_MAX "py_max"
-
-#define AQUARIUM_LAYER_MODE_BOTTOM "bottom"
-#define AQUARIUM_LAYER_MODE_TOP "top"
-#define AQUARIUM_LAYER_MODE_AUTO "auto"
-
-#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 (T0C + 24)
-
-#define FISH_RARITY_BASIC 1000
-#define FISH_RARITY_RARE 400
-#define FISH_RARITY_VERY_RARE 200
-#define FISH_RARITY_GOOD_LUCK_FINDING_THIS 1
-#define FISH_RARITY_NOPE 0
-
-#define AQUARIUM_FLUID_FRESHWATER "Freshwater"
-#define AQUARIUM_FLUID_SALTWATER "Saltwater"
-#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/dcs/signals/signals_fish.dm b/code/__DEFINES/dcs/signals/signals_fish.dm
index f65b8c99805..90e580e9166 100644
--- a/code/__DEFINES/dcs/signals/signals_fish.dm
+++ b/code/__DEFINES/dcs/signals/signals_fish.dm
@@ -24,5 +24,10 @@
#define COMSIG_FISHING_ROD_CAST "fishing_rod_cast"
#define FISHING_ROD_CAST_HANDLED (1 << 0)
+/// From /datum/fish_source/proc/dispense_reward(), not set if the reward is a dud: (reward, user)
+#define COMSIG_FISHING_ROD_CAUGHT_FISH "fishing_rod_caught_fish"
+/// From /obj/item/fishing_rod/proc/hook_item(): (reward, user)
+#define COMSIG_FISHING_ROD_HOOKED_ITEM "fishing_rod_hooked_item"
+
/// Sent when the challenge is to be interrupted: (reason)
#define COMSIG_FISHING_SOURCE_INTERRUPT_CHALLENGE "fishing_spot_interrupt_challenge"
diff --git a/code/__DEFINES/experisci.dm b/code/__DEFINES/experisci.dm
index 9130e0ae565..c74290ff5f7 100644
--- a/code/__DEFINES/experisci.dm
+++ b/code/__DEFINES/experisci.dm
@@ -26,6 +26,8 @@
/// Destructive experiments which will destroy the sample
#define EXPERIMENT_TRAIT_DESTRUCTIVE (1 << 0)
+/// Used by scanning experiments: instead of storing refs or be a number, the list for scanned atoms is used as typecache
+#define EXPERIMENT_TRAIT_TYPECACHE (1 << 1)
/// Will always attempt to action every experiment eligible with a single input,
/// no experiment selection required
@@ -33,3 +35,7 @@
/// Experiment handlers with this flag will not automatically connect to the first techweb they find
/// on initialization
#define EXPERIMENT_CONFIG_NO_AUTOCONNECT (1 << 1)
+/// Experiment handlers with this flag won't pester the user of objects not pertinent to the test or if no experiment is selected
+#define EXPERIMENT_CONFIG_SILENT_FAIL (1 << 2)
+/// Experiment handlers with this flag will bypass any delay when trying to scan something
+#define EXPERIMENT_CONFIG_IMMEDIATE_ACTION (1 << 3)
diff --git a/code/__DEFINES/fish.dm b/code/__DEFINES/fish.dm
new file mode 100644
index 00000000000..89b7963d91d
--- /dev/null
+++ b/code/__DEFINES/fish.dm
@@ -0,0 +1,136 @@
+/// Use in fish tables to denote miss chance.
+#define FISHING_DUD "dud"
+
+// Baseline fishing difficulty levels
+#define FISHING_DEFAULT_DIFFICULTY 15
+
+/// Difficulty modifier when bait is fish's favorite
+#define FAV_BAIT_DIFFICULTY_MOD -5
+/// Difficulty modifier when bait is fish's disliked
+#define DISLIKED_BAIT_DIFFICULTY_MOD 15
+/// Difficulty modifier when our fisherman has the trait TRAIT_SETTLER
+#define SETTLER_DIFFICULTY_MOD -5
+
+#define FISH_TRAIT_MINOR_DIFFICULTY_BOOST 5
+
+// These define how the fish will behave in the minigame
+#define FISH_AI_DUMB "dumb"
+#define FISH_AI_ZIPPY "zippy"
+#define FISH_AI_SLOW "slow"
+
+#define ADDITIVE_FISHING_MOD "additive"
+#define MULTIPLICATIVE_FISHING_MOD "multiplicative"
+
+// These defines are intended for use to interact with fishing hooks when going
+// through the fishing rod, and not the hook itself. They could probably be
+// handled differently, but for now that's how they work. It's grounds for
+// a future refactor, however.
+/// Fishing hook trait that signifies that it's shiny. Useful for fishes
+/// that care about shiner hooks more.
+#define FISHING_HOOK_SHINY (1 << 0)
+/// Fishing hook trait that lessens the bounce from hitting the edges of the minigame bar.
+#define FISHING_HOOK_WEIGHTED (1 << 1)
+///See FISHING_MINIGAME_RULE_BIDIRECTIONAL
+#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)
+///Automatically kills the fish after a while, at the cost of killing it.
+#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 fishing line.
+#define FISHING_LINE_BOUNCY (1 << 2)
+
+///Keeps the bait from falling from gravity, instead allowing the player to move the bait down with right click.
+#define FISHING_MINIGAME_RULE_BIDIRECTIONAL (1 << 2)
+///Prevents the player from losing the minigame when the completion reaches 0
+#define FISHING_MINIGAME_RULE_NO_ESCAPE (1 << 3)
+///Automatically kills the fish after a while, at the cost of killing it
+#define FISHING_MINIGAME_RULE_KILL (1 << 4)
+///Prevents the fishing skill from having an effect on the minigame and experience from being awarded
+#define FISHING_MINIGAME_RULE_NO_EXP (1 << 5)
+///If enabled, the minigame will occasionally screw around and invert the velocity of the bait
+#define FISHING_MINIGAME_RULE_ANTIGRAV (1 << 6)
+///Will filp the minigame hud for the duration of the effect
+#define FISHING_MINIGAME_RULE_FLIP (1 << 7)
+
+///all the effects that are active and will last for a few seconds before triggering a cooldown
+#define FISHING_MINIGAME_ACTIVE_EFFECTS (FISHING_MINIGAME_RULE_ANTIGRAV|FISHING_MINIGAME_RULE_FLIP)
+
+/// The default additive value for fishing hook catch weight modifiers.
+#define FISHING_DEFAULT_HOOK_BONUS_ADDITIVE 0
+/// The default multiplicative value for fishing hook catch weight modifiers.
+#define FISHING_DEFAULT_HOOK_BONUS_MULTIPLICATIVE 1
+
+//Fish icon defines, used by fishing minigame
+#define FISH_ICON_DEF "fish"
+#define FISH_ICON_HOSTILE "hostile"
+#define FISH_ICON_STAR "star"
+#define FISH_ICON_CHUNKY "chunky"
+#define FISH_ICON_SLIME "slime"
+#define FISH_ICON_COIN "coin"
+#define FISH_ICON_GEM "gem"
+#define FISH_ICON_CRAB "crab"
+#define FISH_ICON_JELLYFISH "jellyfish"
+#define FISH_ICON_BONE "bone"
+
+#define AQUARIUM_ANIMATION_FISH_SWIM "fish"
+#define AQUARIUM_ANIMATION_FISH_DEAD "dead"
+
+#define AQUARIUM_PROPERTIES_PX_MIN "px_min"
+#define AQUARIUM_PROPERTIES_PX_MAX "px_max"
+#define AQUARIUM_PROPERTIES_PY_MIN "py_min"
+#define AQUARIUM_PROPERTIES_PY_MAX "py_max"
+
+#define AQUARIUM_LAYER_MODE_BOTTOM "bottom"
+#define AQUARIUM_LAYER_MODE_TOP "top"
+#define AQUARIUM_LAYER_MODE_AUTO "auto"
+
+#define FISH_ALIVE "alive"
+#define FISH_DEAD "dead"
+
+///Fish size thresholds for w_class.
+#define FISH_SIZE_TINY_MAX 30
+#define FISH_SIZE_SMALL_MAX 50
+#define FISH_SIZE_NORMAL_MAX 90
+#define FISH_SIZE_BULKY_MAX 130
+
+///The coefficient for maximum weight/size divergence relative to the averages.
+#define MAX_FISH_DEVIATION_COEFF 2.5
+
+///The volume of the grind results is multiplied by the fish' weight and divided by this.
+#define FISH_GRIND_RESULTS_WEIGHT_DIVISOR 500
+///The number of fillets is multiplied by the fish' size and divided by this.
+#define FISH_FILLET_NUMBER_SIZE_DIVISOR 30
+
+///The breeding timeout for newly instantiated fish is multiplied by this.
+#define NEW_FISH_BREEDING_TIMEOUT_MULT 2
+///The last feeding timestamp of newly instantiated fish is multiplied by this: ergo, they spawn 50% hungry.
+#define NEW_FISH_LAST_FEEDING_MULT 0.5
+
+#define MIN_AQUARIUM_TEMP T0C
+#define MAX_AQUARIUM_TEMP (T0C + 100)
+#define DEFAULT_AQUARIUM_TEMP (T0C + 24)
+
+///How likely one's to find a given fish from random fish cases.
+#define FISH_RARITY_BASIC 1000
+#define FISH_RARITY_RARE 400
+#define FISH_RARITY_VERY_RARE 200
+#define FISH_RARITY_GOOD_LUCK_FINDING_THIS 5
+#define FISH_RARITY_NOPE 0
+
+///Aquarium fluid variables. The fish' required fluid has to match this, or it'll slowly die.
+#define AQUARIUM_FLUID_FRESHWATER "Freshwater"
+#define AQUARIUM_FLUID_SALTWATER "Saltwater"
+#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"
+
+///Fluff. The name of the aquarium company shown in the fish catalog
+#define AQUARIUM_COMPANY "Aquatech Ltd."
diff --git a/code/__DEFINES/fishing.dm b/code/__DEFINES/fishing.dm
deleted file mode 100644
index dc73623f27c..00000000000
--- a/code/__DEFINES/fishing.dm
+++ /dev/null
@@ -1,62 +0,0 @@
-/// Use in fish tables to denote miss chance.
-#define FISHING_DUD "dud"
-
-// Baseline fishing difficulty levels
-#define FISHING_DEFAULT_DIFFICULTY 15
-
-/// Difficulty modifier when bait is fish's favorite
-#define FAV_BAIT_DIFFICULTY_MOD -5
-/// Difficulty modifier when bait is fish's disliked
-#define DISLIKED_BAIT_DIFFICULTY_MOD 15
-/// Difficulty modifier when our fisherman has the trait TRAIT_SETTLER
-#define SETTLER_DIFFICULTY_MOD -5
-
-
-#define FISH_TRAIT_MINOR_DIFFICULTY_BOOST 5
-
-// These define how the fish will behave in the minigame
-#define FISH_AI_DUMB "dumb"
-#define FISH_AI_ZIPPY "zippy"
-#define FISH_AI_SLOW "slow"
-
-#define ADDITIVE_FISHING_MOD "additive"
-#define MULTIPLICATIVE_FISHING_MOD "multiplicative"
-
-// These defines are intended for use to interact with fishing hooks when going
-// through the fishing rod, and not the hook itself. They could probably be
-// handled differently, but for now that's how they work. It's grounds for
-// a future refactor, however.
-/// Fishing hook trait that signifies that it's shiny. Useful for fishes
-/// that care about shiner hooks more.
-#define FISHING_HOOK_SHINY (1 << 0)
-/// 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_MINIGAME_RULE_BIDIRECTIONAL (1 << 2)
-#define FISHING_MINIGAME_RULE_NO_ESCAPE (1 << 3)
-#define FISHING_MINIGAME_RULE_KILL (1 << 4)
-#define FISHING_MINIGAME_RULE_NO_EXP (1 << 5)
-
-/// The default additive value for fishing hook catch weight modifiers.
-#define FISHING_DEFAULT_HOOK_BONUS_ADDITIVE 0
-/// The default multiplicative value for fishing hook catch weight modifiers.
-#define FISHING_DEFAULT_HOOK_BONUS_MULTIPLICATIVE 1
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index cd110f2464d..73fbe6723e6 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -319,6 +319,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_BLOOD_DEFICIENCY "blood_deficiency"
#define TRAIT_JOLLY "jolly"
#define TRAIT_NOCRITDAMAGE "no_crit"
+///Added to mob or mind, changes the icons of the fish shown in the minigame UI depending on the possible reward.
+#define TRAIT_REVEAL_FISH "reveal_fish"
/// Stops the mob from slipping on water, or banana peels, or pretty much anything that doesn't have [GALOSHES_DONT_HELP] set
#define TRAIT_NO_SLIP_WATER "noslip_water"
@@ -909,6 +911,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_FISH_AMPHIBIOUS "fish_amphibious"
///Trait needed for the lubefish evolution
#define TRAIT_FISH_FED_LUBE "fish_fed_lube"
+#define TRAIT_FISH_NO_HUNGER "fish_no_hunger"
// common trait sources
#define TRAIT_GENERIC "generic"
@@ -1082,6 +1085,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// sticky nodrop sounds like a bad soundcloud rapper's name
#define STICKY_NODROP "sticky-nodrop"
#define SKILLCHIP_TRAIT "skillchip"
+#define SKILL_TRAIT "skill"
#define BUSY_FLOORBOT_TRAIT "busy-floorbot"
#define PULLED_WHILE_SOFTCRIT_TRAIT "pulled-while-softcrit"
#define LOCKED_BORG_TRAIT "locked-borg"
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 54fe44a609b..512b528f86f 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -269,6 +269,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_FISH_TOXIN_IMMUNE" = TRAIT_FISH_TOXIN_IMMUNE,
"TRAIT_FISH_CROSSBREEDER" = TRAIT_FISH_CROSSBREEDER,
"TRAIT_FISH_FED_LUBE" = TRAIT_FISH_FED_LUBE,
+ "TRAIT_FISH_NO_HUNGER" = TRAIT_FISH_NO_HUNGER,
),
))
diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm
index 3f230b8754d..f88c27a7135 100644
--- a/code/datums/components/fishing_spot.dm
+++ b/code/datums/components/fishing_spot.dm
@@ -12,6 +12,7 @@
fish_source = configuration
else
return COMPONENT_INCOMPATIBLE
+ fish_source.on_fishing_spot_init()
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(handle_attackby))
RegisterSignal(parent, COMSIG_FISHING_ROD_CAST, PROC_REF(handle_cast))
diff --git a/code/datums/mind/skills.dm b/code/datums/mind/skills.dm
index 5847236435b..474291d5ae0 100644
--- a/code/datums/mind/skills.dm
+++ b/code/datums/mind/skills.dm
@@ -18,12 +18,10 @@
experience_multiplier += experience_multiplier_reasons[key]
known_skills[skill][SKILL_EXP] = max(0, known_skills[skill][SKILL_EXP] + amt*experience_multiplier) //Update exp. Prevent going below 0
known_skills[skill][SKILL_LVL] = update_skill_level(skill)//Check what the current skill level is based on that skill's exp
- if(silent)
- return
if(known_skills[skill][SKILL_LVL] > old_level)
- S.level_gained(src, known_skills[skill][SKILL_LVL], old_level)
+ S.level_gained(src, known_skills[skill][SKILL_LVL], old_level, silent)
else if(known_skills[skill][SKILL_LVL] < old_level)
- S.level_lost(src, known_skills[skill][SKILL_LVL], old_level)
+ S.level_lost(src, known_skills[skill][SKILL_LVL], old_level, silent)
///Set experience of a specific skill to a number
/datum/mind/proc/set_experience(skill, amt, silent = FALSE)
diff --git a/code/datums/skills/_skill.dm b/code/datums/skills/_skill.dm
index ec52ebd9e96..b8438c67927 100644
--- a/code/datums/skills/_skill.dm
+++ b/code/datums/skills/_skill.dm
@@ -27,8 +27,7 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill))
span_nicegreen("I'm getting a little better at [name]!"),
span_nicegreen("I'm getting much better at [name]!"),
span_nicegreen("I feel like I've become quite proficient at [name]!"),
- "After lots of practice, I've begun to truly understand the intricacies \
- and surprising depth behind [name]. I now consider myself a master [title].",
+ span_nicegreen("After lots of practice, I've begun to truly understand the intricacies and surprising depth behind [name]. I now consider myself a master [title]."),
span_nicegreen("Through incredible determination and effort, I've reached the peak of my [name] abiltities. I'm finally able to consider myself a legendary [title]!") )
levelDownMessages = list(span_nicegreen("I have somehow completely lost all understanding of [name]. Please tell an admin if you see this."),
span_nicegreen("I'm starting to forget what [name] really even is. I need more practice..."),
@@ -46,13 +45,18 @@ GLOBAL_LIST_INIT(skill_types, subtypesof(/datum/skill))
* * mind - The mind that you'll want to send messages
* * new_level - The newly gained level. Can check the actual level to give different messages at different levels, see defines in skills.dm
* * old_level - Similar to the above, but the level you had before levelling up.
+ * * silent - Silences the announcement if TRUE
*/
-/datum/skill/proc/level_gained(datum/mind/mind, new_level, old_level)//just for announcements (doesn't go off if the xp gain is silent)
+/datum/skill/proc/level_gained(datum/mind/mind, new_level, old_level, silent)
+ if(silent)
+ return
to_chat(mind.current, levelUpMessages[new_level]) //new_level will be a value from 1 to 6, so we get appropriate message from the 6-element levelUpMessages list
/**
* level_lost: See level_gained, same idea but fires on skill level-down
*/
-/datum/skill/proc/level_lost(datum/mind/mind, new_level, old_level)
+/datum/skill/proc/level_lost(datum/mind/mind, new_level, old_level, silent)
+ if(silent)
+ return
to_chat(mind.current, levelDownMessages[old_level]) //old_level will be a value from 1 to 6, so we get appropriate message from the 6-element levelUpMessages list
/**
diff --git a/code/datums/skills/fishing.dm b/code/datums/skills/fishing.dm
index d5ecff3c8f5..ddf90e1a0a3 100644
--- a/code/datums/skills/fishing.dm
+++ b/code/datums/skills/fishing.dm
@@ -8,3 +8,17 @@
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
+
+/datum/skill/fishing/New()
+ . = ..()
+ levelUpMessages[SKILL_LEVEL_MASTER] = span_nicegreen("After lots of practice, I've begun to truly understand the surprising depth behind [name]. As a master [title], I can take an easier guess of what I'm trying to catch now.")
+
+/datum/skill/fishing/level_gained(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ if(new_level >= SKILL_LEVEL_MASTER && old_level < SKILL_LEVEL_MASTER)
+ ADD_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
+
+/datum/skill/fishing/level_lost(datum/mind/mind, new_level, old_level, silent)
+ . = ..()
+ if(old_level >= SKILL_LEVEL_MASTER && new_level < SKILL_LEVEL_MASTER)
+ REMOVE_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
diff --git a/code/game/machinery/computer/operating_computer.dm b/code/game/machinery/computer/operating_computer.dm
index f7b0ef24ffd..3b639a9ce57 100644
--- a/code/game/machinery/computer/operating_computer.dm
+++ b/code/game/machinery/computer/operating_computer.dm
@@ -25,11 +25,15 @@
if(!CONFIG_GET(flag/no_default_techweb_link) && !linked_techweb)
CONNECT_TO_RND_SERVER_ROUNDSTART(linked_techweb, src)
- experiment_handler = AddComponent( \
+ var/list/operating_signals = list(
+ COMSIG_OPERATING_COMPUTER_AUTOPSY_COMPLETE = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_autopsy_experiment),
+ )
+ experiment_handler = AddComponent(
/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/autopsy), \
config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE, \
config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ experiment_signals = operating_signals, \
)
/obj/machinery/computer/operating/Destroy()
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index faa48559100..4bea5b1335c 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1303,6 +1303,15 @@
req_components = list(
/datum/stock_part/card_reader = 1)
+/obj/item/circuitboard/machine/fishing_portal_generator
+ name = "Fishing Portal Generator"
+ greyscale_colors = CIRCUIT_COLOR_SERVICE
+ build_path = /obj/machinery/fishing_portal_generator
+ req_components = list(
+ /datum/stock_part/matter_bin = 2,
+ /datum/stock_part/capacitor = 1)
+ needs_anchored = FALSE
+
//Supply
/obj/item/circuitboard/machine/ore_redemption
name = "Ore Redemption"
diff --git a/code/modules/cargo/packs/general.dm b/code/modules/cargo/packs/general.dm
index fa7a3bfa374..5bfcf01eb00 100644
--- a/code/modules/cargo/packs/general.dm
+++ b/code/modules/cargo/packs/general.dm
@@ -292,13 +292,6 @@
crate_value = value
contents_uplink_type = uplink
-/datum/supply_pack/misc/fishing_portal
- name = "Fishing Portal Generator Crate"
- desc = "Not enough fish near your location? Fishing portal has your back."
- cost = CARGO_CRATE_VALUE * 4
- contains = list(/obj/machinery/fishing_portal_generator)
- crate_name = "fishing portal crate"
-
/datum/supply_pack/misc/papercutter
name = "Paper Cutters Crate"
desc = "Contains 3 office-grade paper cutters, equipped with sharp blades that can cut any paper into two thin slips.\
diff --git a/code/modules/experisci/destructive_scanner.dm b/code/modules/experisci/destructive_scanner.dm
index 3591a31cd7b..ef89dc9b94a 100644
--- a/code/modules/experisci/destructive_scanner.dm
+++ b/code/modules/experisci/destructive_scanner.dm
@@ -19,10 +19,17 @@
// Late load to ensure the component initialization occurs after the machines are initialized
/obj/machinery/destructive_scanner/LateInitialize()
. = ..()
+
+ var/static/list/destructive_signals = list(
+ COMSIG_MACHINERY_DESTRUCTIVE_SCAN = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_destructive_experiment),
+ )
+
AddComponent(/datum/component/experiment_handler, \
allowed_experiments = list(/datum/experiment/scanning),\
config_mode = EXPERIMENT_CONFIG_CLICK, \
- start_experiment_callback = CALLBACK(src, PROC_REF(activate)))
+ start_experiment_callback = CALLBACK(src, PROC_REF(activate)), \
+ experiment_signals = destructive_signals, \
+ )
///Activates the machine; checks if it can actually scan, then starts.
/obj/machinery/destructive_scanner/proc/activate()
diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm
index 8eee5bcc592..577ae59a900 100644
--- a/code/modules/experisci/experiment/experiments.dm
+++ b/code/modules/experisci/experiment/experiments.dm
@@ -323,7 +323,7 @@
///Damage percent that each mech needs to be at for a scan to work.
var/damage_percent
-/datum/experiment/scanning/random/mecha_damage_scan/New()
+/datum/experiment/scanning/random/mecha_damage_scan/New(datum/techweb/techweb)
. = ..()
damage_percent = rand(15, 95)
//updating the description with the damage_percent var set
diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm
index abc5d4ad1dd..29e7da95391 100644
--- a/code/modules/experisci/experiment/handlers/experiment_handler.dm
+++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm
@@ -38,6 +38,7 @@
disallowed_traits = null,
config_flags = null,
datum/callback/start_experiment_callback = null,
+ list/experiment_signals
)
. = ..()
if(!ismovable(parent))
@@ -49,13 +50,8 @@
src.config_flags = config_flags
src.start_experiment_callback = start_experiment_callback
- if(isitem(parent))
- RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(try_run_handheld_experiment))
- RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, PROC_REF(ignored_handheld_experiment_attempt))
- if(istype(parent, /obj/machinery/destructive_scanner))
- RegisterSignal(parent, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, PROC_REF(try_run_destructive_experiment))
- if(istype(parent, /obj/machinery/computer/operating))
- RegisterSignal(parent, COMSIG_OPERATING_COMPUTER_AUTOPSY_COMPLETE, PROC_REF(try_run_autopsy_experiment))
+ for(var/signal in experiment_signals)
+ RegisterSignal(parent, signal, experiment_signals[signal])
// Determine UI display mode
switch(config_mode)
@@ -85,9 +81,9 @@
*/
/datum/component/experiment_handler/proc/try_run_handheld_experiment(datum/source, atom/target, mob/user, params)
SIGNAL_HANDLER
- if (!should_run_handheld_experiment(source, target, user, params))
+ if (!should_run_handheld_experiment(source, target, user))
return
- INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user, params)
+ INVOKE_ASYNC(src, PROC_REF(try_run_handheld_experiment_async), source, target, user)
return COMPONENT_CANCEL_ATTACK_CHAIN
/**
@@ -98,7 +94,7 @@
if (!proximity_flag)
return
. |= COMPONENT_AFTERATTACK_PROCESSED_ITEM
- if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
+ if ((selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) || config_flags & EXPERIMENT_CONFIG_SILENT_FAIL)
return .
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -107,7 +103,7 @@
/**
* Checks that an experiment can be run using the provided target, used for preventing the cancellation of the attack chain inappropriately
*/
-/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user)
// Check that there is actually an experiment selected
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
return
@@ -127,16 +123,17 @@
/**
* This proc exists because Jared Fogle really likes async
*/
-/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user, params)
+/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user)
if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))
- to_chat(user, span_notice("You do not have an experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ to_chat(user, span_notice("You do not have an experiment selected!"))
return
- if(!do_after(user, 1 SECONDS, target = target))
+ if(!(config_flags & EXPERIMENT_CONFIG_IMMEDIATE_ACTION) && !do_after(user, 1 SECONDS, target = target))
return
if(action_experiment(source, target))
playsound(user, 'sound/machines/ping.ogg', 25)
to_chat(user, span_notice("You scan [target]."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, span_notice("[target] is not related to your currently selected experiment."))
@@ -148,8 +145,9 @@
SIGNAL_HANDLER
var/atom/movable/our_scanner = parent
if (selected_experiment == null)
- playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
- to_chat(our_scanner, span_notice("No experiment selected!"))
+ if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
+ playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25)
+ to_chat(our_scanner, span_notice("No experiment selected!"))
return
var/successful_scan
for(var/scan_target in scanned_atoms)
@@ -159,7 +157,7 @@
if(successful_scan)
playsound(our_scanner, 'sound/machines/ping.ogg', 25)
to_chat(our_scanner, span_notice("The scan succeeds."))
- else
+ else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL))
playsound(src, 'sound/machines/buzz-sigh.ogg', 25)
our_scanner.say("The scan did not result in anything.")
@@ -261,6 +259,7 @@
/datum/component/experiment_handler/proc/link_techweb(datum/techweb/new_web)
if (new_web == linked_web)
return
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = new_web
@@ -268,6 +267,7 @@
* Unlinks this handler from the selected techweb
*/
/datum/component/experiment_handler/proc/unlink_techweb()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
linked_web = null
@@ -278,13 +278,15 @@
* * experiment - The experiment to attempt to link to
*/
/datum/component/experiment_handler/proc/link_experiment(datum/experiment/experiment)
- if (experiment && can_select_experiment(experiment))
+ if (can_select_experiment(experiment))
selected_experiment = experiment
+ selected_experiment.on_selected(src)
/**
* Unlinks this handler from the selected experiment
*/
/datum/component/experiment_handler/proc/unlink_experiment()
+ selected_experiment?.on_unselected(src)
selected_experiment = null
/**
@@ -299,31 +301,19 @@
return FALSE
// Check against the list of allowed experimentors
- if (experiment.allowed_experimentors && experiment.allowed_experimentors.len)
- var/matched = FALSE
- for (var/experimentor in experiment.allowed_experimentors)
- if (istype(parent, experimentor))
- matched = TRUE
- break
- if (!matched)
- return FALSE
+ if (length(experiment.allowed_experimentors) && !is_type_in_list(parent, experiment.allowed_experimentors))
+ return FALSE
// Check that this experiment is visible currently
- if (!linked_web || !(experiment in linked_web.available_experiments))
+ if (!(experiment in linked_web?.available_experiments))
return FALSE
// Check that this experiment type isn't blacklisted
- for (var/badsci in blacklisted_experiments)
- if (istype(experiment, badsci))
- return FALSE
+ if(is_type_in_list(experiment, blacklisted_experiments))
+ return FALSE
- // Check against the allowed experiment types
- for (var/goodsci in allowed_experiments)
- if (istype(experiment, goodsci))
- return TRUE
-
- // If we haven't returned yet then this shouldn't be allowed
- return FALSE
+ // Finally, check against the allowed experiment types
+ return is_type_in_list(experiment, allowed_experiments)
/datum/component/experiment_handler/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -355,12 +345,13 @@
.["techwebs"] += list(data)
.["experiments"] = list()
if (linked_web)
- for (var/datum/experiment/experiment in linked_web.available_experiments)
+ for (var/datum/experiment/experiment as anything in linked_web.available_experiments)
+ if(!can_select_experiment(experiment))
+ continue
var/list/data = list(
name = experiment.name,
description = experiment.description,
tag = experiment.exp_tag,
- selectable = can_select_experiment(experiment),
selected = selected_experiment == experiment,
progress = experiment.check_progress(),
performance_hint = experiment.performance_hint,
diff --git a/code/modules/experisci/experiment/types/experiment.dm b/code/modules/experisci/experiment/types/experiment.dm
index f760723f8db..add015622f6 100644
--- a/code/modules/experisci/experiment/types/experiment.dm
+++ b/code/modules/experisci/experiment/types/experiment.dm
@@ -22,11 +22,16 @@
/// A textual hint shown on the UI in a tooltip to help a user determine how to perform
/// the experiment
var/performance_hint
+ /**
+ * If set, these techweb points will be rewarded for completing the experiment.
+ * Useful for those loose ends not tied to any specific node discount or requirement.
+ */
+ var/list/points_reward
/**
* Performs any necessary initialization of tags and other variables
*/
-/datum/experiment/New()
+/datum/experiment/New(datum/techweb/techweb)
if (traits & EXPERIMENT_TRAIT_DESTRUCTIVE)
exp_tag = "Destructive [exp_tag]"
@@ -60,6 +65,14 @@
/datum/experiment/proc/actionable(...)
return !is_complete()
+///Called when the experiment is selected by an experiment handler, for specific signals and the such.
+/datum/experiment/proc/on_selected(datum/component/experiment_handler/experiment_handler)
+ return
+
+///Called when the opposite happens.
+/datum/experiment/proc/on_unselected(datum/component/experiment_handler/experiment_handler)
+ return
+
/**
* Proc that tries to perform the experiment, and then checks if its completed.
*/
diff --git a/code/modules/experisci/experiment/types/exploration.dm b/code/modules/experisci/experiment/types/exploration.dm
index a6a5d2cd4cf..821e69a103a 100644
--- a/code/modules/experisci/experiment/types/exploration.dm
+++ b/code/modules/experisci/experiment/types/exploration.dm
@@ -52,7 +52,7 @@
/// If not null the required_condition will be picked from this list
var/list/possible_random_site_types
-/datum/experiment/exploration_scan/random/New()
+/datum/experiment/exploration_scan/random/New(datum/techweb/techweb)
. = ..()
if(length(possible_random_site_types))
required_site_type = pick(possible_random_site_types)
diff --git a/code/modules/experisci/experiment/types/random_scanning.dm b/code/modules/experisci/experiment/types/random_scanning.dm
index e80a80b5a0e..c9d39bd47b4 100644
--- a/code/modules/experisci/experiment/types/random_scanning.dm
+++ b/code/modules/experisci/experiment/types/random_scanning.dm
@@ -8,7 +8,7 @@
/// Max amount of a requirement per type
var/max_requirement_per_type = 100
-/datum/experiment/scanning/random/New()
+/datum/experiment/scanning/random/New(datum/techweb/techweb)
// Generate random contents
if (possible_types.len)
var/picked = 0
diff --git a/code/modules/experisci/experiment/types/scanning.dm b/code/modules/experisci/experiment/types/scanning.dm
index d9bbed88c8f..54bd2ad637e 100644
--- a/code/modules/experisci/experiment/types/scanning.dm
+++ b/code/modules/experisci/experiment/types/scanning.dm
@@ -17,16 +17,18 @@
var/list/required_atoms = list()
/// The list of atoms with sub-lists of atom references for scanned atoms contributing to the experiment (Or a count of atoms destoryed for destructive expiriments)
var/list/scanned = list()
+ /// If set, it'll be used in place of the generic "Scan samples of \a [initial(target.name)]" in serialize_progress_stage()
+ var/scan_message
/**
* Initializes the scanned atoms lists
*
* Initializes the internal scanned atoms list to keep track of which atoms have already been scanned
*/
-/datum/experiment/scanning/New()
+/datum/experiment/scanning/New(datum/techweb/techweb)
. = ..()
for (var/req_atom in required_atoms)
- scanned[req_atom] = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 0 : list()
+ scanned[req_atom] = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? 0 : list()
/**
* Checks if the scanning experiment is complete
@@ -37,8 +39,12 @@
/datum/experiment/scanning/is_complete()
. = TRUE
var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE
+ var/typecache = traits & EXPERIMENT_TRAIT_TYPECACHE
for (var/req_atom in required_atoms)
var/list/seen = scanned[req_atom]
+ ///typecache experiments work all the same whether it's destructive or not
+ if(typecache && length(seen) == required_atoms[req_atom])
+ continue
if (destructive && (!(req_atom in scanned) || scanned[req_atom] != required_atoms[req_atom]))
return FALSE
if (!destructive && (!seen || seen.len != required_atoms[req_atom]))
@@ -65,8 +71,9 @@
* * seen_instances - The number of instances seen of this atom
*/
/datum/experiment/scanning/proc/serialize_progress_stage(atom/target, list/seen_instances)
- var/scanned_total = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len
- return EXPERIMENT_PROG_INT("Scan samples of \a [initial(target.name)]", scanned_total, required_atoms[target])
+ var/scanned_total = (traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !(traits & EXPERIMENT_TRAIT_TYPECACHE)) ? scanned[target] : seen_instances.len
+ var/message = scan_message || "Scan samples of \a [initial(target.name)]"
+ return EXPERIMENT_PROG_INT(message, scanned_total, required_atoms[target])
/**
* Attempts to scan an atom towards the experiment's goal
@@ -79,7 +86,10 @@
/datum/experiment/scanning/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, atom/target)
var/contributing_index_value = get_contributing_index(target)
if (contributing_index_value)
- scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
+ if(traits & EXPERIMENT_TRAIT_TYPECACHE)
+ scanned[contributing_index_value][target.type] = TRUE
+ else
+ scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : WEAKREF(target)
if(traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !isliving(target))//only qdel things when destructive scanning and they're not living (living things get gibbed)
qdel(target)
do_after_experiment(target, contributing_index_value)
diff --git a/code/modules/experisci/experiment/types/scanning_fish.dm b/code/modules/experisci/experiment/types/scanning_fish.dm
new file mode 100644
index 00000000000..83978010869
--- /dev/null
+++ b/code/modules/experisci/experiment/types/scanning_fish.dm
@@ -0,0 +1,116 @@
+///a superlist containing typecaches shared between the several fish scanning experiments for each techweb.
+GLOBAL_LIST_EMPTY(scanned_fish_by_techweb)
+
+/**
+ * A special scanning experiment that unlocks further settings for the fishing portal generator.
+ * Mainly as an inventive solution to many a fish source being limited to maps that have it,
+ * and to make the fishing portal generator a bit than just gubby and goldfish.
+ */
+/datum/experiment/scanning/fish
+ name = "Fish Scanning Experiment 1"
+ description = "An experiment requiring different fish species to be scanned to unlock the 'Beach' setting for the fishing portal generator."
+ performance_hint = "Scan fish. Examine scanner to review progress. Unlock new fishing portals."
+ allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/destructive_scanner, /obj/item/fishing_rod/tech)
+ traits = EXPERIMENT_TRAIT_TYPECACHE
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 750)
+ required_atoms = list(/obj/item/fish = 4)
+ scan_message = "Scan different species of fish"
+ ///Further experiments added to the techweb when this one is completed.
+ var/list/next_experiments = list(/datum/experiment/scanning/fish/second)
+ ///Completing a experiment may also enable a fish source to be used for use for the portal generator.
+ var/fish_source_reward = /datum/fish_source/portal/beach
+
+/**
+ * We make sure the scanned list is shared between all fish scanning experiments for this techweb,
+ * since this is about scanning each species, and having to redo it for each species is a hassle.
+ */
+/datum/experiment/scanning/fish/New(datum/techweb/techweb)
+ . = ..()
+ if(isnull(techweb))
+ return
+ var/techweb_ref = REF(techweb)
+ var/list/scanned_fish = GLOB.scanned_fish_by_techweb[techweb_ref]
+ if(isnull(scanned_fish))
+ scanned_fish = list()
+ GLOB.scanned_fish_by_techweb[techweb_ref] = scanned_fish
+ for(var/atom_type in required_atoms)
+ LAZYINITLIST(scanned_fish[atom_type])
+ scanned = scanned_fish
+
+/**
+ * Registers a couple signals to review the fish scanned so far.
+ * It'd be an hassle not having any way (beside memory) to know which fish species have been scanned already otherwise.
+ */
+/datum/experiment/scanning/fish/on_selected(datum/component/experiment_handler/experiment_handler)
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_handler_examine))
+ RegisterSignal(experiment_handler.parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_handler_examine_more))
+
+/datum/experiment/scanning/fish/on_unselected(datum/component/experiment_handler/experiment_handler)
+ UnregisterSignal(experiment_handler.parent, list(COMSIG_ATOM_EXAMINE, COMSIG_ATOM_EXAMINE_MORE))
+
+/datum/experiment/scanning/fish/proc/on_handler_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ examine_list += span_notice("Examine again to review all the species of fish scanned so far.")
+
+/datum/experiment/scanning/fish/proc/on_handler_examine_more(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ var/message = span_notice("Fish species scanned hitherto, if any:")
+ message += ""
+ for(var/atom_type in required_atoms)
+ for(var/obj/item/fish/fish_path as anything in scanned[atom_type])
+ message += "[initial(fish_path.name)]"
+ message += ""
+ examine_list += message
+
+///Only scannable fish will contribute towards the experiment.
+/datum/experiment/scanning/fish/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return target.experisci_scannable
+
+/**
+ * After a fish scanning experiment is done, more may be unlocked. If so, add them to the techweb
+ * and automatically link the handler to the next experiment in the list as a bit of qol.
+ */
+/datum/experiment/scanning/fish/finish_experiment(datum/component/experiment_handler/experiment_handler, ...)
+ . = ..()
+ if(next_experiments)
+ experiment_handler.linked_web.add_experiments(next_experiments)
+ var/datum/experiment/next_in_line = locate(next_experiments[1]) in experiment_handler.linked_web.available_experiments
+ experiment_handler.link_experiment(next_in_line)
+
+/datum/experiment/scanning/fish/second
+ name = "Fish Scanning Experiment 2"
+ description = "An experiment requiring more fish species to be scanned to unlock the 'Chasm' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 1500)
+ required_atoms = list(/obj/item/fish = 8)
+ next_experiments = list(/datum/experiment/scanning/fish/third)
+ fish_source_reward = /datum/fish_source/portal/chasm
+
+/datum/experiment/scanning/fish/third
+ name = "Fish Scanning Experiment 3"
+ description = "An experiment requiring even more fish species to be scanned to unlock the 'Ocean' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ required_atoms = list(/obj/item/fish = 14)
+ next_experiments = list(/datum/experiment/scanning/fish/fourth, /datum/experiment/scanning/fish/holographic)
+ fish_source_reward = /datum/fish_source/portal/ocean
+
+/datum/experiment/scanning/fish/holographic
+ name = "Holographic Fish Scanning Experiment"
+ description = "This one actually requires holographic fish to unlock the 'Randomizer' setting for the fishing portal."
+ performance_hint = "Load in the 'Beach' template at the Holodeck to fish some holo-fish."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 500)
+ required_atoms = list(/obj/item/fish/holo = 4)
+ scan_message = "Scan different species of holographic fish"
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/random
+
+///holo fishes are normally unscannable, but this is an experiment for them, so we don't care for the experisci_scannable variable.
+/datum/experiment/scanning/fish/holographic/final_contributing_index_checks(obj/item/fish/target, typepath)
+ return TRUE
+
+/datum/experiment/scanning/fish/fourth
+ name = "Fish Scanning Experiment 4"
+ description = "An experiment requiring lotsa fish species to unlock the 'Hyperspace' setting for the fishing portal."
+ points_reward = list(TECHWEB_POINT_TYPE_GENERIC = 3250)
+ required_atoms = list(/obj/item/fish = 21)
+ next_experiments = null
+ fish_source_reward = /datum/fish_source/portal/hyperspace
diff --git a/code/modules/experisci/experiment/types/scanning_material.dm b/code/modules/experisci/experiment/types/scanning_material.dm
index 714205289de..fb8a7ff354b 100644
--- a/code/modules/experisci/experiment/types/scanning_material.dm
+++ b/code/modules/experisci/experiment/types/scanning_material.dm
@@ -9,7 +9,7 @@
///List of materials actually required, indexed by the atom that is required.
var/required_materials = list()
-/datum/experiment/scanning/random/material/New()
+/datum/experiment/scanning/random/material/New(datum/techweb/techweb)
. = ..()
for(var/req_atom in required_atoms)
var/chosen_material = pick(possible_material_types)
diff --git a/code/modules/experisci/experiment/types/scanning_plants.dm b/code/modules/experisci/experiment/types/scanning_plants.dm
index c34822d6e7e..b92a4cc20b4 100644
--- a/code/modules/experisci/experiment/types/scanning_plants.dm
+++ b/code/modules/experisci/experiment/types/scanning_plants.dm
@@ -10,7 +10,7 @@
///List of plant genes actually required, indexed by the atom that is required.
var/list/required_genes = list()
-/datum/experiment/scanning/random/plants/New()
+/datum/experiment/scanning/random/plants/New(datum/techweb/techweb)
. = ..()
if(possible_plant_genes.len)
for(var/req_atom in required_atoms)
diff --git a/code/modules/experisci/handheld_scanner.dm b/code/modules/experisci/handheld_scanner.dm
index e0fd4d480d5..97aa034afa1 100644
--- a/code/modules/experisci/handheld_scanner.dm
+++ b/code/modules/experisci/handheld_scanner.dm
@@ -19,9 +19,15 @@
// Late initialize to allow for the rnd servers to initialize first
/obj/item/experi_scanner/LateInitialize()
. = ..()
+ var/static/list/handheld_signals = list(
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
AddComponent(/datum/component/experiment_handler, \
- allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical),\
- disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE)
+ allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical), \
+ disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE, \
+ experiment_signals = handheld_signals, \
+ )
/obj/item/experi_scanner/suicide_act(mob/living/carbon/user)
user.visible_message(span_suicide("[user] is giving in to the Great Toilet Beyond! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/modules/fishing/aquarium/fish_analyzer.dm b/code/modules/fishing/aquarium/fish_analyzer.dm
index c7feab860ca..8afe053a4d5 100644
--- a/code/modules/fishing/aquarium/fish_analyzer.dm
+++ b/code/modules/fishing/aquarium/fish_analyzer.dm
@@ -190,16 +190,17 @@
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"
+ if(!HAS_TRAIT(fish, TRAIT_FISH_NO_HUNGER))
+ 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"
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 21585a07546..1275886141b 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -136,6 +136,9 @@
var/min_pressure = WARNING_LOW_PRESSURE
var/max_pressure = HAZARD_HIGH_PRESSURE
+ /// If this fish type counts towards the Fish Species Scanning experiments
+ var/experisci_scannable = TRUE
+
/obj/item/fish/Initialize(mapload, apply_qualities = TRUE)
. = ..()
AddComponent(/datum/component/aquarium_content, PROC_REF(get_aquarium_animation), list(COMSIG_FISH_STATUS_CHANGED,COMSIG_FISH_STIRRED))
@@ -175,7 +178,7 @@
balloon_alert(user, "[src] is dead!")
return TRUE
feed(item.reagents)
- balloon_alert(user, "you feed [src]")
+ balloon_alert(user, "fed [src]")
return TRUE
/obj/item/fish/examine(mob/user)
@@ -185,14 +188,14 @@
. += span_notice("It weighs [weight] g.")
///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)
+/obj/item/fish/proc/randomize_size_and_weight(base_size = average_size, base_weight = average_weight, deviation = 0.2)
+ var/size_deviation = 0.2 * base_size
+ var/new_size = round(clamp(gaussian(base_size, size_deviation), average_size * 1/MAX_FISH_DEVIATION_COEFF, average_size * MAX_FISH_DEVIATION_COEFF))
- var/weight_deviation = 0.2 * avg_weight
- var/new_weight = round(max(1,gaussian(avg_weight, weight_deviation)), 1)
+ var/weight_deviation = 0.2 * base_weight
+ var/new_weight = round(clamp(gaussian(base_weight, weight_deviation), average_weight * 1/MAX_FISH_DEVIATION_COEFF, average_weight * MAX_FISH_DEVIATION_COEFF))
- update_size_and_weight(new_size, new_weight, first_run)
+ update_size_and_weight(new_size, new_weight)
///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)
@@ -414,11 +417,14 @@
return FALSE
return TRUE
+/obj/item/fish/proc/is_hungry()
+ return !HAS_TRAIT(src, TRAIT_FISH_NO_HUNGER) && world.time - last_feeding >= feeding_frequency
+
/obj/item/fish/proc/process_health(seconds_per_tick)
var/health_change_per_second = 0
if(!proper_environment())
health_change_per_second -= 3 //Dying here
- if(world.time - last_feeding >= feeding_frequency)
+ if(is_hungry())
health_change_per_second -= 0.5 //Starving
else
health_change_per_second += 0.5 //Slowly healing
@@ -541,10 +547,11 @@
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
-/// This animation should be applied to actual parent atom instead of vc_object.
-/proc/flop_animation(atom/movable/animation_target)
+
+/// This flopping animation played while the fish is alive.
+/obj/item/fish/proc/flop_animation()
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
- animate(animation_target, time = pause_between, loop = -1)
+ animate(src, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
var/matrix/up_matrix = matrix()
@@ -565,6 +572,7 @@
animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
+
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
@@ -578,7 +586,7 @@
if(flopping) //Requires update_transform/animate_wrappers to be less restrictive.
return
flopping = TRUE
- flop_animation(src)
+ flop_animation()
/// Stops flopping animation
/obj/item/fish/proc/stop_flopping()
@@ -593,7 +601,7 @@
/obj/item/fish/proc/refresh_flopping()
if(flopping)
- flop_animation(src)
+ flop_animation()
/// Returns random fish, using random_case_rarity probabilities.
/proc/random_fish_type(required_fluid)
diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm
index bec868ad24e..63de4cfdf1d 100644
--- a/code/modules/fishing/fish/fish_traits.dm
+++ b/code/modules/fishing/fish/fish_traits.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
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
+ /// Depending on the value, fish with trait will be reported as more or less difficult in the catalog.
+ var/added_difficulty = 0
/// 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)
@@ -161,7 +163,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/datum/fish_trait/necrophage
name = "Necrophage"
- catalog_description = "This fish will eat the carcasses of dead fishes when hungry."
+ catalog_description = "This fish will eat carcasses of dead fish when hungry."
incompatible_traits = list(/datum/fish_trait/vegan)
/datum/fish_trait/necrophage/apply_to_fish(obj/item/fish/fish)
@@ -169,7 +171,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/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))
+ if(!source.is_hungry() || !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))
@@ -233,7 +235,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
/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))
+ if(!source.is_hungry() || !isaquarium(source.loc))
return
var/obj/structure/aquarium/aquarium = source.loc
for(var/obj/item/fish/victim in aquarium.get_fishes(TRUE, source))
@@ -334,6 +336,7 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
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."
+ added_difficulty = 5
/datum/fish_trait/lubed/apply_to_fish(obj/item/fish/fish)
fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP)
@@ -352,3 +355,26 @@ GLOBAL_LIST_INIT(fish_traits, init_subtypes_w_path_keys(/datum/fish_trait, list(
ADD_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS, FISH_TRAIT_DATUM)
if(fish.required_fluid_type == AQUARIUM_FLUID_AIR)
fish.required_fluid_type = AQUARIUM_FLUID_FRESHWATER
+
+/datum/fish_trait/mixotroph
+ name = "Mixotroph"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish is capable of substaining itself by producing its own sources of energy (food)."
+ incompatible_traits = list(/datum/fish_trait/predator, /datum/fish_trait/necrophage)
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ ADD_TRAIT(fish, TRAIT_FISH_NO_HUNGER, FISH_TRAIT_DATUM)
+
+/datum/fish_trait/antigrav
+ name = "Anti-Gravity"
+ inheritability = 75
+ diff_traits_inheritability = 25
+ catalog_description = "This fish will invert the gravity of the bait at random. May fall upward outside after being caught."
+ added_difficulty = 15
+
+/datum/fish_trait/antigrav/minigame_mod(obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/minigame)
+ minigame.special_effects |= FISHING_MINIGAME_RULE_ANTIGRAV
+
+/datum/fish_trait/antigrav/apply_to_fish(obj/item/fish/fish)
+ fish.AddElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY)
diff --git a/code/modules/fishing/fish/fish_types.dm b/code/modules/fishing/fish/fish_types.dm
index 652b0aba8aa..5ff62266ad9 100644
--- a/code/modules/fishing/fish/fish_types.dm
+++ b/code/modules/fishing/fish/fish_types.dm
@@ -341,7 +341,6 @@
icon_state = "sludgefish_purple"
dedicated_in_aquarium_icon_state = "sludgefish_purple_small"
random_case_rarity = FISH_RARITY_NOPE
- random_case_rarity = FISH_RARITY_VERY_RARE
fish_traits = list(/datum/fish_trait/parthenogenesis)
/obj/item/fish/slimefish
@@ -439,11 +438,13 @@
fillet_type = null
death_text = "%SRC gently disappears."
fish_traits = list(/datum/fish_trait/no_mating) //just to be sure, these shouldn't reproduce
+ experisci_scannable = FALSE
/obj/item/fish/holo/Initialize(mapload)
. = ..()
var/area/station/holodeck/holo_area = get_area(src)
if(!istype(holo_area))
+ addtimer(CALLBACK(src, PROC_REF(set_status), FISH_DEAD), 1 MINUTES)
return
holo_area.linked.add_to_spawned(src)
@@ -488,9 +489,9 @@
sprite_height = 5
/obj/item/fish/holo/checkered
- name = "unrendered holographic fish" //it's a meta joke, buddy.
+ name = "unrendered holographic fish"
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"
+ icon_state = "checkered" //it's a meta joke, buddy.
dedicated_in_aquarium_icon_state = "checkered_small"
sprite_width = 4
@@ -502,3 +503,37 @@
sprite_height = 4
sprite_width = 10
average_size = 50
+
+/obj/item/fish/starfish
+ name = "cosmostarfish"
+ desc = "A peculiar, gravity-defying, echinoderm-looking critter from hyperspace."
+ icon_state = "starfish"
+ dedicated_in_aquarium_icon_state = "starfish_small"
+ icon_state_dead = "starfish_dead"
+ sprite_width = 4
+ average_size = 30
+ average_weight = 300
+ stable_population = 3
+ required_fluid_type = AQUARIUM_FLUID_AIR
+ random_case_rarity = FISH_RARITY_NOPE
+ required_temperature_min = 0
+ required_temperature_max = INFINITY
+ safe_air_limits = null
+ min_pressure = 0
+ max_pressure = INFINITY
+ grind_results = list(/datum/reagent/bluespace = 10, /datum/reagent/consumable/liquidgibs = 5)
+ fillet_type = null
+ fish_traits = list(/datum/fish_trait/antigrav, /datum/fish_trait/mixotroph)
+
+/obj/item/fish/starfish/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/item/fish/starfish/update_overlays()
+ . = ..()
+ if(status == FISH_ALIVE)
+ . += emissive_appearance(icon, "starfish_emissive", src)
+
+///It spins, and dimly glows in the dark.
+/obj/item/fish/starfish/flop_animation()
+ DO_FLOATING_ANIM(src)
diff --git a/code/modules/fishing/fish_catalog.dm b/code/modules/fishing/fish_catalog.dm
index d41b4657e50..a0f66c2227d 100644
--- a/code/modules/fishing/fish_catalog.dm
+++ b/code/modules/fishing/fish_catalog.dm
@@ -68,14 +68,6 @@
if(source.catalog_description && (fish_type in source.fish_table))
spot_descriptions += source.catalog_description
.["spots"] = english_list(spot_descriptions, nothing_text = "Unknown")
- ///Difficulty descriptor
- switch(initial(fishy.fishing_difficulty_modifier))
- if(-INFINITY to 10)
- .["difficulty"] = "Easy"
- if(20 to 30)
- .["difficulty"] = "Medium"
- else
- .["difficulty"] = "Hard"
var/list/fish_list_properties = collect_fish_properties()
var/list/fav_bait = fish_list_properties[fishy][NAMEOF(fishy, favorite_bait)]
var/list/disliked_bait = fish_list_properties[fishy][NAMEOF(fishy, disliked_bait)]
@@ -91,12 +83,22 @@
// Fish traits description
var/list/trait_descriptions = list()
var/list/fish_traits = fish_list_properties[fishy][NAMEOF(fishy, fish_traits)]
+ var/fish_difficulty = initial(fishy.fishing_difficulty_modifier)
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
trait_descriptions += trait.catalog_description
+ fish_difficulty += trait.added_difficulty
if(!length(trait_descriptions))
trait_descriptions += "This fish exhibits no special behavior."
.["traits"] = trait_descriptions
+ ///Difficulty descriptor
+ switch(fish_difficulty)
+ if(-INFINITY to 9)
+ .["difficulty"] = "Easy"
+ if(10 to 19)
+ .["difficulty"] = "Medium"
+ else
+ .["difficulty"] = "Hard"
return .
/obj/item/book/fish_catalog/ui_assets(mob/user)
diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm
index 6169b41fd88..3f34222ad87 100644
--- a/code/modules/fishing/fishing_equipment.dm
+++ b/code/modules/fishing/fishing_equipment.dm
@@ -11,7 +11,8 @@
desc = "Simple fishing line."
icon = 'icons/obj/fishing.dmi'
icon_state = "reel_blue"
- var/fishing_line_traits = NONE
+ ///A list of traits that this fishing line has, checked by fish traits and the minigame.
+ var/list/fishing_line_traits
/// Color of the fishing line
var/line_color = "#808080"
@@ -59,7 +60,8 @@
icon_state = "hook"
w_class = WEIGHT_CLASS_TINY
- var/fishing_hook_traits = NONE
+ /// A list of traits that this fishing hook has, checked by fish traits and the minigame
+ var/list/fishing_hook_traits
/// icon state added to main rod icon when this hook is equipped
var/rod_overlay_icon_state = "hook_overlay"
/// What subtype of `/obj/item/chasm_detritus` do we fish out of chasms? Defaults to `/obj/item/chasm_detritus`.
@@ -175,7 +177,7 @@
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 gets caught in its jaws."
icon_state = "jaws"
- fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_ENSNARE|FISHING_HOOK_KILL
+ fishing_hook_traits = FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_NO_ESCAPE|FISHING_HOOK_KILL
rod_overlay_icon_state = "hook_jaws_overlay"
/obj/item/storage/toolbox/fishing
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index c0a8c53e0b0..06ac03d64ac 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -43,8 +43,12 @@
var/fish_ai = FISH_AI_DUMB
/// Rule modifiers (eg weighted bait)
var/special_effects = NONE
- /// Did the game get past the baiting phase, used to track if bait should be consumed afterwards
- var/bait_taken = FALSE
+ /// A list of possible active minigame effects. If not empty, one will be picked from time to time.
+ var/list/active_effects
+ /// The cooldown between switching active effects
+ COOLDOWN_DECLARE(active_effect_cd)
+ /// The current active effect
+ var/current_active_effect
/// Result path
var/reward_path = FISHING_DUD
/// Minigame difficulty
@@ -61,6 +65,8 @@
var/obj/effect/fishing_lure/lure
/// Background icon state from fishing_hud.dmi
var/background = "background_default"
+ /// Fish icon state from fishing_hud.dmi
+ var/fish_icon = "fish"
/// Fishing line visual
var/datum/beam/fishing_line
@@ -168,7 +174,10 @@
RegisterSignal(user, COMSIG_MOB_FISHING_REWARD_DISPENSED, PROC_REF(hurt_fish))
difficulty += comp.fish_source.calculate_difficulty(reward_path, rod, user, src)
- difficulty = round(difficulty)
+ difficulty = clamp(round(difficulty), 1, 100)
+
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ fish_icon = GLOB.specific_fish_icons[reward_path] || "fish"
/**
* If the chances are higher than 1% (100% at maximum difficulty), they'll scale
@@ -219,6 +228,7 @@
/datum/fishing_challenge/proc/start(mob/living/user)
/// Create fishing line visuals
fishing_line = used_rod.create_fishing_line(lure, target_py = 5)
+ active_effects = bitfield_to_list(special_effects & FISHING_MINIGAME_ACTIVE_EFFECTS)
// If fishing line breaks los / rod gets dropped / deleted
RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(on_line_deleted))
RegisterSignal(used_rod, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self))
@@ -299,7 +309,30 @@
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
- send_alert("!!!")
+ if(HAS_TRAIT(user, TRAIT_REVEAL_FISH))
+ switch(fish_icon)
+ if(FISH_ICON_DEF)
+ send_alert("fish!!!")
+ if(FISH_ICON_HOSTILE)
+ send_alert("hostile!!!")
+ if(FISH_ICON_STAR)
+ send_alert("starfish!!!")
+ if(FISH_ICON_CHUNKY)
+ send_alert("round fish!!!")
+ if(FISH_ICON_JELLYFISH)
+ send_alert("jellyfish!!!")
+ if(FISH_ICON_SLIME)
+ send_alert("slime!!!")
+ if(FISH_ICON_COIN)
+ send_alert("valuable!!!")
+ if(FISH_ICON_GEM)
+ send_alert("ore!!!")
+ if(FISH_ICON_CRAB)
+ send_alert("crustacean!!!")
+ if(FISH_ICON_BONE)
+ send_alert("bones!!!")
+ else
+ 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
@@ -350,6 +383,9 @@
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(start_reeling))
RegisterSignal(user.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(stop_reeling))
RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(on_user_logout))
+ if(length(active_effects))
+ // Give the player a moment to prepare for active minigame effects
+ COOLDOWN_START(src, active_effect_cd, rand(5, 9) SECONDS)
START_PROCESSING(SSfishing, src)
///Stop processing and remove references to the minigame hud
@@ -374,11 +410,44 @@
///Update the state of the fish, the bait and the hud
/datum/fishing_challenge/process(seconds_per_tick)
+ if(length(active_effects) && COOLDOWN_FINISHED(src, active_effect_cd))
+ select_active_effect()
move_fish(seconds_per_tick)
move_bait(seconds_per_tick)
if(!QDELETED(fishing_hud))
update_visuals()
+///The proc that handles fancy effects like flipping the hud or skewing movement
+/datum/fishing_challenge/proc/select_active_effect()
+ ///bring forth an active effect
+ if(isnull(current_active_effect))
+ current_active_effect = pick(active_effects)
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ fishing_hud.icon_state = "background_antigrav"
+ SEND_SOUND(user, sound('sound/effects/arcade_jump.ogg', volume = 50))
+ COOLDOWN_START(src, active_effect_cd, rand(6, 9) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.icon_state = "background_flip"
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ SEND_SOUND(user, sound('sound/effects/boing.ogg'))
+ COOLDOWN_START(src, active_effect_cd, rand(5, 6) SECONDS)
+ return
+
+ ///go back to normal
+ switch(current_active_effect)
+ if(FISHING_MINIGAME_RULE_ANTIGRAV)
+ var/sound/inverted_sound = sound('sound/effects/arcade_jump.ogg', volume = 50)
+ inverted_sound.frequency = -1
+ SEND_SOUND(user, inverted_sound)
+ COOLDOWN_START(src, active_effect_cd, rand(10, 13) SECONDS)
+ if(FISHING_MINIGAME_RULE_FLIP)
+ fishing_hud.transform = fishing_hud.transform.Scale(1, -1)
+ COOLDOWN_START(src, active_effect_cd, rand(8, 12) SECONDS)
+
+ fishing_hud.icon_state = background
+ current_active_effect = null
+
///The proc that moves the fish around, just like in the old TGUI, mostly.
/datum/fishing_challenge/proc/move_fish(seconds_per_tick)
var/long_chance = long_jump_chance * seconds_per_tick * 10
@@ -469,6 +538,9 @@
velocity_change = round(velocity_change)
+ if(current_active_effect == FISHING_MINIGAME_RULE_ANTIGRAV)
+ velocity_change = -velocity_change
+
/**
* Pull the brake on the velocity if the current velocity and the acceleration
* have different directions, making the bait less slippery, thus easier to control
@@ -529,7 +601,7 @@
icon_state = challenge.background
add_overlay("frame")
hud_bait = new(null, null, challenge)
- hud_fish = new
+ hud_fish = new(null, null, challenge)
hud_completion = new(null, null, challenge)
vis_contents += list(hud_bait, hud_fish, hud_completion)
challenge.user.client.screen += src
@@ -562,6 +634,11 @@
icon_state = "fish"
vis_flags = VIS_INHERIT_ID
+/atom/movable/screen/hud_fish/Initialize(mapload, datum/hud/hud_owner, datum/fishing_challenge/challenge)
+ . = ..()
+ if(challenge)
+ icon_state = challenge.fish_icon
+
/atom/movable/screen/hud_completion
icon = 'icons/hud/fishing_hud.dmi'
icon_state = "completion_0"
diff --git a/code/modules/fishing/fishing_portal_machine.dm b/code/modules/fishing/fishing_portal_machine.dm
index 3fc6b0eb938..b156a37ba05 100644
--- a/code/modules/fishing/fishing_portal_machine.dm
+++ b/code/modules/fishing/fishing_portal_machine.dm
@@ -1,40 +1,58 @@
/obj/machinery/fishing_portal_generator
name = "fish-porter 3000"
- desc = "fishing anywhere, anytime, anyway what was i talking about"
-
+ desc = "Fishing anywhere, anytime... anyway what was I talking about?"
icon = 'icons/obj/fishing.dmi'
- icon_state = "portal_off"
-
+ icon_state = "portal"
idle_power_usage = 0
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 2
-
anchored = FALSE
density = TRUE
+ circuit = /obj/item/circuitboard/machine/fishing_portal_generator
- var/fishing_source = /datum/fish_source/portal
+ ///The current fishing spot loaded in
var/datum/component/fishing_spot/active
+/obj/machinery/fishing_portal_generator/on_set_panel_open()
+ update_appearance()
+ return ..()
+
/obj/machinery/fishing_portal_generator/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return TOOL_ACT_TOOLTYPE_SUCCESS
+/obj/machinery/fishing_portal_generator/examine(mob/user)
+ . = ..()
+ . += span_notice("You can unlock further portal settings by completing fish scanning experiments.")
+
+/obj/machinery/fishing_portal_generator/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ obj_flags |= EMAGGED
+ balloon_alert(user, "syndicate setting loaded")
+ playsound(src, SFX_SPARKS, 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
/obj/machinery/fishing_portal_generator/interact(mob/user, special_state)
. = ..()
if(active)
deactivate()
else
- activate()
+ select_fish_source(user)
-/obj/machinery/fishing_portal_generator/update_icon(updates)
+/obj/machinery/fishing_portal_generator/update_overlays()
. = ..()
- if(active)
- icon_state = "portal_on"
- else
- icon_state = "portal_off"
+ if(panel_open)
+ . += "portal_open"
+ if(!active)
+ return
+ . += "portal_on"
+ var/datum/fish_source/portal/portal = active.fish_source
+ . += portal.overlay_state
+ . += emissive_appearance(icon, "portal_emissive", src)
-/obj/machinery/fishing_portal_generator/proc/activate()
- active = AddComponent(/datum/component/fishing_spot, fishing_source)
+/obj/machinery/fishing_portal_generator/proc/activate(datum/fish_source/selected_source)
+ active = AddComponent(/datum/component/fishing_spot, selected_source)
use_power = ACTIVE_POWER_USE
update_icon()
@@ -46,3 +64,39 @@
/obj/machinery/fishing_portal_generator/on_set_is_operational(old_value)
if(old_value)
deactivate()
+
+///Create a radial menu from a list of available fish sources. If only the default is available, activate it right away.
+/obj/machinery/fishing_portal_generator/proc/select_fish_source(mob/user)
+ var/datum/fish_source/portal/default = GLOB.preset_fish_sources[/datum/fish_source/portal]
+ var/list/available_fish_sources = list(default.radial_name = default)
+ if(obj_flags & EMAGGED)
+ var/datum/fish_source/portal/syndicate = GLOB.preset_fish_sources[/datum/fish_source/portal/syndicate]
+ available_fish_sources[syndicate.radial_name] = syndicate
+ for (var/datum/techweb/techweb as anything in SSresearch.techwebs)
+ var/get_fish_sources = FALSE
+ for(var/obj/machinery/rnd/server/server as anything in techweb.techweb_servers)
+ if(!is_valid_z_level(get_turf(server), get_turf(src)))
+ continue
+ get_fish_sources = TRUE
+ break
+ if(!get_fish_sources)
+ continue
+ for(var/experiment_type in typesof(/datum/experiment/scanning/fish))
+ var/datum/experiment/scanning/fish/experiment = techweb.completed_experiments[experiment_type]
+ if(!experiment)
+ continue
+ var/datum/fish_source/portal/reward = GLOB.preset_fish_sources[experiment.fish_source_reward]
+ available_fish_sources[reward.radial_name] = reward
+
+ if(length(available_fish_sources) == 1)
+ activate(default)
+ return
+ var/list/choices = list()
+ for(var/radial_name in available_fish_sources)
+ var/datum/fish_source/portal/source = available_fish_sources[radial_name]
+ choices[radial_name] = image(icon = 'icons/hud/radial_fishing.dmi', icon_state = source.radial_state)
+
+ var/choice = show_radial_menu(user, src, choices, radius = 38, custom_check = CALLBACK(src, TYPE_PROC_REF(/atom, can_interact), user), tooltips = TRUE)
+ if(!choice || !can_interact(user))
+ return
+ activate(available_fish_sources[choice])
diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm
index ed9e164644e..1abba8e414c 100644
--- a/code/modules/fishing/fishing_rod.dm
+++ b/code/modules/fishing/fishing_rod.dm
@@ -67,7 +67,6 @@
/obj/item/fishing_rod/examine(mob/user)
. = ..()
- . += "Right-Click in your active hand to access its slots UI"
var/list/equipped_stuff = list()
if(line)
equipped_stuff += "[icon2html(line, user)] [line.name]"
@@ -79,6 +78,7 @@
. += span_notice("\a [icon2html(bait, user)] [bait] is being used as bait.")
else
. += span_warning("It doesn't have any bait attached. Fishing will be more tedious!")
+ . += span_notice("Right-Click in your active hand to access its slots UI")
/**
* Catch weight modifier for the given fish_type (or FISHING_DUD)
@@ -185,6 +185,7 @@
return
currently_hooked_item = target_atom
create_fishing_line(target_atom)
+ SEND_SIGNAL(src, COMSIG_FISHING_ROD_HOOKED_ITEM, target_atom, user)
/// Checks what can be hooked
/obj/item/fishing_rod/proc/can_be_hooked(atom/movable/target)
@@ -508,16 +509,34 @@
/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."
+ ui_description = "This rod has an infinite supply of synth-bait. Also doubles as an Experi-Scanner for fish."
icon_state = "fishing_rod_science"
reel_overlay = "reel_science"
/obj/item/fishing_rod/tech/Initialize(mapload)
. = ..()
+
+ var/static/list/fishing_signals = list(
+ COMSIG_FISHING_ROD_HOOKED_ITEM = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_FISHING_ROD_CAUGHT_FISH = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_PRE_ATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_handheld_experiment),
+ COMSIG_ITEM_AFTERATTACK = TYPE_PROC_REF(/datum/component/experiment_handler, ignored_handheld_experiment_attempt),
+ )
+ AddComponent(/datum/component/experiment_handler, \
+ config_mode = EXPERIMENT_CONFIG_ALTCLICK, \
+ allowed_experiments = list(/datum/experiment/scanning/fish), \
+ config_flags = EXPERIMENT_CONFIG_SILENT_FAIL|EXPERIMENT_CONFIG_IMMEDIATE_ACTION, \
+ experiment_signals = fishing_signals, \
+ )
+
var/obj/item/food/bait/doughball/synthetic/infinite_supply_of_bait = new(src)
bait = infinite_supply_of_bait
update_icon()
+/obj/item/fishing_rod/tech/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-Click to access the Experiment Configuration UI")
+
/obj/item/fishing_rod/tech/consume_bait(atom/movable/reward)
return
diff --git a/code/modules/fishing/sources/_fish_source.dm b/code/modules/fishing/sources/_fish_source.dm
index 5a34db9ce5f..657b2f30968 100644
--- a/code/modules/fishing/sources/_fish_source.dm
+++ b/code/modules/fishing/sources/_fish_source.dm
@@ -1,11 +1,47 @@
GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_source, list()))
+/**
+ * When adding new fishable rewards to a table/counts, you can specify an icon to show in place of the
+ * generic fish icon in the minigame UI should the user have the TRAIT_REVEAL_FISH trait, by adding it to
+ * this list.
+ *
+ * A lot of the icons here may be a tad inaccurate, but since we're limited to the free font awesome icons we
+ * have access to, we got to make do.
+ */
+GLOBAL_LIST_INIT(specific_fish_icons, zebra_typecacheof(list(
+ /mob/living/basic/carp = FISH_ICON_DEF,
+ /mob/living/basic/mining = FISH_ICON_HOSTILE,
+ /obj/effect/decal/remains = FISH_ICON_BONE,
+ /obj/effect/mob_spawn/corpse = FISH_ICON_BONE,
+ /obj/item/coin = FISH_ICON_COIN,
+ /obj/item/fish = FISH_ICON_DEF,
+ /obj/item/fish/armorfish = FISH_ICON_CRAB,
+ /obj/item/fish/boned = FISH_ICON_BONE,
+ /obj/item/fish/chasm_crab = FISH_ICON_CRAB,
+ /obj/item/fish/gunner_jellyfish = FISH_ICON_JELLYFISH,
+ /obj/item/fish/holo/crab = FISH_ICON_CRAB,
+ /obj/item/fish/holo/puffer = FISH_ICON_CHUNKY,
+ /obj/item/fish/mastodon = FISH_ICON_BONE,
+ /obj/item/fish/pufferfish = FISH_ICON_CHUNKY,
+ /obj/item/fish/slimefish = FISH_ICON_SLIME,
+ /obj/item/fish/sludgefish = FISH_ICON_SLIME,
+ /obj/item/fish/starfish = FISH_ICON_STAR,
+ /obj/item/storage/wallet = FISH_ICON_COIN,
+ /obj/item/stack/sheet/bone = FISH_ICON_BONE,
+ /obj/item/stack/sheet/mineral = FISH_ICON_GEM,
+ /obj/item/stack/ore = FISH_ICON_GEM,
+ /obj/structure/closet/crate = FISH_ICON_COIN,
+)))
+
/**
* 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
+ /**
+ * Fish catch weight table - these are relative weights
+ *
+ */
var/list/fish_table = list()
/// If a key from fish_table is present here, that fish is availible in limited quantity and is reduced by one on successful fishing
var/list/fish_counts = list()
@@ -25,6 +61,10 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
if(!(path in fish_table))
stack_trace("path [path] found in the 'fish_counts' list but not in the fish_table one of [type]")
+///Called when src is set as the fish source of a fishing spot component
+/datum/fish_source/proc/on_fishing_spot_init(/datum/component/fishing_spot/spot)
+ return
+
/// Can we fish in this spot at all. Returns DENIAL_REASON or null if we're good to go
/datum/fish_source/proc/reason_we_cant_fish(obj/item/fishing_rod/rod, mob/fisherman)
return rod.reason_we_cant_fish(src)
@@ -35,7 +75,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* This includes the source's fishing difficulty, that of the fish, the rod,
* favorite and disliked baits, fish traits and the fisherman skill.
*
- * For non-fish, it's just the source's fishing difficulty minus the fisherman skill, rod and settler modifiers.
+ * For non-fish, it's just the source's fishing difficulty minus the fisherman skill.
*/
/datum/fish_source/proc/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
. = fishing_difficulty
@@ -96,7 +136,7 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
* 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)
+/datum/fish_source/proc/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
return
///Proc called when the challenge is interrupted within the fish source code.
@@ -116,7 +156,9 @@ GLOBAL_LIST_INIT(preset_fish_sources, init_subtypes_w_path_keys(/datum/fish_sour
user.add_mob_memory(/datum/memory/caught_fish, protagonist = user, deuteragonist = initial(caught.name))
var/turf/fishing_spot = get_turf(source.lure)
var/atom/movable/reward = dispense_reward(source.reward_path, user, fishing_spot)
- source.used_rod?.consume_bait(reward)
+ if(source.used_rod)
+ SEND_SIGNAL(source.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user)
+ source.used_rod.consume_bait(reward)
/// Gives out the reward if possible
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, turf/fishing_spot)
diff --git a/code/modules/fishing/sources/source_types.dm b/code/modules/fishing/sources/source_types.dm
index ffb37753881..e2e5491dd1d 100644
--- a/code/modules/fishing/sources/source_types.dm
+++ b/code/modules/fishing/sources/source_types.dm
@@ -19,11 +19,168 @@
/datum/fish_source/portal
fish_table = list(
- FISHING_DUD = 5,
+ FISHING_DUD = 7,
/obj/item/fish/goldfish = 10,
/obj/item/fish/guppy = 10,
+ /obj/item/fish/angelfish = 10,
)
- catalog_description = "Fish dimension (Fishing portal generator)"
+ catalog_description = "Aquarium dimension (Fishing portal generator)"
+ ///The name of this option shown in the radial menu on the fishing portal generator
+ var/radial_name = "Aquarium"
+ ///The icon state shown for this option in the radial menu
+ var/radial_state = "fish_tank"
+ ///The icon state of the overlay shown on the machine when active.
+ var/overlay_state = "portal_aquarium"
+
+/datum/fish_source/portal/beach
+ fish_table = list(
+ FISHING_DUD = 10,
+ /obj/item/fish/clownfish = 10,
+ /obj/item/fish/pufferfish = 10,
+ /obj/item/fish/cardinal = 10,
+ /obj/item/fish/greenchromis = 10,
+ )
+ catalog_description = "Beach dimension (Fishing portal generator)"
+ radial_name = "Beach"
+ radial_state = "palm_beach"
+
+/datum/fish_source/portal/chasm
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/chasm_crab = 10,
+ /obj/item/fish/boned = 5,
+ /obj/item/stack/sheet/bone = 5,
+ )
+ catalog_description = "Chasm dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Chasm"
+ overlay_state = "portal_chasm"
+ radial_state = "ground_hole"
+
+/datum/fish_source/portal/ocean
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/lanternfish = 5,
+ /obj/item/fish/firefish = 5,
+ /obj/item/fish/dwarf_moonfish = 5,
+ /obj/item/fish/gunner_jellyfish = 5,
+ /obj/item/fish/needlefish = 5,
+ /obj/item/fish/armorfish = 5,
+ )
+ catalog_description = "Ocean dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Ocean"
+ overlay_state = "portal_ocean"
+ radial_state = "seaboat"
+
+/datum/fish_source/portal/hyperspace
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/starfish = 6,
+ /obj/item/stack/ore/bluespace_crystal = 2,
+ /mob/living/basic/carp = 2,
+ )
+ fish_counts = list(
+ /obj/item/stack/ore/bluespace_crystal = 10,
+ )
+ catalog_description = "Hyperspace dimension (Fishing portal generator)"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+ radial_name = "Hyperspace"
+ overlay_state = "portal_hyperspace"
+ radial_state = "space_rocket"
+
+///Unlocked by emagging the fishing portal generator with an emag.
+/datum/fish_source/portal/syndicate
+ background = "background_lavaland"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/fish/donkfish = 5,
+ /obj/item/fish/emulsijack = 5,
+ )
+ catalog_description = "Syndicate dimension (Fishing portal generator)"
+ radial_name = "Syndicate"
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
+ overlay_state = "portal_syndicate"
+ radial_state = "syndi_snake"
+
+/**
+ * A special portal fish source which fish table is populated on init with the contents of all
+ * portal fish sources, except for FISHING_DUD, and a couple more caveats.
+ */
+/datum/fish_source/portal/random
+ fish_table = null //It's populated the first time the source is loaded on a fishing portal generator.
+ catalog_description = null // it'd make a bad entry in the catalog.
+ radial_name = "Randomizer"
+ overlay_state = "portal_randomizer"
+ var/static/list/all_portal_fish_sources_at_once
+ radial_state = "misaligned_question_mark"
+
+///Generate the fish table if we don't have one already.
+/datum/fish_source/portal/random/on_fishing_spot_init(datum/component/fishing_spot/spot)
+ if(fish_table)
+ return
+
+ ///rewards not found in other fishing portals
+ fish_table = list(
+ /obj/item/fish/holo/checkered = 1,
+ )
+
+ for(var/portal_type in GLOB.preset_fish_sources)
+ if(portal_type == type || !ispath(portal_type, /datum/fish_source/portal))
+ continue
+ var/datum/fish_source/portal/preset_portal = GLOB.preset_fish_sources[portal_type]
+ fish_table |= preset_portal.fish_table
+
+ ///We don't serve duds.
+ fish_table -= FISHING_DUD
+
+ for(var/reward_path in fish_table)
+ fish_table[reward_path] = rand(1, 4)
+
+///Difficulty has to be calculated before the rest, because of how it influences jump chances
+/datum/fish_source/portal/random/calculate_difficulty(result, obj/item/fishing_rod/rod, mob/fisherman, datum/fishing_challenge/challenge)
+ . = ..()
+ . += rand(-10, 15)
+
+///In the spirit of randomness, we skew a few values here and there
+/datum/fish_source/portal/random/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
+ challenge.bait_bounce_mult = clamp(challenge.bait_bounce_mult + (rand(-3, 3) * 0.1), 0.1, 1)
+ challenge.completion_loss = max(challenge.completion_loss + rand(-2, 2), 0)
+ challenge.completion_gain = max(challenge.completion_gain + rand(-1, 1), 2)
+ challenge.short_jump_velocity_limit += rand(-100, 100)
+ challenge.long_jump_velocity_limit += rand(-100, 100)
+ var/static/list/active_effects = bitfield_to_list(FISHING_MINIGAME_ACTIVE_EFFECTS)
+ for(var/effect in active_effects)
+ if(prob(30))
+ challenge.special_effects |= effect
+
+///Cherry on top, fish caught from the randomizer portal also have (almost completely) random traits
+/datum/fish_source/portal/random/spawn_reward(reward_path, mob/fisherman, turf/fishing_spot)
+ if(!ispath(reward_path, /obj/item/fish))
+ return ..()
+
+ var/static/list/weighted_traits
+ if(!weighted_traits)
+ weighted_traits = list()
+ for(var/trait_type as anything in GLOB.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ weighted_traits[trait.type] = round(trait.inheritability**2/100)
+
+ var/obj/item/fish/caught_fish = new reward_path(get_turf(fisherman), FALSE)
+ var/list/fixed_traits = list()
+ for(var/trait_type in caught_fish.fish_traits)
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ if(caught_fish.type in trait.guaranteed_inheritance_types)
+ fixed_traits += trait_type
+ var/list/new_traits = list()
+ for(var/iteration in rand(1, 4))
+ new_traits |= pick_weight(weighted_traits)
+ caught_fish.inherit_traits(new_traits, fixed_traits = fixed_traits)
+ caught_fish.randomize_size_and_weight(deviation = 0.3)
+ caught_fish.progenitors = full_capitalize(caught_fish.name)
+ return caught_fish
+
/datum/fish_source/chasm
catalog_description = "Chasm depths"
@@ -99,7 +256,7 @@
fish_table = list(
FISHING_DUD = 18,
/obj/item/fish/sludgefish = 18,
- /obj/item/fish/slimefish = 2,
+ /obj/item/fish/slimefish = 4,
/obj/item/storage/wallet/money = 2,
)
fish_counts = list(
@@ -125,7 +282,7 @@
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)
+/datum/fish_source/holographic/pre_challenge_started(obj/item/fishing_rod/rod, mob/user, datum/fishing_challenge/challenge)
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_area))
/datum/fish_source/holographic/proc/check_area(mob/user)
diff --git a/code/modules/library/skill_learning/skillchip.dm b/code/modules/library/skill_learning/skillchip.dm
index f9fd629c1ed..762e8e0162c 100644
--- a/code/modules/library/skill_learning/skillchip.dm
+++ b/code/modules/library/skill_learning/skillchip.dm
@@ -489,4 +489,13 @@
activate_message = span_notice("You recall learning from your grandmother how they baked their cookies with love.")
deactivate_message = span_notice("You forget all memories imparted upon you by your grandmother. Were they even your real grandma?")
+/obj/item/skillchip/master_angler
+ name = "Mast-Angl-Er skillchip"
+ auto_traits = list(TRAIT_REVEAL_FISH)
+ skill_name = "Fisherman's Discernment"
+ skill_description = "While fishing, it'll make a smidge easier to guess whatever you're trying to catch."
+ skill_icon = "fish"
+ activate_message = span_notice("You feel the knowledge and passion of several sunbaked, seasoned fishermen burn within you.")
+ deactivate_message = span_notice("You no longer feel like casting a fishing rod by the sunny riverside.")
+
#undef SKILLCHIP_CATEGORY_GENERAL
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 72bdaa02a95..0a2c96dddd5 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1096,3 +1096,13 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ROBOTICS
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/board/fishing_portal_generator
+ name = "Fishing Portal Generator Board"
+ desc = "The circuit board for the fishing portal generator"
+ id = "fishing_portal_generator"
+ build_path = /obj/item/circuitboard/machine/fishing_portal_generator
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_SERVICE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm
index 521504da351..8a06607ec59 100644
--- a/code/modules/research/techweb/_techweb.dm
+++ b/code/modules/research/techweb/_techweb.dm
@@ -283,7 +283,7 @@
var/datum/experiment/experiment = completed_experiment
if (experiment == experiment_type)
return FALSE
- available_experiments += new experiment_type()
+ available_experiments += new experiment_type(src)
/**
* Adds a list of experiments to this techweb by their types, ensures that no duplicates are added.
@@ -310,13 +310,21 @@
var/refund = skipped_experiment_types[completed_experiment.type] || 0
if(refund > 0)
add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = refund))
- result_text += ", refunding [refund] points."
+ result_text += ", refunding [refund] points"
// Nothing more to gain here, but we keep it in the list to prevent double dipping
skipped_experiment_types[completed_experiment.type] = -1
- else
- result_text += "!"
+ var/points_rewarded
+ if(completed_experiment.points_reward)
+ add_point_list(completed_experiment.points_reward)
+ points_rewarded = ",[refund > 0 ? " and" : ""] rewarding "
+ var/list/english_list_keys = list()
+ for(var/points_type in completed_experiment.points_reward)
+ english_list_keys += "[completed_experiment.points_reward[points_type]] [points_type]"
+ points_rewarded += "[english_list(english_list_keys)] points"
+ result_text += points_rewarded
+ result_text += "!"
- log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""].")
+ log_research("[completed_experiment.name] ([completed_experiment.type]) has been completed on techweb [id]/[organization][refund ? ", refunding [refund] points" : ""][points_rewarded].")
return result_text
/datum/techweb/proc/printout_points()
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index d12ebfaca1e..507199164b8 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -49,6 +49,7 @@
"extinguisher",
"fax",
"fishing_rod",
+ "fishing_portal_generator",
"flashlight",
"fluid_ducts",
"foam_dart",
@@ -1490,6 +1491,19 @@
required_experiments = list(/datum/experiment/scanning/random/plants/wild)
discount_experiments = list(/datum/experiment/scanning/random/plants/traits = 3000)
+/datum/techweb_node/fishing
+ id = "fishing"
+ display_name = "Fishing Technology"
+ description = "Cutting edge fishing advancements."
+ prereq_ids = list("base")
+ design_ids = list(
+ "fishing_rod_tech",
+ "stabilized_hook",
+ "fish_analyzer",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2000)
+ required_experiments = list(/datum/experiment/scanning/fish)
+
/datum/techweb_node/exp_tools
id = "exp_tools"
display_name = "Experimental Tools"
@@ -2354,20 +2368,6 @@
hidden = TRUE
experimental = TRUE
-/datum/techweb_node/fishing
- id = "fishing"
- display_name = "Fishing Technology"
- description = "Cutting edge fishing advancements."
- prereq_ids = list("base")
- design_ids = list(
- "fishing_rod_tech",
- "stabilized_hook",
- "fish_analyzer",
- )
- research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
- hidden = TRUE
- experimental = TRUE
-
/datum/techweb_node/advanced_plastic_surgery
id = "plastic_surgery"
display_name = "Advanced Plastic Surgery"
diff --git a/code/modules/unit_tests/fish_unit_tests.dm b/code/modules/unit_tests/fish_unit_tests.dm
index 1ef15f8d0f5..d0d39227f43 100644
--- a/code/modules/unit_tests/fish_unit_tests.dm
+++ b/code/modules/unit_tests/fish_unit_tests.dm
@@ -28,10 +28,10 @@
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(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(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.
@@ -41,11 +41,24 @@
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_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")
+/datum/unit_test/fish_scanning
+
+/datum/unit_test/fish_scanning/Run()
+ var/scannable_fishes = 0
+ for(var/obj/item/fish/fish_prototype as anything in subtypesof(/obj/item/fish))
+ if(initial(fish_prototype.experisci_scannable))
+ scannable_fishes++
+ for(var/datum/experiment/scanning/fish/fish_scan as anything in typesof(/datum/experiment/scanning/fish))
+ fish_scan = new fish_scan
+ var/scan_key = fish_scan.required_atoms[1]
+ if(fish_scan.required_atoms[scan_key] > scannable_fishes)
+ TEST_FAIL("[fish_scan.type] has requirements higher than the number of scannable fish types in the game: [scannable_fishes]")
+
///dummy fish item used for the tests, as well with related subtypes and datums.
/obj/item/fish/testdummy
grind_results = list()
diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm
index 80fb1350841..e51205c00e4 100644
--- a/code/modules/vending/games.dm
+++ b/code/modules/vending/games.dm
@@ -56,6 +56,7 @@
/obj/item/skillchip/sabrage = 2,
/obj/item/skillchip/useless_adapter = 5,
/obj/item/skillchip/wine_taster = 2,
+ /obj/item/skillchip/master_angler = 2,
),
),
list(
diff --git a/icons/hud/fishing_hud.dmi b/icons/hud/fishing_hud.dmi
index b68acee09b7..58c478d0710 100644
Binary files a/icons/hud/fishing_hud.dmi and b/icons/hud/fishing_hud.dmi differ
diff --git a/icons/hud/radial_fishing.dmi b/icons/hud/radial_fishing.dmi
new file mode 100644
index 00000000000..65fd55176b7
Binary files /dev/null and b/icons/hud/radial_fishing.dmi differ
diff --git a/icons/obj/aquarium.dmi b/icons/obj/aquarium.dmi
index 3a27c83c906..19e2e68c4f8 100644
Binary files a/icons/obj/aquarium.dmi and b/icons/obj/aquarium.dmi differ
diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi
index 39bcc853442..f7ab9fc1ad9 100644
Binary files a/icons/obj/fishing.dmi and b/icons/obj/fishing.dmi differ
diff --git a/sound/attributions.txt b/sound/attributions.txt
index 09ac2bd5642..3ae6c797dd3 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -99,3 +99,9 @@ https://freesound.org/people/FunWithSound/sounds/456965/
beeps_jingle.ogg is adapted from Eponn's "Achievement happy Beeps Jingle", which is public domain (CC 0):
https://freesound.org/people/Eponn/sounds/619838/
+
+boing.ogg is adapted from reelworldstudio's "Cartoon Boing", which is public domain (CC 0):
+https://freesound.org/people/reelworldstudio/sounds/161122/
+
+arcade_jump.ogg is adapted from se2001's "8-Bit Jump 3", which is public domain (CC 0):
+hhttps://freesound.org/people/se2001/sounds/528568/
diff --git a/sound/effects/arcade_jump.ogg b/sound/effects/arcade_jump.ogg
new file mode 100644
index 00000000000..65f0cc448b5
Binary files /dev/null and b/sound/effects/arcade_jump.ogg differ
diff --git a/sound/effects/boing.ogg b/sound/effects/boing.ogg
new file mode 100644
index 00000000000..8328cc33926
Binary files /dev/null and b/sound/effects/boing.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 972ed35951e..2fe8275a014 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -39,7 +39,6 @@
#include "code\__DEFINES\antagonists.dm"
#include "code\__DEFINES\apc_defines.dm"
#include "code\__DEFINES\appearance.dm"
-#include "code\__DEFINES\aquarium.dm"
#include "code\__DEFINES\area_editor.dm"
#include "code\__DEFINES\art.dm"
#include "code\__DEFINES\assemblies.dm"
@@ -93,7 +92,7 @@
#include "code\__DEFINES\external_organs.dm"
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
-#include "code\__DEFINES\fishing.dm"
+#include "code\__DEFINES\fish.dm"
#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\font_awesome_icons.dm"
@@ -3759,6 +3758,7 @@
#include "code\modules\experisci\experiment\types\physical_experiment.dm"
#include "code\modules\experisci\experiment\types\random_scanning.dm"
#include "code\modules\experisci\experiment\types\scanning.dm"
+#include "code\modules\experisci\experiment\types\scanning_fish.dm"
#include "code\modules\experisci\experiment\types\scanning_machinery.dm"
#include "code\modules\experisci\experiment\types\scanning_material.dm"
#include "code\modules\experisci\experiment\types\scanning_people.dm"
diff --git a/tgui/packages/tgui/interfaces/ExperimentConfigure.js b/tgui/packages/tgui/interfaces/ExperimentConfigure.js
index e515c2bfb94..02cc759dd7f 100644
--- a/tgui/packages/tgui/interfaces/ExperimentConfigure.js
+++ b/tgui/packages/tgui/interfaces/ExperimentConfigure.js
@@ -142,7 +142,7 @@ export const ExperimentConfigure = (props, context) => {
{experiments.map((exp, i) => {
- return ;
+ return ;
})}
@@ -168,44 +168,24 @@ export const ExperimentConfigure = (props, context) => {
export const Experiment = (props, context) => {
const { act, data } = useBackend(context);
- const { exp, controllable } = props;
- const {
- name,
- description,
- tag,
- selectable,
- selected,
- progress,
- performance_hint,
- ref,
- } = exp;
+ const { exp } = props;
+ const { name, description, tag, selected, progress, performance_hint, ref } =
+ exp;
return (