diff --git a/_maps/map_files/daftmarsh/daftmarsh.dmm b/_maps/map_files/daftmarsh/daftmarsh.dmm
index f03f65ea31d..37b8d66a975 100644
--- a/_maps/map_files/daftmarsh/daftmarsh.dmm
+++ b/_maps/map_files/daftmarsh/daftmarsh.dmm
@@ -11618,7 +11618,7 @@
/area/outdoors/town)
"kdA" = (
/obj/structure/closet/crate/chest/crate,
-/obj/item/fishing/lure,
+/obj/effect/spawner/map_spawner/random_lure,
/turf/open/floor/wood,
/area/indoors/town)
"kdP" = (
diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm
index 223bd2cda05..ff3ec39d959 100644
--- a/_maps/map_files/debug/roguetest.dmm
+++ b/_maps/map_files/debug/roguetest.dmm
@@ -2823,9 +2823,7 @@
/turf/open/floor/grass/yel,
/area/outdoors)
"GO" = (
-/obj/effect/landmark/start/adventurerlate,
/obj/machinery/light/fueled/wallfire/candle,
-/obj/effect/landmark/latejoin,
/turf/open/floor/ruinedwood,
/area/outdoors)
"GU" = (
@@ -3410,6 +3408,8 @@
pixel_x = -6
},
/obj/structure/table/vtable/v2,
+/obj/effect/spawner/map_spawner/random_lure,
+/obj/effect/spawner/map_spawner/random_lure,
/turf/open/floor/wood,
/area/indoors/town/manor)
"VA" = (
diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm
index 2887418e8bc..229f359315e 100644
--- a/_maps/map_files/vanderlin/vanderlin.dmm
+++ b/_maps/map_files/vanderlin/vanderlin.dmm
@@ -1670,7 +1670,7 @@
/area/indoors/town/orphanage)
"aMw" = (
/obj/structure/closet/crate/chest/neu,
-/obj/item/fishing/lure,
+/obj/effect/spawner/map_spawner/random_lure,
/obj/effect/decal/cleanable/dirt/dust,
/turf/open/floor/wood,
/area/indoors/town)
diff --git a/_maps/matthios_tomb/room/goblincamp.dmm b/_maps/matthios_tomb/room/goblincamp.dmm
index 6598abacef8..73d4b0769d8 100644
--- a/_maps/matthios_tomb/room/goblincamp.dmm
+++ b/_maps/matthios_tomb/room/goblincamp.dmm
@@ -5,7 +5,7 @@
"ao" = (
/obj/structure/chair/stool,
/obj/item/fishingrod,
-/obj/item/fishing/lure,
+/obj/effect/spawner/map_spawner/random_lure,
/turf/open/floor/ruinedwood,
/area/under/tomb/cave)
"aN" = (
diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index d09c42f651b..0e04f5a5e24 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -114,6 +114,7 @@
#define COMSIG_MOB_UPDATE_SIGHT "mob_update_sight" //from base of /mob/update_sight(): ()
#define COMSIG_MOB_SAY "mob_say" // from /mob/living/say(): ()
#define COMPONENT_UPPERCASE_SPEECH 1
+ #define COMPONENT_SPEECH_CANCEL (1<<1)
// used to access COMSIG_MOB_SAY argslist
#define SPEECH_MESSAGE 1
// #define SPEECH_BUBBLE_TYPE 2
@@ -347,3 +348,13 @@
#define COMSIG_TIPS_REMOVE "comsig_tip_remove"
///used incase we care about a tracker dying
#define COMSIG_LIVING_TRACKER_REMOVED "tracker_removed"
+///used when a command is issued to someone, if they have the correct component acts on this
+#define COMSIG_PARENT_COMMAND_RECEIVED "command_received"
+
+#define COMSIG_AUGMENT_INSTALL "augment_install"
+#define COMSIG_AUGMENT_REMOVE "augment_remove"
+#define COMSIG_AUGMENT_REPAIR "augment_repair"
+#define COMSIG_AUGMENT_GET_STABILITY "augment_get_stability"
+
+#define COMPONENT_AUGMENT_SUCCESS (1<<0)
+#define COMPONENT_AUGMENT_FAILED (1<<1)
diff --git a/code/__DEFINES/fish.dm b/code/__DEFINES/fish.dm
index cfac1ccc5be..414edd7d3d4 100644
--- a/code/__DEFINES/fish.dm
+++ b/code/__DEFINES/fish.dm
@@ -202,7 +202,7 @@
///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 60
+#define FISH_FILLET_NUMBER_SIZE_DIVISOR 30
///The slowdown of the fish when carried begins at this value
#define FISH_WEIGHT_SLOWDOWN 2100
diff --git a/code/__DEFINES/footsteps.dm b/code/__DEFINES/footsteps.dm
index b6f16b99423..ca65e08161b 100644
--- a/code/__DEFINES/footsteps.dm
+++ b/code/__DEFINES/footsteps.dm
@@ -27,6 +27,7 @@
#define FOOTSTEP_MOB_SHOE "footstep_shoe"
#define FOOTSTEP_MOB_HUMAN "footstep_human" //Warning: Only works on /mob/living/carbon/human
#define FOOTSTEP_MOB_SLIME "footstep_slime"
+#define FOOTSTEP_MOB_METAL "footstep_metal"
//priority defines for the footstep_override element
#define STEP_SOUND_NO_PRIORITY 0
@@ -186,3 +187,24 @@ GLOBAL_LIST_INIT(heavyfootstep, list(
'sound/foley/footsteps/FTMUD (5).ogg'), 100, 0),
))
+GLOBAL_LIST_INIT(metalfootstep, list(
+ FOOTSTEP_GENERIC_HEAVY = list(list(
+ 'sound/foley/footsteps/armor/powerarmor (1).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (2).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (3).ogg',), 100, 0),
+ FOOTSTEP_WATER = list(list(
+ 'sound/foley/footsteps/armor/powerarmor (1).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (2).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (3).ogg',), 100, 0),
+ FOOTSTEP_SHALLOW = list(list(
+ 'sound/foley/footsteps/armor/powerarmor (1).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (2).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (3).ogg',), 100, 0),
+ FOOTSTEP_LAVA = list(list(
+ 'sound/blank.ogg'), 100, 0),
+ FOOTSTEP_MUD = list(list(
+ 'sound/foley/footsteps/armor/powerarmor (1).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (2).ogg',
+ 'sound/foley/footsteps/armor/powerarmor (3).ogg',), 100, 0),
+))
+
diff --git a/code/__DEFINES/species/_species.dm b/code/__DEFINES/species/_species.dm
index b198c229ce5..bedef0eb389 100644
--- a/code/__DEFINES/species/_species.dm
+++ b/code/__DEFINES/species/_species.dm
@@ -15,6 +15,7 @@
#define SPEC_ID_TRITON "triton"
#define SPEC_ID_MEDICATOR "medicator"
#define SPEC_ID_HALFLING "halfling"
+#define SPEC_ID_AUTOMATON "automaton"
#define SPEC_ID_ORC "orc"
#define SPEC_ID_GOBLIN "goblin"
@@ -44,6 +45,7 @@
SPEC_ID_ZIZOMBIE,\
SPEC_ID_HUMAN_SPACE,\
SPEC_ID_HALFLING,\
+ SPEC_ID_AUTOMATON, \
)
/// Species where females get underwear, no underwear for kobold, rakshari, medicator and triton, dwarves handled seperately
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 337762ae04e..d20324f5532 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -161,6 +161,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_NOLIMBDISABLE "no_limb_disable"
#define TRAIT_EASYLIMBDISABLE "easy_limb_disable"
#define TRAIT_TOXINLOVER "toxinlover"
+#define TRAIT_NO_EXPERIENCE "unlearning"
#define TRAIT_NOBREATH "no_breath"
#define TRAIT_HOLY "holy"
#define TRAIT_NOCRITDAMAGE "no_crit"
diff --git a/code/__HELPERS/fish.dm b/code/__HELPERS/fish.dm
index 676d408d6c7..363303e163f 100644
--- a/code/__HELPERS/fish.dm
+++ b/code/__HELPERS/fish.dm
@@ -8,7 +8,8 @@
if(FISH_BAIT_FOODTYPE)
if(isfood(bait))
return bait:foodtype & special_identifier[FISH_BAIT_VALUE]
- return NONE
+ else
+ return bait:bait_flag & special_identifier[FISH_BAIT_VALUE]
if(FISH_BAIT_REAGENT)
return bait.reagents?.has_reagent(special_identifier[FISH_BAIT_VALUE], special_identifier[FISH_BAIT_AMOUNT], check_subtypes = TRUE)
else
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index 30bfbe3766d..14d1b236634 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -658,6 +658,9 @@ SUBSYSTEM_DEF(job)
/// Gives the player the stuff they should have with their rank
/datum/controller/subsystem/job/proc/EquipRank(mob/living/carbon/human/equipping, datum/job/job, client/player_client)
equipping.job = job.title
+ equipping.job_type = job.type
+ if(job.parent_job)
+ equipping.job_type = job.parent_job.type
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
diff --git a/code/controllers/subsystem/merchant.dm b/code/controllers/subsystem/merchant.dm
index 1f6aba2b0bd..9aa35ba32fa 100644
--- a/code/controllers/subsystem/merchant.dm
+++ b/code/controllers/subsystem/merchant.dm
@@ -26,6 +26,9 @@ SUBSYSTEM_DEF(merchant)
var/list/faction_rotation_schedule = list() // When each faction becomes active
var/list/active_faction_traders = list()
+ ///this is our list of created nations
+ var/list/nations = list()
+
/// Cache of recipe component costs to avoid recalculation
var/static/list/recipe_base_values = list()
/// Cached list of all valid bounty items (items that can be obtained through gameplay)
@@ -44,6 +47,7 @@ SUBSYSTEM_DEF(merchant)
/datum/controller/subsystem/merchant/Initialize(timeofday)
+ setup_map_nations()
// Initialize recipe values and bounty cache BEFORE factions cause they use it
initialize_recipe_values()
initialize_bounty_cache()
@@ -61,6 +65,8 @@ SUBSYSTEM_DEF(merchant)
initialize_factions()
return ..()
+/datum/controller/subsystem/merchant/proc/setup_map_nations()
+ return //! TODO: when lore done set this up
/**
* Initializes recipe base values for ALL recipes
@@ -498,6 +504,17 @@ SUBSYSTEM_DEF(merchant)
continue
pack.unlocked = TRUE
+/datum/controller/subsystem/merchant/proc/handle_lift_contents(obj/structure/industrial_lift/tram/platform, list/items, datum/nation/shipped_nation)
+ if(!length(nations))
+ return
+ if(shipped_nation)
+ shipped_nation.handle_import_shipment(items, platform)
+ for(var/datum/nation/other_nation in nations)
+ if(shipped_nation == other_nation)
+ continue
+ shipped_nation.handle_global_shipment(items)
+
+
/obj/Initialize()
. = ..()
if(sellprice)
diff --git a/code/datums/components/abberant_eater.dm b/code/datums/components/abberant_eater.dm
index 52762cf6c58..42e3df6dace 100644
--- a/code/datums/components/abberant_eater.dm
+++ b/code/datums/components/abberant_eater.dm
@@ -35,6 +35,7 @@
playsound(M,'sound/misc/eat.ogg', rand(30,60), TRUE)
SEND_SIGNAL(source, COMSIG_FOOD_EATEN, M, user)
+ SEND_SIGNAL(user, COMSIG_MOB_FOOD_EAT, source)
source.on_consume(user)
qdel(source)
return TRUE
diff --git a/code/datums/components/augments/augment_datums/_base.dm b/code/datums/components/augments/augment_datums/_base.dm
new file mode 100644
index 00000000000..b444b26226e
--- /dev/null
+++ b/code/datums/components/augments/augment_datums/_base.dm
@@ -0,0 +1,16 @@
+/datum/augment
+ var/name = "base augment"
+ var/desc = "A mechanical augmentation."
+ var/stability_cost = 0 // Negative values add stability, positive values reduce it
+ var/engineering_difficulty = SKILL_LEVEL_NOVICE
+ var/installation_time = 10 SECONDS
+ var/mob/living/carbon/parent
+
+/datum/augment/proc/on_install(mob/living/carbon/human/H)
+ return
+
+/datum/augment/proc/on_remove(mob/living/carbon/human/H)
+ return
+
+/datum/augment/proc/get_examine_info()
+ return
diff --git a/code/datums/components/augments/augment_datums/skills.dm b/code/datums/components/augments/augment_datums/skills.dm
new file mode 100644
index 00000000000..3811decc64e
--- /dev/null
+++ b/code/datums/components/augments/augment_datums/skills.dm
@@ -0,0 +1,177 @@
+/datum/augment/skill
+ var/list/skill_changes = list() // List of skill changes: list(/datum/skill/combat/swords = 1)
+ stability_cost = 0 // Skills are zero-cost by default
+
+/datum/augment/skill/on_install(mob/living/carbon/human/H)
+ for(var/skill_type in skill_changes)
+ var/change = skill_changes[skill_type]
+ H.adjust_skillrank(skill_type, change, TRUE)
+
+/datum/augment/skill/on_remove(mob/living/carbon/human/H)
+ for(var/skill_type in skill_changes)
+ var/change = skill_changes[skill_type]
+ H.adjust_skillrank(skill_type, -change, TRUE)
+
+/datum/augment/skill/get_examine_info()
+ var/list/info = list()
+ info += span_info("Skill changes:")
+ for(var/skill_type in skill_changes)
+ var/datum/skill/S = skill_type
+ var/change = skill_changes[skill_type]
+ info += span_info(" [initial(S.name)]: [change > 0 ? "+" : ""][change]")
+ return info
+
+/datum/augment/skill/combat_matrix
+ name = "combat analysis matrix"
+ desc = "Advanced combat prediction algorithms enhance melee capabilities."
+ skill_changes = list(/datum/skill/combat/wrestling = 1, /datum/skill/combat/unarmed = 1)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/blade_processor
+ name = "blade trajectory processor"
+ desc = "Calculates optimal cutting angles and improves sword technique."
+ skill_changes = list(/datum/skill/combat/swords = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
+
+/datum/augment/skill/whip_servo
+ name = "whip articulation servo"
+ desc = "Precise joint control improves whip and flail technique."
+ skill_changes = list(/datum/skill/combat/whipsflails = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
+
+/datum/augment/skill/polearm_stabilizer
+ name = "polearm stability enhancer"
+ desc = "Balance optimization for polearm combat."
+ skill_changes = list(/datum/skill/combat/polearms = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
+
+/datum/augment/skill/shield_actuator
+ name = "shield response actuator"
+ desc = "Rapid reaction systems for improved shield defense."
+ skill_changes = list(/datum/skill/combat/shields = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/crossbow_targeting
+ name = "crossbow targeting system"
+ desc = "Integrated rangefinding and trajectory calculation."
+ skill_changes = list(/datum/skill/combat/crossbows = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+/datum/augment/skill/bow_stabilizer
+ name = "bow draw stabilizer"
+ desc = "Stabilizes draw and release for improved archery."
+ skill_changes = list(/datum/skill/combat/bows = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+/datum/augment/skill/smithing_optimizer
+ name = "smithing precision optimizer"
+ desc = "Enhances hammer control and metal working precision."
+ skill_changes = list(/datum/skill/craft/blacksmithing = 1, /datum/skill/craft/smelting = 1)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/weaponcraft_matrix
+ name = "weapon fabrication matrix"
+ desc = "Advanced weapon construction knowledge database."
+ skill_changes = list(/datum/skill/craft/weaponsmithing = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
+
+/datum/augment/skill/armorcraft_matrix
+ name = "armor fabrication matrix"
+ desc = "Armor construction optimization routines."
+ skill_changes = list(/datum/skill/craft/armorsmithing = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
+
+/datum/augment/skill/carpentry_guide
+ name = "carpentry guidance system"
+ desc = "Woodworking pattern recognition and optimization."
+ skill_changes = list(/datum/skill/craft/carpentry = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/masonry_analyzer
+ name = "masonry structural analyzer"
+ desc = "Stone cutting and placement optimization."
+ skill_changes = list(/datum/skill/craft/masonry = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/engineering_core
+ name = "advanced engineering core"
+ desc = "Complex mechanical systems comprehension module."
+ skill_changes = list(/datum/skill/craft/engineering = 2)
+ engineering_difficulty = SKILL_LEVEL_MASTER
+ installation_time = 25 SECONDS
+
+/datum/augment/skill/alchemy_database
+ name = "alchemical database"
+ desc = "Stored formulas and reagent interaction data."
+ skill_changes = list(/datum/skill/craft/alchemy = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+// Skill augments - Labor skills
+/datum/augment/skill/mining_efficiency
+ name = "mining efficiency module"
+ desc = "Ore vein detection and optimal extraction patterns."
+ skill_changes = list(/datum/skill/labor/mining = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/farming_analyzer
+ name = "agricultural analysis system"
+ desc = "Soil composition and crop health monitoring."
+ skill_changes = list(/datum/skill/labor/farming = 2)
+ engineering_difficulty = SKILL_LEVEL_APPRENTICE
+ installation_time = 12 SECONDS
+
+/datum/augment/skill/butchering_guide
+ name = "butchering precision guide"
+ desc = "Anatomical mapping for optimal material extraction."
+ skill_changes = list(/datum/skill/labor/butchering = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/lumberjack_optimizer
+ name = "lumber harvesting optimizer"
+ desc = "Tree structural analysis and efficient cutting patterns."
+ skill_changes = list(/datum/skill/labor/lumberjacking = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/medicine_database
+ name = "medical knowledge database"
+ desc = "Extensive anatomical and medical procedure library."
+ skill_changes = list(/datum/skill/misc/medicine = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+/datum/augment/skill/lockpick_analyzer
+ name = "lock mechanism analyzer"
+ desc = "Advanced tumbler pattern recognition."
+ skill_changes = list(/datum/skill/misc/lockpicking = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/skill/climbing_optimizer
+ name = "climbing optimization module"
+ desc = "Grip strength distribution and path finding."
+ skill_changes = list(/datum/skill/misc/climbing = 2)
+ engineering_difficulty = SKILL_LEVEL_APPRENTICE
+ installation_time = 12 SECONDS
+
+/datum/augment/skill/stealth_dampener
+ name = "acoustic dampening system"
+ desc = "Noise reduction and movement pattern optimization."
+ skill_changes = list(/datum/skill/misc/sneaking = 2)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 18 SECONDS
diff --git a/code/datums/components/augments/augment_datums/special.dm b/code/datums/components/augments/augment_datums/special.dm
new file mode 100644
index 00000000000..d97aabbe928
--- /dev/null
+++ b/code/datums/components/augments/augment_datums/special.dm
@@ -0,0 +1,206 @@
+/datum/augment/special
+ var/list/granted_actions = list()
+
+/datum/augment/special/on_install(mob/living/carbon/human/H)
+ for(var/action_type in granted_actions)
+ var/datum/action/augment/spell = new action_type
+ spell.Grant(H)
+ spell.augment = src
+
+/datum/augment/special/on_remove(mob/living/carbon/human/H)
+ for(var/action_type in granted_actions)
+ var/datum/action/A = locate(action_type) in H.actions
+ if(A)
+ A.Remove(H)
+
+/datum/augment/special/get_examine_info()
+ var/list/info = list()
+ if(granted_actions.len)
+ info += span_info("Grants special abilities")
+ return info
+
+/datum/augment/special/dualwield
+ name = "C.C.M.S implant"
+ desc = "Short for Complementary Combat Maneuvering System. Processes spinal nerve signals to enact forced complementary maneuvers, allowing dual wielding of weapons."
+ stability_cost = -20
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 25 SECONDS
+
+/datum/augment/special/dualwield/on_install(mob/living/carbon/human/H)
+ RegisterSignal(H, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_item_attack))
+ to_chat(H, span_notice("Your motor systems synchronize with the C.C.M.S implant."))
+
+/datum/augment/special/dualwield/on_remove(mob/living/carbon/human/H)
+ UnregisterSignal(H, COMSIG_MOB_ITEM_ATTACK)
+ to_chat(H, span_notice("The C.C.M.S implant's connection to your motor systems fades."))
+
+/datum/augment/special/dualwield/proc/on_item_attack(datum/source, mob/target, mob/user, params, obj/item/weapon)
+ SIGNAL_HANDLER
+
+ var/mob/living/carbon/human/H = source
+ if(!istype(H))
+ return
+
+ if(!(H.cmode))
+ return
+
+ if(weapon != H.get_active_held_item())
+ return
+
+ var/obj/item/offhand = H.get_inactive_held_item()
+ if(!offhand)
+ return
+
+ var/attack_time = (user.next_move - world.time) * 0.5
+ addtimer(CALLBACK(src, PROC_REF(complement_attack), H, offhand, target), attack_time, TIMER_UNIQUE)
+
+/datum/augment/special/dualwield/proc/complement_attack(mob/living/carbon/human/H, obj/item/item, mob/target)
+ if(QDELETED(H) || QDELETED(target))
+ return
+
+ if(H.get_inactive_held_item() != item)
+ return
+
+ if(handle_side_effects(H, item, target))
+ return
+
+ if(H.CanReach(target, item))
+ UnregisterSignal(H, COMSIG_MOB_ITEM_ATTACK)
+ item.attack(target, H)
+ RegisterSignal(H, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_item_attack))
+
+/datum/augment/special/dualwield/proc/handle_side_effects(mob/living/carbon/human/H, obj/item/item, mob/target)
+ return FALSE
+
+/datum/augment/special/dualwield/refurbished
+ name = "refurbished C.C.M.S implant"
+ desc = "A refurbished dual wielding implant. The nerve filaments have degraded and may misfire or cause damage."
+ stability_cost = -10
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 20 SECONDS
+
+/datum/augment/special/dualwield/refurbished/handle_side_effects(mob/living/carbon/human/H, obj/item/item, mob/target)
+ if(prob(20))
+ H.visible_message(
+ span_warning("[H]'s arm twitches."),
+ span_danger("Your C.C.M.S misfires!")
+ )
+ return TRUE
+
+ if(prob(30))
+ H.visible_message(
+ span_warning("[H]'s arm spazzes out!"),
+ span_danger("Your arm spazzes out!")
+ )
+ var/obj/item/bodypart/arm = H.get_holding_bodypart_of_item(item)
+ arm?.receive_damage(brute = 10)
+
+ return FALSE
+
+/datum/action/augment
+ var/datum/augment/augment
+
+/datum/action/augment/sandevistan
+ name = "Sandevistan Activation"
+ desc = "Activate your sandevistan to slow time around you."
+ button_icon_state = "time_slow"
+
+/datum/action/augment/sandevistan/Trigger(trigger_flags)
+ if(!..())
+ return FALSE
+
+ var/datum/augment/special/sandevistan/sandy = augment
+ if(istype(sandy))
+ sandy.activate()
+ return TRUE
+
+/datum/augment/special/sandevistan
+ name = "Militech Apogee Sandevistan"
+ desc = "Experimental timeslowing implant. Activates a localized chrono-distortion field, slowing time for everything around you while you move at normal speed."
+ stability_cost = -25
+ engineering_difficulty = SKILL_LEVEL_MASTER
+ installation_time = 30 SECONDS
+ granted_actions = list(/datum/action/augment/sandevistan)
+
+ var/cooldown_time = 45 SECONDS
+ var/active_time = 15 SECONDS
+ var/active = FALSE
+ COOLDOWN_DECLARE(in_the_zone)
+
+/datum/augment/special/sandevistan/on_install(mob/living/carbon/human/H)
+ . = ..()
+ to_chat(H, span_notice("Neural interface established. Sandevistan ready."))
+
+/datum/augment/special/sandevistan/proc/activate()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ if(active)
+ to_chat(H, span_warning("The sandevistan is already active!"))
+ return
+
+ if(!COOLDOWN_FINISHED(src, in_the_zone))
+ to_chat(H, span_warning("The implant is recharging... ([DisplayTimeText(COOLDOWN_TIMELEFT(src, in_the_zone))] remaining)"))
+ return
+
+ COOLDOWN_START(src, in_the_zone, cooldown_time)
+ active = TRUE
+
+ H.AddComponent(/datum/component/after_image, 16, 0.5, TRUE)
+ H.AddComponent(/datum/component/slowing_field, 0.1, 5, 3)
+
+ to_chat(H, span_notice("Time seems to slow around you..."))
+
+ addtimer(CALLBACK(src, PROC_REF(deactivate)), active_time)
+
+/datum/augment/special/sandevistan/proc/deactivate()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ active = FALSE
+
+ var/datum/component/after_image/AI = H.GetComponent(/datum/component/after_image)
+ if(AI)
+ qdel(AI)
+
+ var/datum/component/slowing_field/SF = H.GetComponent(/datum/component/slowing_field)
+ if(SF)
+ qdel(SF)
+
+ to_chat(H, span_notice("Time returns to normal."))
+
+/datum/augment/special/sandevistan/get_examine_info()
+ var/list/info = ..()
+ info += span_info("Cooldown: [DisplayTimeText(cooldown_time)]")
+ info += span_info("Duration: [DisplayTimeText(active_time)]")
+ return info
+
+/datum/augment/special/sandevistan/refurbished
+ name = "refurbished sandevistan"
+ desc = "A hastily refurbished sandevistan. The chrono-field generator is unstable and may cause neural feedback."
+ stability_cost = -15
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ cooldown_time = 65 SECONDS
+
+/datum/augment/special/sandevistan/refurbished/activate()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ if(prob(45))
+ H.adjustOrganLoss(ORGAN_SLOT_BRAIN, 10)
+ to_chat(H, span_warning("Neural feedback! Your mind reels from the information overload!"))
+
+ return ..()
+
+/datum/augment/special/sandevistan/refurbished/deactivate()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ ..()
+
+ H.adjustBruteLoss(10)
+ to_chat(H, span_warning("Your body strains from the temporal stress, causing minor injuries."))
diff --git a/code/datums/components/augments/augment_datums/stats.dm b/code/datums/components/augments/augment_datums/stats.dm
new file mode 100644
index 00000000000..7b09230d86b
--- /dev/null
+++ b/code/datums/components/augments/augment_datums/stats.dm
@@ -0,0 +1,130 @@
+/datum/augment/stats
+ var/list/stat_changes = list() // List of stat changes: list(STATKEY_STR = 1, STATKEY_SPD = -1)
+
+/datum/augment/stats/on_install(mob/living/carbon/human/H)
+ for(var/stat in stat_changes)
+ H.change_stat(stat, stat_changes[stat])
+
+/datum/augment/stats/on_remove(mob/living/carbon/human/H)
+ for(var/stat in stat_changes)
+ H.change_stat(stat, -stat_changes[stat])
+
+/datum/augment/stats/get_examine_info()
+ var/list/info = list()
+ info += span_info("Stat changes:")
+ for(var/stat in stat_changes)
+ var/change = stat_changes[stat]
+ info += span_info(" [stat]: [change > 0 ? "+" : ""][change]")
+ return info
+
+/datum/augment/stats/strength_servo
+ name = "hydraulic strength servo"
+ desc = "Enhances physical power through pressurized hydraulics, at the cost of core stability."
+ stability_cost = -15
+ stat_changes = list(STATKEY_STR = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/stats/perception_lens
+ name = "enhanced optical array"
+ desc = "Improves visual acuity and target acquisition."
+ stability_cost = -10
+ stat_changes = list(STATKEY_PER = 2)
+ engineering_difficulty = SKILL_LEVEL_APPRENTICE
+ installation_time = 12 SECONDS
+
+/datum/augment/stats/processing_core
+ name = "overclocked logic engine"
+ desc = "Increases processing speed and analytical capability, straining the core matrix."
+ stability_cost = -20
+ stat_changes = list(STATKEY_INT = 3)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+/datum/augment/stats/reinforced_frame
+ name = "reinforced skeletal frame"
+ desc = "Strengthens the automaton's frame against damage."
+ stability_cost = -15
+ stat_changes = list(STATKEY_CON = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/stats/endurance_battery
+ name = "extended capacity battery"
+ desc = "Allows for longer operational periods without rest."
+ stability_cost = -10
+ stat_changes = list(STATKEY_END = 2)
+ engineering_difficulty = SKILL_LEVEL_APPRENTICE
+ installation_time = 12 SECONDS
+
+/datum/augment/stats/mobility_actuator
+ name = "high-efficiency actuators"
+ desc = "Improves movement speed through advanced mechanical joints."
+ stability_cost = -12
+ stat_changes = list(STATKEY_SPD = 2)
+ engineering_difficulty = SKILL_LEVEL_JOURNEYMAN
+ installation_time = 15 SECONDS
+
+/datum/augment/stats/power_limiter
+ name = "strength governor"
+ desc = "Limits power output to improve core stability."
+ stability_cost = 10
+ stat_changes = list(STATKEY_STR = -1)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 8 SECONDS
+
+/datum/augment/stats/sensor_dampener
+ name = "sensor dampening module"
+ desc = "Reduces sensor sensitivity to decrease processing load."
+ stability_cost = 8
+ stat_changes = list(STATKEY_PER = -1)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 8 SECONDS
+
+/datum/augment/stats/logic_limiter
+ name = "simplified logic circuit"
+ desc = "Reduces processing complexity for improved stability."
+ stability_cost = 15
+ stat_changes = list(STATKEY_INT = -2)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 10 SECONDS
+
+/datum/augment/stats/lightweight_frame
+ name = "lightweight chassis"
+ desc = "Reduces structural integrity for better energy efficiency."
+ stability_cost = 10
+ stat_changes = list(STATKEY_CON = -1)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 8 SECONDS
+
+/datum/augment/stats/efficiency_mode
+ name = "power conservation mode"
+ desc = "Reduces operational capacity to improve stability."
+ stability_cost = 8
+ stat_changes = list(STATKEY_END = -1)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 8 SECONDS
+
+/datum/augment/stats/servo_governor
+ name = "movement limiter"
+ desc = "Restricts movement speed to reduce mechanical stress."
+ stability_cost = 10
+ stat_changes = list(STATKEY_SPD = -1)
+ engineering_difficulty = SKILL_LEVEL_NOVICE
+ installation_time = 8 SECONDS
+
+/datum/augment/stats/balanced_matrix
+ name = "stabilized enhancement matrix"
+ desc = "A carefully balanced augmentation that improves multiple attributes."
+ stability_cost = -5
+ stat_changes = list(STATKEY_STR = 1, STATKEY_CON = 1)
+ engineering_difficulty = SKILL_LEVEL_EXPERT
+ installation_time = 20 SECONDS
+
+/datum/augment/stats/core_stabilizer
+ name = "core stabilization array"
+ desc = "Dramatically improves core stability without affecting performance."
+ stability_cost = 25
+ stat_changes = list()
+ engineering_difficulty = SKILL_LEVEL_MASTER
+ installation_time = 25 SECONDS
diff --git a/code/datums/components/augments/augmentable.dm b/code/datums/components/augments/augmentable.dm
new file mode 100644
index 00000000000..dd2927288e0
--- /dev/null
+++ b/code/datums/components/augments/augmentable.dm
@@ -0,0 +1,163 @@
+/datum/component/augmentable
+ var/max_stability = 100
+ var/current_stability = 100
+ var/min_stability = 0
+ var/list/installed_augments = list()
+ var/brute_mod_per_stability = 0.01 // 1% increased brute damage per point below max
+ var/limb_explosion_threshold = 20
+ var/limb_explosion_chance = 5
+
+/datum/component/augmentable/Initialize()
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ RegisterSignal(parent, COMSIG_AUGMENT_INSTALL, PROC_REF(install_augment))
+ RegisterSignal(parent, COMSIG_AUGMENT_REMOVE, PROC_REF(remove_augment))
+ RegisterSignal(parent, COMSIG_AUGMENT_REPAIR, PROC_REF(repair))
+ RegisterSignal(parent, COMSIG_AUGMENT_GET_STABILITY, PROC_REF(get_stability))
+
+ START_PROCESSING(SSobj, src)
+ ADD_TRAIT(parent, TRAIT_NO_EXPERIENCE, "[type]")
+
+/datum/component/augmentable/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ REMOVE_TRAIT(parent, TRAIT_NO_EXPERIENCE, "[type]")
+ return ..()
+
+/datum/component/augmentable/proc/get_brute_modifier()
+ var/stability_loss = max_stability - current_stability
+ return 1 + (stability_loss * brute_mod_per_stability)
+
+/datum/component/augmentable/proc/modify_stability(amount, mob/user)
+ current_stability = clamp(current_stability + amount, min_stability, max_stability)
+
+ var/mob/parent_mob = parent
+ if(user)
+ if(amount > 0)
+ parent_mob.say("CORE STABILITY INCREASED: [current_stability]%.", forced = TRUE)
+ else
+ parent_mob.say("CORE STABILITY DECREASED: [current_stability]%.", forced = TRUE)
+
+ update_stability_effects()
+
+/datum/component/augmentable/proc/update_stability_effects()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ if(H.dna?.species)
+ var/modifier = get_brute_modifier()
+ H.physiology.brute_mod = modifier
+
+/datum/component/augmentable/process()
+ if(current_stability <= limb_explosion_threshold)
+ check_catastrophic_failure()
+
+/datum/component/augmentable/proc/check_catastrophic_failure()
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ if(current_stability <= 0)
+ catastrophic_failure(H)
+ return
+
+ if(prob(limb_explosion_chance))
+ explode_random_limb(H)
+
+/datum/component/augmentable/proc/explode_random_limb(mob/living/carbon/human/H)
+ var/list/valid_limbs = list()
+ for(var/obj/item/bodypart/BP in H.bodyparts)
+ if(BP.body_zone != BODY_ZONE_CHEST && BP.body_zone != BODY_ZONE_HEAD)
+ valid_limbs += BP
+
+ if(!valid_limbs.len)
+ return
+
+ var/obj/item/bodypart/chosen = pick(valid_limbs)
+ H.visible_message(
+ span_danger("[H]'s [chosen] suddenly explodes in a shower of sparks and debris!"),
+ span_userdanger("Your [chosen] catastrophically fails and explodes!")
+ )
+
+ playsound(H, 'sound/misc/explode/explosion.ogg', 75, TRUE)
+ var/datum/effect_system/spark_spread/S = new()
+ S.set_up(3, 1, H.loc)
+ S.start()
+
+ chosen.dismember()
+ modify_stability(-10)
+
+/datum/component/augmentable/proc/catastrophic_failure(mob/living/carbon/human/H)
+ H.visible_message(
+ span_danger("[H]'s entire frame shudders violently before exploding into a catastrophic shower of metal and steam!"),
+ span_userdanger("CRITICAL FAILURE - SYSTEM MELTDOWN!")
+ )
+
+ playsound(H, 'sound/misc/explode/explosion.ogg', 100, TRUE)
+
+ for(var/obj/item/bodypart/BP in H.bodyparts)
+ if(BP.body_zone != BODY_ZONE_CHEST && BP.body_zone != BODY_ZONE_HEAD)
+ BP.dismember()
+
+ var/datum/effect_system/spark_spread/S = new()
+ S.set_up(5, 1, H.loc)
+ S.start()
+
+ H.adjustFireLoss(50)
+ H.Unconscious(100)
+
+/datum/component/augmentable/proc/get_stability()
+ return current_stability
+
+/datum/component/augmentable/proc/install_augment(datum/source, datum/augment/A, mob/user)
+ if(current_stability + A.stability_cost < min_stability)
+ to_chat(user, span_warning("Installing this augment would destabilize the core beyond safe limits!"))
+ return COMPONENT_AUGMENT_FAILED
+
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return COMPONENT_AUGMENT_FAILED
+
+ modify_stability(A.stability_cost, user)
+
+ installed_augments += A
+ A.parent = H
+ A.on_install(H)
+
+ to_chat(user, span_notice("Successfully installed [A.name]."))
+ return COMPONENT_AUGMENT_SUCCESS
+
+/datum/component/augmentable/proc/remove_augment(datum/source, datum/augment/A, mob/user)
+ if(!(A in installed_augments))
+ return COMPONENT_AUGMENT_FAILED
+
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return COMPONENT_AUGMENT_FAILED
+
+ modify_stability(-A.stability_cost, user)
+
+ installed_augments -= A
+ A.on_remove(H)
+
+ to_chat(user, span_notice("Removed [A.name]."))
+ return COMPONENT_AUGMENT_SUCCESS
+
+/datum/component/augmentable/proc/repair(datum/source, amount, mob/user)
+ var/mob/living/carbon/human/H = parent
+ if(!istype(H))
+ return
+
+ H.adjustBruteLoss(-amount)
+ H.adjustFireLoss(-amount/2)
+
+ modify_stability(amount/5, user)
+
+ H.visible_message(
+ span_notice("[user] repairs [H]'s damaged components."),
+ span_notice("[user] repairs your damaged components.")
+ )
+
+ return COMPONENT_AUGMENT_SUCCESS
diff --git a/code/datums/components/blood_stability.dm b/code/datums/components/blood_stability.dm
index 05246f27d39..7ad29ee58c1 100644
--- a/code/datums/components/blood_stability.dm
+++ b/code/datums/components/blood_stability.dm
@@ -29,7 +29,7 @@
if(!blood_storage[blood_type])
blood_storage[blood_type] = 0
blood_storage[blood_type] = min(max_storage_per_type, blood_storage[blood_type] + amount)
- to_chat(host, span_notice("Your body has absorbed [amount] units of [blood_type]. Total: [blood_storage[blood_type]]"))
+ to_chat(host, span_notice("Your body has absorbed [amount] units of [initial(blood_type.name)] Blood. Total: [blood_storage[blood_type]]"))
return TRUE
/datum/component/blood_stability/proc/has_blood_amount(blood_type, amount)
diff --git a/code/datums/components/chimeric_organ.dm b/code/datums/components/chimeric_organ.dm
index 96698a72817..d23c4c48989 100644
--- a/code/datums/components/chimeric_organ.dm
+++ b/code/datums/components/chimeric_organ.dm
@@ -32,6 +32,7 @@
///this is our failed %
var/failed_precent = 0
COOLDOWN_DECLARE(last_fail_message)
+ COOLDOWN_DECLARE(last_fail)
/datum/component/chimeric_organ/Initialize(maximum_tier_difference = 1)
. = ..()
@@ -123,7 +124,7 @@
var/datum/component/blood_stability/blood_stab = organ_owner.GetComponent(/datum/component/blood_stability)
if(!blood_stab)
- trigger_organ_failure("no blood stability component", 100)
+ trigger_organ_failure("no blood stability component", 100, TRUE)
return
// Check if we meet all blood requirements
@@ -147,7 +148,7 @@
/datum/component/chimeric_organ/proc/force_trigger()
var/datum/component/blood_stability/blood_stab = organ_owner.GetComponent(/datum/component/blood_stability)
if(!blood_stab)
- trigger_organ_failure("no blood stability component", 100)
+ trigger_organ_failure("no blood stability component", 100, TRUE)
return
if(!check_blood_requirements(blood_stab))
@@ -189,16 +190,19 @@
blood_requirements[blood_type] += cost
break
-/datum/component/chimeric_organ/proc/trigger_organ_failure(reason, amount)
+/datum/component/chimeric_organ/proc/trigger_organ_failure(reason, amount, bypass)
if(failed)
return
+ if(!COOLDOWN_FINISHED(src, last_fail) && !bypass)
+ return
+ COOLDOWN_START(src, last_fail, 15 SECONDS)
failed_precent += amount
if(failed_precent < 100)
if(COOLDOWN_FINISHED(src, last_fail_message))
to_chat(organ_owner, span_danger("Your [parent] is starting to fail because it [reason]!"))
organ_owner.emote("painscream")
COOLDOWN_START(src, last_fail_message, 30 SECONDS)
- organ_owner.adjustToxLoss(5)
+ organ_owner.adjustToxLoss(1)
return
failed = TRUE
diff --git a/code/datums/components/command_listener/_component.dm b/code/datums/components/command_listener/_component.dm
new file mode 100644
index 00000000000..813211cced9
--- /dev/null
+++ b/code/datums/components/command_listener/_component.dm
@@ -0,0 +1,167 @@
+/datum/component/command_follower
+ var/datum/follower_command/current_command
+ var/atom/movable/screen/command_display/hud_element
+ var/mob/living/carbon/human/owner
+ var/list/order_priority = list(
+ /datum/job/lord = 1,
+ /datum/job/consort = 2,
+ /datum/job/hand = 3,
+ /datum/job/prince = 4,
+ /datum/job/captain = 5,
+ /datum/job/steward = 6,
+ /datum/job/magician = 7,
+ /datum/job/archivist = 8,
+ /datum/job/courtphys = 9,
+ /datum/job/minor_noble = 10,
+ /datum/job/royalknight = 15,
+ /datum/job/veteran = 16,
+ /datum/job/lieutenant = 17,
+ /datum/job/town_elder = 18,
+ /datum/job/guardsman = 19,
+ /datum/job/gatemaster = 19,
+ /datum/job/jailor = 19,
+ /datum/job/dungeoneer = 19,
+ /datum/job/men_at_arms = 20,
+ /datum/job/forestwarden = 20,
+ /datum/job/forestguard = 20,
+ /datum/job/priest = 25,
+ /datum/job/inquisitor = 26,
+ /datum/job/templar = 27,
+ /datum/job/orthodoxist = 27,
+ /datum/job/absolver = 27,
+ /datum/job/monk = 28,
+ /datum/job/adept = 28
+ )
+
+ var/list/available_commands = list(
+ /datum/follower_command/custom,
+ /datum/follower_command/protect,
+ /datum/follower_command/kill,
+ /datum/follower_command/guard_position,
+ /datum/follower_command/follow,
+ )
+
+/datum/component/command_follower/Initialize(list/command_typepaths = list())
+ if(!ishuman(parent))
+ return COMPONENT_INCOMPATIBLE
+ owner = parent
+
+ for(var/command_path in command_typepaths)
+ var/datum/follower_command/cmd = new command_path()
+ available_commands[cmd.command_name] = command_path
+ qdel(cmd)
+
+ create_hud_element()
+ RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_login))
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted))
+ RegisterSignal(parent, COMSIG_PARENT_COMMAND_RECEIVED, PROC_REF(receive_command))
+ RegisterSignal(parent, COMSIG_CLICK_CTRL, PROC_REF(on_ctrl_click))
+
+/datum/component/command_follower/Destroy()
+ clear_command()
+ if(hud_element)
+ QDEL_NULL(hud_element)
+ available_commands = null
+ owner = null
+ return ..()
+
+/datum/component/command_follower/proc/create_hud_element()
+ hud_element = new()
+ hud_element.screen_loc = "WEST+1,NORTH-1:14"
+ if(owner?.client)
+ owner.client.screen += hud_element
+ update_hud()
+
+/datum/component/command_follower/proc/on_login(datum/source)
+ if(owner?.client && hud_element)
+ owner.client.screen += hud_element
+ update_hud()
+ if(current_command)
+ current_command.on_client_login(owner)
+
+/datum/component/command_follower/proc/on_parent_deleted(datum/source)
+ clear_command()
+
+/datum/component/command_follower/proc/update_hud()
+ if(!hud_element)
+ return
+ if(!current_command)
+ hud_element.maptext = ""
+ hud_element.alpha = 0
+ return
+ hud_element.alpha = 255
+ var/command_text = ""
+ command_text += "Command: [current_command.command_name]
"
+ command_text += "Issuer: [current_command.issuer_name]"
+ command_text += ""
+ hud_element.maptext = command_text
+ hud_element.maptext_width = 128
+ hud_element.maptext_height = 32
+
+/datum/component/command_follower/proc/receive_command(datum/source, datum/follower_command/new_command, mob/living/carbon/human/issuer)
+ if(!new_command || !issuer)
+ return FALSE
+ if(current_command)
+ var/current_priority = get_job_priority(current_command.issuer_job)
+ var/new_priority = get_job_priority(issuer.job_type)
+ if(new_priority > current_priority)
+ owner.say("Command rejected: [issuer] lacks authority to override [current_command.issuer_name]'s command.", forced = TRUE)
+ clear_command()
+ current_command = new_command
+ current_command.issuer_name = issuer.real_name
+ current_command.issuer_job = issuer.job_type
+ current_command.component = src
+ current_command.execute(owner, issuer)
+ update_hud()
+ return TRUE
+
+/datum/component/command_follower/proc/clear_command()
+ if(current_command)
+ current_command.terminate(owner)
+ QDEL_NULL(current_command)
+ update_hud()
+
+/datum/component/command_follower/proc/get_job_priority(job_type)
+ if(!job_type)
+ return 999
+ if(job_type in order_priority)
+ return order_priority[job_type]
+ return 999
+
+/datum/component/command_follower/proc/on_ctrl_click(datum/source, mob/living/clicker)
+ SIGNAL_HANDLER
+
+ if(!length(available_commands))
+ return
+ if(!clicker.client)
+ return
+
+ // Check if clicker has authority
+ var/clicker_priority = get_job_priority(clicker.job_type)
+ if(clicker_priority >= 999)
+ to_chat(clicker, span_warning("You lack the authority to issue commands."))
+ return
+
+ INVOKE_ASYNC(src, PROC_REF(show_command_menu), clicker)
+
+/datum/component/command_follower/proc/show_command_menu(mob/living/clicker)
+ var/list/choices = list()
+ for(var/datum/follower_command/cmd_name as anything in available_commands)
+ choices += initial(cmd_name.command_name)
+ choices[initial(cmd_name.command_name)] = cmd_name
+
+ var/choice = browser_input_list(clicker, "Select a command to issue to [owner]:", "Issue Command", choices)
+ if(!choice)
+ return
+
+ var/command_path = choices[choice]
+ var/datum/follower_command/new_cmd = new command_path()
+
+ if(!new_cmd.post_setup(owner, clicker))
+ qdel(new_cmd)
+ return
+
+ SEND_SIGNAL(owner, COMSIG_PARENT_COMMAND_RECEIVED, new_cmd, clicker)
+
+/atom/movable/screen/command_display
+ name = "Command Display"
diff --git a/code/datums/components/command_listener/commands/_base.dm b/code/datums/components/command_listener/commands/_base.dm
new file mode 100644
index 00000000000..5afc3ec8bc3
--- /dev/null
+++ b/code/datums/components/command_listener/commands/_base.dm
@@ -0,0 +1,17 @@
+/datum/follower_command
+ var/command_name = "Unknown Command"
+ var/issuer_name = "Unknown"
+ var/issuer_job = null
+ var/datum/component/command_follower/component
+
+/datum/follower_command/proc/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ return
+
+/datum/follower_command/proc/terminate(mob/living/carbon/human/automaton)
+ return
+
+/datum/follower_command/proc/post_setup(mob/living/carbon/human/automaton, mob/living/issuer)
+ return TRUE
+
+/datum/follower_command/proc/on_client_login(mob/living/carbon/human/automaton)
+ return
diff --git a/code/datums/components/command_listener/commands/custom.dm b/code/datums/components/command_listener/commands/custom.dm
new file mode 100644
index 00000000000..c82f3e307e4
--- /dev/null
+++ b/code/datums/components/command_listener/commands/custom.dm
@@ -0,0 +1,16 @@
+/datum/follower_command/custom
+ command_name = "Custom Order"
+ var/custom_text = ""
+
+/datum/follower_command/custom/post_setup(mob/living/carbon/human/automaton, mob/living/issuer)
+ custom_text = browser_input_text(issuer, "Enter your command for [automaton]:", "Custom Command", max_length = 256)
+ if(!custom_text)
+ return FALSE
+ command_name = custom_text
+ return TRUE
+
+/datum/follower_command/custom/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ automaton.say("Executing Order: [custom_text]", forced = TRUE)
+
+/datum/follower_command/custom/terminate(mob/living/carbon/human/automaton)
+ return
diff --git a/code/datums/components/command_listener/commands/follow.dm b/code/datums/components/command_listener/commands/follow.dm
new file mode 100644
index 00000000000..c076698b5d8
--- /dev/null
+++ b/code/datums/components/command_listener/commands/follow.dm
@@ -0,0 +1,28 @@
+/datum/follower_command/follow
+ command_name = "Follow Target"
+ var/atom/target
+ var/datum/component/bounded/bound_component
+
+/datum/follower_command/follow/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ target = issuer
+ if(!target || !automaton)
+ return
+
+ bound_component = automaton.AddComponent(/datum/component/bounded, \
+ target, \
+ 0, \
+ 2, \
+ null, \
+ CALLBACK(src, PROC_REF(on_target_destroyed)), \
+ FALSE, \
+ FALSE)
+
+ automaton.say("EXECUTING: Following [target]", forced = TRUE)
+
+/datum/follower_command/follow/terminate(mob/living/carbon/human/automaton)
+ if(bound_component)
+ QDEL_NULL(bound_component)
+
+/datum/follower_command/follow/proc/on_target_destroyed()
+ if(component)
+ component.clear_command()
diff --git a/code/datums/components/command_listener/commands/guard.dm b/code/datums/components/command_listener/commands/guard.dm
new file mode 100644
index 00000000000..5f67fe4afbc
--- /dev/null
+++ b/code/datums/components/command_listener/commands/guard.dm
@@ -0,0 +1,24 @@
+/datum/follower_command/guard_position
+ command_name = "Guard Position"
+ var/turf/guard_location
+ var/datum/component/bounded/bound_component
+
+/datum/follower_command/guard_position/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ guard_location = get_turf(automaton)
+ if(!guard_location || !automaton)
+ return
+
+ bound_component = automaton.AddComponent(/datum/component/bounded, \
+ guard_location, \
+ 0, \
+ 3, \
+ null, \
+ null, \
+ FALSE, \
+ FALSE)
+
+ automaton.say("EXECUTING: Guarding position.", forced = TRUE)
+
+/datum/follower_command/guard_position/terminate(mob/living/carbon/human/automaton)
+ if(bound_component)
+ QDEL_NULL(bound_component)
diff --git a/code/datums/components/command_listener/commands/kill.dm b/code/datums/components/command_listener/commands/kill.dm
new file mode 100644
index 00000000000..9e29453db1c
--- /dev/null
+++ b/code/datums/components/command_listener/commands/kill.dm
@@ -0,0 +1,60 @@
+/datum/follower_command/kill
+ command_name = "Kill"
+ var/target_name = ""
+ var/list/target_images = list()
+ var/datum/callback/update_timer
+
+/datum/follower_command/kill/post_setup(mob/living/carbon/human/automaton, mob/living/issuer)
+ target_name = browser_input_text(issuer, "Enter the name of the target to eliminate:", "Kill Target", max_length = MAX_NAME_LEN)
+ if(!target_name)
+ return FALSE
+ return TRUE
+
+/datum/follower_command/kill/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ update_overlays(automaton)
+ update_timer = addtimer(CALLBACK(src, PROC_REF(update_overlays), automaton), 5 SECONDS, TIMER_STOPPABLE | TIMER_LOOP)
+
+/datum/follower_command/kill/terminate(mob/living/carbon/human/automaton)
+ if(update_timer)
+ deltimer(update_timer)
+ clear_overlays(automaton)
+
+/datum/follower_command/kill/on_client_login(mob/living/carbon/human/automaton)
+ update_overlays(automaton)
+
+/datum/follower_command/kill/proc/update_overlays(mob/living/carbon/human/automaton)
+ if(!automaton?.client)
+ return
+
+ clear_overlays(automaton)
+
+ for(var/mob/living/target in GLOB.player_list)
+ if(!target.client)
+ continue
+ if(!findtext(target.real_name, target_name) && !findtext(target.name, target_name))
+ continue
+
+ var/image/overlay = image('icons/effects/effects.dmi', target, "scanline")
+ overlay.color = "#FF0000"
+ overlay.alpha = 120
+ overlay.blend_mode = BLEND_INSET_OVERLAY
+ overlay.appearance_flags = RESET_COLOR | RESET_ALPHA | KEEP_TOGETHER
+ overlay.layer = ABOVE_MOB_LAYER
+ overlay.plane = GAME_PLANE_FOV_HIDDEN
+
+ automaton.client.images += overlay
+ target_images += overlay
+
+/datum/follower_command/kill/proc/clear_overlays(mob/living/carbon/human/automaton)
+ if(!automaton?.client)
+ return
+
+ for(var/image/img in target_images)
+ automaton.client.images -= img
+ target_images.Cut()
+
+/datum/follower_command/kill/Destroy()
+ if(update_timer)
+ deltimer(update_timer)
+ target_images = null
+ return ..()
diff --git a/code/datums/components/command_listener/commands/protect.dm b/code/datums/components/command_listener/commands/protect.dm
new file mode 100644
index 00000000000..28bf9da29fb
--- /dev/null
+++ b/code/datums/components/command_listener/commands/protect.dm
@@ -0,0 +1,60 @@
+/datum/follower_command/protect
+ command_name = "Protect"
+ var/target_name = ""
+ var/list/target_images = list()
+ var/datum/callback/update_timer
+
+/datum/follower_command/protect/post_setup(mob/living/carbon/human/automaton, mob/living/issuer)
+ target_name = browser_input_text(issuer, "Enter the name of the person to protect:", "Protect Target", max_length = MAX_NAME_LEN)
+ if(!target_name)
+ return FALSE
+ return TRUE
+
+/datum/follower_command/protect/execute(mob/living/carbon/human/automaton, mob/living/issuer)
+ update_overlays(automaton)
+ update_timer = addtimer(CALLBACK(src, PROC_REF(update_overlays), automaton), 5 SECONDS, TIMER_STOPPABLE | TIMER_LOOP)
+
+/datum/follower_command/protect/terminate(mob/living/carbon/human/automaton)
+ if(update_timer)
+ deltimer(update_timer)
+ clear_overlays(automaton)
+
+/datum/follower_command/protect/on_client_login(mob/living/carbon/human/automaton)
+ update_overlays(automaton)
+
+/datum/follower_command/protect/proc/update_overlays(mob/living/carbon/human/automaton)
+ if(!automaton?.client)
+ return
+
+ clear_overlays(automaton)
+
+ for(var/mob/living/target in GLOB.player_list)
+ if(!target.client)
+ continue
+ if(!findtext(target.real_name, target_name) && !findtext(target.name, target_name))
+ continue
+
+ var/image/overlay = image('icons/effects/effects.dmi', target, "scanline")
+ overlay.color = "#00FF00"
+ overlay.alpha = 120
+ overlay.blend_mode = BLEND_INSET_OVERLAY
+ overlay.appearance_flags = RESET_COLOR | RESET_ALPHA | KEEP_TOGETHER
+ overlay.layer = ABOVE_MOB_LAYER
+ overlay.plane = GAME_PLANE_FOV_HIDDEN
+
+ automaton.client.images += overlay
+ target_images += overlay
+
+/datum/follower_command/protect/proc/clear_overlays(mob/living/carbon/human/automaton)
+ if(!automaton?.client)
+ return
+
+ for(var/image/img in target_images)
+ automaton.client.images -= img
+ target_images.Cut()
+
+/datum/follower_command/protect/Destroy()
+ if(update_timer)
+ deltimer(update_timer)
+ target_images = null
+ return ..()
diff --git a/code/datums/components/container_craft/recipes/cooking_pot/_cooking_pot.dm b/code/datums/components/container_craft/recipes/cooking_pot/_cooking_pot.dm
index 8667f69c0a3..9c1e5dd2a19 100644
--- a/code/datums/components/container_craft/recipes/cooking_pot/_cooking_pot.dm
+++ b/code/datums/components/container_craft/recipes/cooking_pot/_cooking_pot.dm
@@ -134,6 +134,7 @@
// Update reagent name with optional ingredients
if(length(found_optional_wildcards))
var/extra_string = " with [wording_choice] "
+ var/extra_taste = "with hints of "
var/first_ingredient = TRUE
var/list/all_used_ingredients = list()
for(var/wildcard_type in found_optional_wildcards)
@@ -143,10 +144,15 @@
for(var/obj/item/ingredient in all_used_ingredients)
if(first_ingredient)
extra_string += ingredient.name
+ extra_taste += ingredient.name
first_ingredient = FALSE
else
extra_string += " and [ingredient.name]"
+ extra_taste += " and [ingredient.name]"
found_product.name += extra_string
+ found_product.taste_description += extra_taste
+ found_product.add_data("custom_name", found_product.name)
+ found_product.add_data("custom_tastes", found_product.taste_description)
// Optionally modify reagent properties based on quality
apply_quality_effects_to_reagent(found_product)
diff --git a/code/datums/components/slowing_field.dm b/code/datums/components/slowing_field.dm
new file mode 100644
index 00000000000..067f662d4fa
--- /dev/null
+++ b/code/datums/components/slowing_field.dm
@@ -0,0 +1,81 @@
+/datum/component/slowing_field
+ ///how slow bullets move as a multiplier (rounded to the nearest 1)
+ var/bullet_speed_multiplier = 1
+ ///our movespeed_multipliers
+ var/atom_speed_multiplier = 1
+ ///list of slowed things
+ var/list/affected = list()
+ ///our area range
+ var/area_range = 1
+
+/datum/component/slowing_field/Initialize(bullet_speed_multiplier = 1, atom_speed_multiplier = 1, area_range = 1)
+ . = ..()
+ src.bullet_speed_multiplier = bullet_speed_multiplier
+ src.atom_speed_multiplier = atom_speed_multiplier
+ src.area_range = area_range
+
+ var/static/list/connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered_turf),
+ COMSIG_ATOM_EXITED = PROC_REF(on_exited_turf),
+ COMSIG_ATOM_INITIALIZED_ON = PROC_REF(on_entered_turf),
+ )
+
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_parent_moved))
+ AddComponent(/datum/component/connect_range, parent, connections, area_range, TRUE)
+ on_parent_moved()
+
+/datum/component/slowing_field/Destroy(force)
+ . = ..()
+ for(var/atom/a as anything in affected)
+ on_exited_turf(src, a)
+
+/datum/component/slowing_field/proc/on_parent_moved(datum/source)
+ SIGNAL_HANDLER
+
+ var/list/remaining = list()
+ remaining += affected
+ for(var/atom/movable/thing in range(area_range, parent))
+ if(!isliving(thing) && !isprojectile(thing))
+ continue
+ if(thing in remaining)
+ remaining -= thing
+ continue
+ on_entered_turf(get_turf(thing), thing)
+
+ for(var/atom/movable/thing as anything in remaining)
+ if(!istype(thing))
+ continue
+ on_exited_turf(get_turf(thing), thing)
+
+/datum/component/slowing_field/proc/on_entered_turf(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+ if(istype(arrived, /obj/effect/temp_visual/decoy/fading))
+ return
+ if(arrived == parent)
+ return
+
+ arrived.add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY)
+ affected |= arrived
+
+ if(isprojectile(arrived))
+ var/obj/projectile/arrived_proj = arrived
+ arrived_proj.speed *= bullet_speed_multiplier
+ else if(isliving(arrived))
+ var/mob/living/arrived_movable = arrived
+ arrived_movable.add_movespeed_modifier("slowing_field", multiplicative_slowdown = atom_speed_multiplier)
+
+/datum/component/slowing_field/proc/on_exited_turf(datum/source, atom/movable/gone, direction)
+ SIGNAL_HANDLER
+ if(istype(gone, /obj/effect/temp_visual/decoy/fading))
+ return
+ if(gone == parent)
+ return
+ gone.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY)
+ affected -= gone
+
+ if(isprojectile(gone))
+ var/obj/projectile/arrived_proj = gone
+ arrived_proj.speed /= bullet_speed_multiplier
+ else if(isliving(gone))
+ var/mob/living/arrived_movable = gone
+ arrived_movable.remove_movespeed_modifier("slowing_field")
diff --git a/code/datums/components/steam_life.dm b/code/datums/components/steam_life.dm
new file mode 100644
index 00000000000..5156d77f371
--- /dev/null
+++ b/code/datums/components/steam_life.dm
@@ -0,0 +1,95 @@
+/datum/component/steam_life
+ var/steam_charge = 100
+ var/max_steam_charge = 100
+ ///this to be easier to understand is how much should be drained per minute
+ var/steam_drain_rate = 4
+ var/mob/living/carbon/human/host
+ var/tmp/needs_particles = TRUE
+
+/datum/component/steam_life/Initialize()
+ if(!ishuman(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ host = parent
+ host?.hud_used?.initialize_bloodpool()
+ host?.hud_used?.bloodpool.set_fill_color("#dcdddb")
+ host.hud_used?.bloodpool?.name = "Steam"
+ host.hud_used?.bloodpool?.desc = "Charge: [steam_charge]/[max_steam_charge]"
+
+ RegisterSignal(host, COMSIG_ATOM_STEAM_INCREASE, PROC_REF(recharge_steam))
+ RegisterSignal(host, COMSIG_MOB_FOOD_EAT, PROC_REF(try_consume_fuel))
+ START_PROCESSING(SSobj, src)
+
+/datum/component/steam_life/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/datum/component/steam_life/process()
+ if(!host)
+ return
+ steam_charge = max(0, steam_charge - (steam_drain_rate / 60))
+
+ if(steam_charge <= 0)
+ host.Unconscious(20)
+ if(prob(10))
+ host.emote("shutdown", forced = TRUE)
+ to_chat(host, span_danger("CRITICAL POWER FAILURE - ENTERING DORMANT MODE"))
+ else if(steam_charge <= 20)
+ if(prob(5))
+ host.emote("malfunction", forced = TRUE)
+ to_chat(host, span_warning("WARNING: POWER RESERVES CRITICALLY LOW"))
+ update_steam()
+
+/datum/component/steam_life/proc/update_steam()
+ if(host.hud_used && !host.hud_used?.bloodpool)
+ host?.hud_used?.initialize_bloodpool()
+ host.hud_used?.bloodpool?.name = "Steam"
+ host.hud_used?.bloodpool?.desc = "Charge: [steam_charge]/[max_steam_charge]"
+ if(steam_charge <= 0)
+ host.remove_shared_particles("steam")
+ needs_particles = TRUE
+ host?.hud_used?.bloodpool?.set_value(0, 1 SECONDS)
+ else
+ if(needs_particles)
+ host.add_shared_particles(/particles/smoke/cig/big/steam, "steam")
+ needs_particles = FALSE
+ host?.hud_used?.bloodpool?.set_value((100 / (max_steam_charge / steam_charge)) / 100, 1 SECONDS)
+
+/datum/component/steam_life/proc/recharge_steam(datum/source, steam_amount)
+ SIGNAL_HANDLER
+
+ steam_charge = min(max_steam_charge, steam_charge + steam_amount)
+ to_chat(host, span_notice("STEAM RESERVES REPLENISHED: [steam_charge]/[max_steam_charge]"))
+
+ if(steam_charge >= max_steam_charge)
+ return FALSE
+
+ return TRUE
+
+/datum/component/steam_life/proc/try_consume_fuel(mob/living/user, obj/item/source)
+ SIGNAL_HANDLER
+
+ if(user.cmode)
+ return FALSE
+
+ if(!istype(source, /obj/item/ore/coal) && !istype(source, /obj/item/grown/log/tree))
+ return FALSE
+
+ var/fuel_amount = 0
+ if(istype(source, /obj/item/ore/coal))
+ fuel_amount = 30
+ user.visible_message(
+ span_notice("[user] feeds the coal into their furnace with a hiss of steam."),
+ span_notice("I consume the coal for fuel. Power: [steam_charge]/[max_steam_charge]")
+ )
+ else if(istype(source, /obj/item/grown/log/tree))
+ fuel_amount = 15
+ user.visible_message(
+ span_notice("[user] stuffs the wood into their furnace, flames licking at the bark."),
+ span_notice("I consume the wood for fuel. Power: [steam_charge]/[max_steam_charge]")
+ )
+
+ steam_charge = min(max_steam_charge, steam_charge + fuel_amount)
+
+ playsound(user, 'sound/items/firelight.ogg', 50, TRUE)
+ return TRUE
diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm
index 709f855026c..a44108f16ab 100644
--- a/code/datums/elements/footstep.dm
+++ b/code/datums/elements/footstep.dm
@@ -38,6 +38,8 @@
footstep_sounds = GLOB.heavyfootstep
if(FOOTSTEP_MOB_SHOE)
footstep_sounds = GLOB.footstep
+ if(FOOTSTEP_MOB_METAL)
+ footstep_sounds = GLOB.metalfootstep
if(FOOTSTEP_MOB_SLIME)
footstep_sounds = 'sound/blank.ogg'
RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(play_simplestep))
@@ -77,7 +79,7 @@
if(steps % 2)
return
- . = list(FOOTSTEP_MOB_SHOE = turf.footstep, FOOTSTEP_MOB_BAREFOOT = turf.barefootstep, FOOTSTEP_MOB_HEAVY = turf.heavyfootstep, FOOTSTEP_MOB_CLAW = turf.clawfootstep, STEP_SOUND_PRIORITY = STEP_SOUND_NO_PRIORITY)
+ . = list(FOOTSTEP_MOB_SHOE = turf.footstep, FOOTSTEP_MOB_BAREFOOT = turf.barefootstep, FOOTSTEP_MOB_HEAVY = turf.heavyfootstep, FOOTSTEP_MOB_METAL = turf.heavyfootstep, FOOTSTEP_MOB_CLAW = turf.clawfootstep, STEP_SOUND_PRIORITY = STEP_SOUND_NO_PRIORITY)
SEND_SIGNAL(source, COMSIG_MOB_PREPARE_STEP_SOUND, .) // Used to override shoe material before turf
SEND_SIGNAL(turf, COMSIG_TURF_PREPARE_STEP_SOUND, .)
return .
diff --git a/code/datums/elements/mob_overlay_effect.dm b/code/datums/elements/mob_overlay_effect.dm
index 3a5c7160a83..b47721e0ebe 100644
--- a/code/datums/elements/mob_overlay_effect.dm
+++ b/code/datums/elements/mob_overlay_effect.dm
@@ -57,6 +57,9 @@
if(S.obj_flags & BLOCK_Z_OUT_DOWN)
return
+ if(isitem(target))
+ return ///this is ALOT of filters
+
if(isobj(target))
var/obj/obj = target
if(obj.obj_flags & IGNORE_SINK)
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 21cbdd79420..93e365a2a0a 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -795,6 +795,8 @@ GLOBAL_LIST_EMPTY(personal_objective_minds)
** check_apprentice - do apprentices receive skill experience too?
*/
/datum/mind/proc/add_sleep_experience(skill, amt, silent = FALSE, check_apprentice = TRUE)
+ if(HAS_TRAIT(current, TRAIT_NO_EXPERIENCE))
+ return FALSE
amt *= GLOB.sleep_experience_modifier
if(current.has_quirk(/datum/quirk/boon/quick_learner))
diff --git a/code/datums/nation/_base.dm b/code/datums/nation/_base.dm
index e150af4650f..69f38aee91c 100644
--- a/code/datums/nation/_base.dm
+++ b/code/datums/nation/_base.dm
@@ -6,6 +6,19 @@
var/nation_rep = 0
///this is the cost in mammons of how much it costs to buy into the nation
var/national_currency_cost = 1
+ ///list of breakpoints to names for nation rep
+ var/alist/reputation_breakpoints = list(
+ "Neutral" = 0,
+ "Friendly" = 50,
+ "Trusted" = 200,
+ "Honored" = 300,
+ "Revered" = 600,
+ "Exalted" = 800,
+ "Legendary" = 1000,
+ )
+
+ ///how many agreements we've finished and can recycle into new agreements
+ var/finished_agreements = 0
///this is a list of all of our nodes starts as paths on init it will bloom into basically singletons
var/list/nodes = list()
@@ -13,6 +26,29 @@
var/list/completed_trades
///this is a lazyman accessor for what we can currently work on trade wise
var/list/lazyman
+ ///this is the reward cache from doing other things with the nation
+ var/list/cached_rewards = list()
+
+ ///list of all possible trade requests we can have
+ var/list/possible_trade_requests = list()
+ ///list of the chosen agreements
+ var/list/active_agreements
+ ///everypony viewing the nation
+ var/list/ui_users = list()
+
+ /// Weighted preferences for trader types - higher numbers = more likely
+ var/list/weighted_trader_data = list(
+ /datum/trader_data/food_merchant = 10,
+ /datum/trader_data/clothing_merchant = 10,
+ /datum/trader_data/tool_merchant = 10,
+ /datum/trader_data/luxury_merchant = 10,
+ /datum/trader_data/alchemist = 10,
+ /datum/trader_data/material_merchant = 10
+ )
+
+ var/list/trader_outfits = list(
+ /obj/effect/mob_spawn/human/rakshari/trader
+ )
/datum/nation/New()
. = ..()
@@ -23,22 +59,62 @@
nodes = actual_nodes
populate_lazyman()
+ setup_possible_cache()
+ setup_agreements(rand(3, length(possible_trade_requests) * 0.25))
/datum/nation/Destroy(force, ...)
. = ..()
QDEL_LIST(nodes)
lazyman = null
+/datum/nation/proc/get_nation_tier()
+ var/last_tier = "Neutral"
+ for(var/tier in reputation_breakpoints)
+ if(nation_rep < reputation_breakpoints[tier])
+ return last_tier
+ last_tier = tier
+
+/datum/nation/proc/progress_nation()
+ populate_lazyman() //this is purely for offcases that an admin fucks with something
+ if(finished_agreements > 0)
+ var/new_requests = rand(1, finished_agreements)
+ finished_agreements -= new_requests
+ setup_agreements(new_requests)
+
+ var/current_slots_used = length(active_agreements) + finished_agreements
+ var/max_slots = get_max_agreements()
+ if(prob(30) && (current_slots_used < max_slots))
+ setup_agreements(1)
+
+/datum/nation/proc/get_max_agreements()
+ var/base_cap = 3
+ var/tier = get_nation_tier()
+ switch(reputation_breakpoints[tier])
+ if(1)
+ base_cap += 1
+ if(2 to 3)
+ base_cap += 2
+ if(4 to 5)
+ base_cap += 4
+ if(6)
+ base_cap += 6
+ return base_cap
+
/datum/nation/proc/populate_lazyman()
for(var/datum/trade/node in nodes)
if(!can_work_on(node))
continue
- LAZYADD(lazyman, node)
+ LAZYOR(lazyman, node)
+
+/datum/nation/proc/setup_possible_cache()
+ for(var/datum/trade_agreement/agreement_path as anything in possible_trade_requests)
+ possible_trade_requests[agreement_path] = initial(agreement_path.weight)
/datum/nation/proc/complete_trade(datum/trade/node)
LAZYADD(completed_trades, node.type)
LAZYREMOVE(lazyman, node)
SSmerchant.unlock_supply_packs(node.supply_packs)
+ populate_lazyman()
/datum/nation/proc/can_work_on(datum/trade/node)
for(var/requirement as anything in node.required_trades)
@@ -46,9 +122,72 @@
return FALSE
return TRUE
-/datum/nation/proc/handle_import_shipment(list/items)
+/datum/nation/proc/handle_import_shipment(list/items, obj/structure/industrial_lift/tram/platform)
+ for(var/datum/trade_agreement/agreement in active_agreements)
+ if(!agreement.active)
+ continue
+ items = agreement.process_shipment(items, platform)
+ if(agreement.amount_requested <= 0)
+ qdel(agreement)
+
for(var/datum/trade/node in lazyman)
var/valid_items = node.return_valid_count(items)
if(!valid_items)
continue
node.progress_trade(valid_items)
+
+/datum/nation/proc/handle_global_shipment(list/items)
+ for(var/datum/trade/node in lazyman)
+ var/valid_items = node.return_valid_count(items, TRUE)
+ if(!valid_items)
+ continue
+ node.progress_trade(valid_items)
+
+
+/datum/nation/proc/setup_agreements(amount)
+ for(var/i = 1 to amount)
+ var/picked_path = pickweight(possible_trade_requests)
+ var/datum/trade_agreement/new_agreement = new picked_path(src)
+ possible_trade_requests[picked_path] = max(new_agreement.minimum_weight, possible_trade_requests[picked_path] - 10)
+ LAZYADD(active_agreements, new_agreement)
+
+/datum/nation/proc/activate_agreement(datum/trade_agreement/agreement)
+ if(agreement.active)
+ return FALSE
+ agreement.activate_agreement()
+ return TRUE
+
+/datum/nation/proc/spawn_traders(datum/lift_master/lift)
+ var/max_traders = rand(1, min(4, length(weighted_trader_data)))
+ var/list/available_types = weighted_trader_data.Copy()
+
+ var/list/lifts = lift.lift_platforms.Copy()
+
+ var/turf/spawn_location = null
+ for(var/i = 1 to max_traders)
+ while(spawn_location == null)
+ if(!length(lifts))
+ lifts = lift.lift_platforms.Copy()
+ var/obj/structure/industrial_lift/tram/picked_lift = pick_n_take(lifts)
+ if(picked_lift.fake)
+ continue
+ spawn_location = get_turf(picked_lift)
+
+ if(!length(available_types))
+ available_types = weighted_trader_data.Copy()
+ var/trader_type = pickweight(available_types)
+ available_types -= trader_type
+ var/datum/trader_data/trader_data = new trader_type()
+ customize_trader_stock(trader_data)
+
+ var/picked_outfit = pick(trader_outfits)
+ if(length(trader_data.outfit_override))
+ picked_outfit = pick(trader_data.outfit_override)
+
+ var/mob/living/simple_animal/hostile/retaliate/trader/faction_trader/new_trader = new(spawn_location, TRUE, picked_outfit, WEAKREF(src))
+ new_trader.set_custom_trade(trader_data)
+ new_trader.faction_ref = WEAKREF(src)
+ spawn_location = null
+
+/datum/nation/proc/customize_trader_stock(datum/trader_data/new_data)
+ return
diff --git a/code/datums/nation/_base_contract.dm b/code/datums/nation/_base_contract.dm
new file mode 100644
index 00000000000..5a4c8af5f1b
--- /dev/null
+++ b/code/datums/nation/_base_contract.dm
@@ -0,0 +1,118 @@
+/datum/trade_agreement
+ var/name = "Generic Contract"
+ var/desc = "Generic lore about why we need this"
+
+ ///if we are activated
+ var/active = FALSE
+
+ ///this is our base contract weight
+ var/weight = 100
+ ///this is the minimum weight we allow
+ var/minimum_weight = 10
+
+ ///if this is set its a timed agreement think famine they NEED food
+ var/time = 0
+ var/fail_time
+
+ ///min max for the amount of items we need
+ var/min_requested = 3
+ var/max_requested = 5
+ var/amount_requested = 0
+
+ ///the mammons for completing this
+ var/mammon_reward = 10
+ ///items if any we give for completing this
+ var/list/reward_items
+ ///list of all the items we can choose from when setting this agreement up (we can add to this in new if you want to do typesof or something)
+ var/list/possible_items
+ ///how many of the possible items we want to pick
+ var/picked_item_count = 0
+ ///the items we actually want for the agreement (this uses a typecheck so children are accepted)
+ var/list/required_items
+ ///the nation we need to send this to
+ var/datum/nation/location
+
+/datum/trade_agreement/New(datum/nation/created_nation)
+ . = ..()
+ amount_requested = rand(min_requested, max_requested)
+ location = created_nation
+ expand_possible_items()
+ select_items()
+
+/datum/trade_agreement/Destroy(force, ...)
+ . = ..()
+ location.active_agreements -= src
+ location.finished_agreements++
+ location = null
+
+/datum/trade_agreement/proc/expand_possible_items()
+ return
+
+/datum/trade_agreement/proc/select_items()
+ if(!length(possible_items))
+ return
+ var/list/takers = possible_items.Copy()
+ for(var/i = 1 to picked_item_count)
+ if(!length(takers))
+ return
+ LAZYADD(required_items, pick_n_take(takers))
+
+/datum/trade_agreement/proc/activate_agreement()
+ active = TRUE
+ if(time)
+ fail_time = world.time + time
+
+///we do a full process here since we don't get paid for agreement items
+/datum/trade_agreement/proc/process_shipment(list/items, obj/structure/industrial_lift/tram/platform)
+ for(var/atom/item in items)
+ if(!(is_type_in_list(item, required_items)))
+ continue
+ amount_requested--
+ items -= item
+ qdel(item)
+ platform.lift_load -= item
+ if(amount_requested <= 0)
+ handle_rewards()
+ break
+ return items
+
+///here we create the items we want and throw them into the nations cached items so when the ship next leaves it spawns those
+/datum/trade_agreement/proc/handle_rewards()
+ spawn_coins() //these are created in nullspace because its frankly easier
+ if(!length(reward_items))
+ return
+ location.cached_rewards |= reward_items
+
+/datum/trade_agreement/proc/spawn_coins()
+ if(mammon_reward <= 0)
+ return
+
+ var/gold_coins = floor(mammon_reward/10)
+ if(gold_coins >= 1)
+ var/stacks = floor(gold_coins/20) // keep this in sync with MAX_COIN_STACK_SIZE in coins.dm
+ if(stacks >= 1)
+ for(var/i in 1 to stacks)
+ location.cached_rewards += new /obj/item/coin/gold(null, 20)
+ var/remainder = gold_coins % 20
+ if(remainder >= 1)
+ location.cached_rewards += new /obj/item/coin/gold(null, remainder)
+ mammon_reward -= gold_coins*10
+ if(!mammon_reward)
+ return
+
+ var/silver_coins = floor(mammon_reward/5)
+ if(silver_coins >= 1)
+ var/stacks = floor(silver_coins/20)
+ if(stacks >= 1)
+ for(var/i in 1 to stacks)
+ location.cached_rewards += new /obj/item/coin/silver(null, 20)
+ var/remainder = silver_coins % 20
+ if(remainder >= 1)
+ location.cached_rewards += new /obj/item/coin/silver(null, remainder)
+ mammon_reward -= silver_coins*5
+ if(!mammon_reward)
+ return
+
+ var/copper = floor(mammon_reward)
+ location.cached_rewards += new /obj/item/coin/copper(null, copper)
+
diff --git a/code/datums/nation/_base_trade.dm b/code/datums/nation/_base_trade.dm
index d1f0fcf47a8..df0e71b2c27 100644
--- a/code/datums/nation/_base_trade.dm
+++ b/code/datums/nation/_base_trade.dm
@@ -2,7 +2,11 @@
var/name = "???"
var/desc = "???"
+ var/trade_icon = 'icons/effects/effects.dmi'
+ var/trade_icon_state = "explosion"
var/datum/nation/papa
+ ///if this is true its basically a node on some other tree
+ var/global_request = FALSE
///these are the trades we need to do prior to this one before we can work on this
var/list/required_trades
@@ -30,7 +34,9 @@
current_imports = target_imports
papa.complete_trade(src)
-/datum/trade/proc/return_valid_count(list/items)
+/datum/trade/proc/return_valid_count(list/items, only_global)
+ if(only_global && !global_request)
+ return
var/count = 0
for(var/atom/atom in items)
if(atom.type in required_trades)
diff --git a/code/datums/nation/nation_ui.dm b/code/datums/nation/nation_ui.dm
new file mode 100644
index 00000000000..7bfebf5a46d
--- /dev/null
+++ b/code/datums/nation/nation_ui.dm
@@ -0,0 +1,907 @@
+/datum/nation/proc/open_trade_ui(mob/user)
+ if(!user?.client)
+ return
+
+ ui_users |= user
+ var/html = generate_trade_html(user)
+ user << browse(html, "window=trade_program;size=1200x800")
+ onclose(user, "trade_program", src)
+
+/datum/nation/proc/update_ui()
+ var/list/data = get_ui_data()
+ var/json_data = json_encode(data)
+
+ for(var/mob/user in ui_users)
+ if(!user?.client)
+ ui_users -= user
+ continue
+ user << output(json_data, "trade_program.browser:updateData")
+
+/datum/nation/proc/get_ui_data()
+ var/list/data = list()
+
+ data["rep"] = nation_rep
+ data["completed_count"] = length(completed_trades)
+
+ var/count = 0
+ for(var/datum/trade_agreement/assign in active_agreements)
+ if(assign.active)
+ count++
+
+ data["active_count"] = count
+
+ var/list/nodes_data = list()
+ for(var/datum/trade/node in nodes)
+ var/is_completed = (node.type in completed_trades)
+ var/is_available = !is_completed && (node in lazyman)
+
+ var/list/node_data = list(
+ "id" = "[node.type]",
+ "name" = node.name,
+ "desc" = node.desc,
+ "status" = is_completed ? "completed" : (is_available ? "available" : "locked"),
+ "current" = node.current_imports,
+ "target" = node.target_imports,
+ "icon" = node.trade_icon ? "\ref[node.trade_icon]" : null,
+ "icon_state" = node.trade_icon_state
+ )
+
+ if(length(node.required_trades))
+ var/list/prereqs = list()
+ for(var/required_path in node.required_trades)
+ var/datum/trade/req_node = find_node_by_path(required_path)
+ if(req_node)
+ prereqs += list(list(
+ "name" = req_node.name,
+ "completed" = (required_path in completed_trades)
+ ))
+ node_data["prerequisites"] = prereqs
+
+ if(length(node.acceptable_imports))
+ var/list/imports = list()
+ for(var/item_path in node.acceptable_imports)
+ imports += get_item_name(item_path)
+ node_data["accepts"] = imports
+
+ if(length(node.supply_packs))
+ var/list/names = list()
+ for(var/datum/supply_pack/pack as anything in node.supply_packs)
+ names += initial(pack.name)
+ node_data["unlocks"] = names
+
+ nodes_data += list(node_data)
+
+ data["nodes"] = nodes_data
+
+ var/list/agreements_data = list()
+ for(var/datum/trade_agreement/agreement in active_agreements)
+ var/original = agreement.max_requested
+ var/remaining = agreement.amount_requested
+ var/progress = original > 0 ? ((original - remaining) / original) * 100 : 0
+
+ var/list/agreement_data = list(
+ "id" = "\ref[agreement]",
+ "name" = agreement.name,
+ "desc" = agreement.desc,
+ "active" = agreement.active,
+ "remaining" = remaining,
+ "progress" = progress,
+ "reward" = agreement.mammon_reward
+ )
+
+ if(length(agreement.required_items))
+ var/list/items = list()
+ for(var/item_path in agreement.required_items)
+ items += get_item_name(item_path)
+ agreement_data["required_items"] = items
+
+ if(agreement.time && agreement.fail_time)
+ agreement_data["time_remaining"] = (agreement.fail_time - world.time) / 10
+
+ if(length(agreement.reward_items))
+ agreement_data["bonus_items"] = length(agreement.reward_items)
+
+ agreements_data += list(agreement_data)
+
+ data["agreements"] = agreements_data
+
+ return data
+
+/datum/nation/proc/generate_trade_html(mob/user)
+ var/list/html_parts = list()
+
+ html_parts += {"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"}
+
+ return html_parts.Join()
+
+/datum/nation/proc/find_node_by_path(node_path)
+ for(var/datum/trade/node in nodes)
+ if(node.type == node_path)
+ return node
+ return null
+
+/datum/nation/proc/get_item_name(item_path)
+ if(ispath(item_path))
+ var/atom/A = item_path
+ return initial(A.name)
+ return "Unknown Item"
+
+/datum/nation/Topic(href, href_list)
+ . = ..()
+
+ var/mob/user = usr
+ if(!user?.client)
+ return
+
+ switch(href_list["action"])
+ if("get_initial_data")
+ //honestly more ui's should be doing this
+ var/list/data = get_ui_data()
+ var/json_data = json_encode(data)
+ user << output(json_data, "trade_program.browser:updateData")
+
+ if("activate_agreement")
+ var/agreement_ref = href_list["agreement"]
+ handle_agreement_activation(user, agreement_ref)
+ update_ui()
+
+/datum/nation/proc/handle_agreement_activation(mob/user, agreement_ref)
+ for(var/datum/trade_agreement/agreement in active_agreements)
+ if("\ref[agreement]" == agreement_ref)
+ if(!activate_agreement(agreement))
+ to_chat(user, "Agreement is already active.")
+ return
diff --git a/code/datums/nation/showcase.dm b/code/datums/nation/showcase.dm
index b3ef736dc78..35ef16aed38 100644
--- a/code/datums/nation/showcase.dm
+++ b/code/datums/nation/showcase.dm
@@ -1,276 +1,125 @@
//! Once we have actual nations this should be snapped away in favor of SSmerchants.nations[nation_type]
//! This is legit just to show what stuff is like
-/datum/trade/iron_basics
- name = "Iron Basics"
- desc = "Basic iron equipment trade"
- min_imports = 5
- max_imports = 8
- supply_packs = list(
- /datum/supply_pack/armor/light/skullcap,
- /datum/supply_pack/armor/light/poth,
- /datum/supply_pack/armor/light/imask,
- /datum/supply_pack/armor/light/chaincoif_iron,
- /datum/supply_pack/armor/light/light_armor_boots
- )
- acceptable_imports = list(
- /obj/item/clothing/head/helmet/skullcap,
- /obj/item/clothing/head/helmet/ironpot,
- /obj/item/clothing/face/facemask,
- /obj/item/clothing/neck/chaincoif/iron,
- /obj/item/clothing/shoes/boots/armor/light
- )
-/datum/trade/leather_start
- name = "Leather Goods"
- desc = "Leather armor and accessories"
- min_imports = 5
- max_imports = 8
- supply_packs = list(
- /datum/supply_pack/armor/light/lightleather_armor,
- /datum/supply_pack/armor/light/leather_bracers,
- /datum/supply_pack/armor/light/heavy_gloves
- )
- acceptable_imports = list(
- /obj/item/clothing/armor/leather,
- /obj/item/clothing/wrists/bracers/leather,
- /obj/item/clothing/gloves/angle
- )
+/datum/trade_agreement/test_request
+ name = "I am Pibble"
+ desc = "Wash my belly"
-/datum/trade/iron_advanced
- name = "Advanced Iron"
- desc = "Higher quality iron equipment"
- min_imports = 8
- max_imports = 12
- required_trades = list(/datum/trade/iron_basics, /datum/trade/leather_start)
- supply_packs = list(
- /datum/supply_pack/armor/light/splint,
- /datum/supply_pack/armor/light/studleather,
- /datum/supply_pack/armor/light/icuirass,
- /datum/supply_pack/armor/light/ihalf_plate,
- /datum/supply_pack/armor/light/ifull_plate,
- /datum/supply_pack/armor/light/chainmail_iron,
- /datum/supply_pack/armor/light/haukberk
- )
- acceptable_imports = list(
- /obj/item/clothing/armor/leather/splint,
- /obj/item/clothing/armor/leather/advanced,
- /obj/item/clothing/armor/cuirass/iron,
- /obj/item/clothing/armor/plate/iron,
- /obj/item/clothing/armor/plate/full/iron,
- /obj/item/clothing/armor/chainmail/iron,
- /obj/item/clothing/armor/chainmail/hauberk/iron
- )
+ possible_items = list(/obj/item/soap)
+ picked_item_count = 1
+ mammon_reward = 100
-/datum/trade/steel_start
- name = "Steel Equipment"
- desc = "Basic steel armor and weapons"
- min_imports = 10
- max_imports = 15
- required_trades = list(/datum/trade/iron_advanced)
- supply_packs = list(
- /datum/supply_pack/armor/steel/nasalh,
- /datum/supply_pack/armor/steel/sallet,
- /datum/supply_pack/armor/steel/buckethelm,
- /datum/supply_pack/armor/steel/smask,
- /datum/supply_pack/armor/steel/chaincoif_steel,
- /datum/supply_pack/armor/steel/cuirass,
- /datum/supply_pack/armor/steel/chainmail,
- /datum/supply_pack/armor/steel/chainmail_hauberk,
- /datum/supply_pack/armor/steel/steel_boots
- )
- acceptable_imports = list(
- /obj/item/clothing/head/helmet/nasal,
- /obj/item/clothing/head/helmet/sallet,
- /obj/item/clothing/head/helmet/heavy/bucket,
- /obj/item/clothing/face/facemask/steel,
- /obj/item/clothing/neck/chaincoif,
- /obj/item/clothing/armor/cuirass,
- /obj/item/clothing/armor/chainmail,
- /obj/item/clothing/armor/chainmail/hauberk,
- /obj/item/clothing/shoes/boots/armor
- )
+/datum/trade/node_1_1
+ name = "Soap Discovery"
+ required_trades = list()
-/datum/trade/steel_advanced
- name = "Elite Steel"
- desc = "Premium steel equipment and rare items"
- min_imports = 12
- max_imports = 20
- required_trades = list(/datum/trade/steel_start)
- supply_packs = list(
- /datum/supply_pack/armor/steel/hounskull,
- /datum/supply_pack/armor/steel/visorsallet,
- /datum/supply_pack/armor/steel/elvenhelm,
- /datum/supply_pack/armor/steel/brigandine,
- /datum/supply_pack/armor/steel/coatofplates,
- /datum/supply_pack/armor/steel/half_plate,
- /datum/supply_pack/armor/steel/elvenplate,
- /datum/supply_pack/armor/steel/plate_gloves
- )
- acceptable_imports = list(
- /obj/item/clothing/head/helmet/visored/hounskull,
- /obj/item/clothing/head/helmet/visored/sallet,
- /obj/item/clothing/head/helmet/sallet/elven,
- /obj/item/clothing/armor/brigandine,
- /obj/item/clothing/armor/brigandine/coatplates,
- /obj/item/clothing/armor/plate,
- /obj/item/clothing/armor/cuirass/rare/elven,
- /obj/item/clothing/gloves/plate
- )
+/datum/trade/node_1_2
+ name = "Basic Hygiene"
+ required_trades = list()
-/datum/nation/debug_showcase
- nodes = list(
- /datum/trade/iron_basics,
- /datum/trade/leather_start,
- /datum/trade/iron_advanced,
- /datum/trade/steel_start,
- /datum/trade/steel_advanced
- )
+/datum/trade/node_1_3
+ name = "Water Management"
+ required_trades = list()
-// Mob proc to show UI
-/mob/proc/show_trade_showcase()
- var/datum/nation/debug_showcase/nation = new()
- show_trade_tree_ui(nation)
+/datum/trade/node_1_4
+ name = "Scent Profiles"
+ required_trades = list()
+
+/datum/trade/node_1_5
+ name = "Lathering Basics"
+ required_trades = list()
+/datum/trade/node_2_1
+ name = "Industrial Scrubbing"
+ required_trades = list(/datum/trade/node_1_1)
-/mob/proc/show_trade_tree_ui(datum/nation/debug_showcase/nation)
- var/dat = "Trade Tree"
- dat += ""
+/datum/trade/node_2_2
+ name = "Laundry Services"
+ required_trades = list(/datum/trade/node_1_2)
- dat += "Trade Tree Showcase
"
- dat += ""
- dat += "
"
+/datum/trade/node_2_3
+ name = "Medical Sanitation"
+ required_trades = list(/datum/trade/node_1_2)
- dat += "
"
+/datum/trade/node_4_3
+ name = "Chemical Synthesis"
+ required_trades = list(/datum/trade/node_3_3)
- //nodes
- for(var/datum/trade/T in nation.nodes)
- var/list/pos_data = node_positions["[T.type]"]
- var/node_x = pos_data["x"]
- var/node_y = pos_data["y"]
+/datum/trade/node_4_4
+ name = "Eternal Fragrance"
+ required_trades = list(/datum/trade/node_3_4)
- var/has_reqs = (T.required_trades && T.required_trades.len) ? "has-reqs" : ""
+/datum/trade/node_4_5
+ name = "Surface Tension Elite"
+ required_trades = list(/datum/trade/node_3_5)
- dat += "
"
- dat += "
[T.name]
"
- dat += "
[T.desc]
"
- dat += "
Imports Required: [T.min_imports]-[T.max_imports]
"
- dat += "
Progress: [T.current_imports]/[T.target_imports]
"
+/datum/trade/node_5_1
+ name = "Absolute Sterility"
+ required_trades = list(/datum/trade/node_4_1)
- if(T.required_trades && T.required_trades.len)
- dat += "
Requires:
"
- for(var/req_path in T.required_trades)
- for(var/datum/trade/RT in nation.nodes)
- if(RT.type == req_path)
- dat += "
- [RT.name]
"
- break
+/datum/trade/node_5_2
+ name = "Molecular Purification"
+ required_trades = list(/datum/trade/node_4_2, /datum/trade/node_4_3)
- dat += "
"
- dat += "
Unlocks ([T.supply_packs.len] items):
"
- var/count = 0
- for(var/pack_path in T.supply_packs)
- if(count >= 5)
- dat += "
... and [T.supply_packs.len - 5] more
"
- break
- var/datum/supply_pack/P = new pack_path()
- dat += "
. [P.name] ([P.cost])
"
- count++
- dat += "
"
+/datum/trade/node_5_3
+ name = "The Great Cleanse"
+ required_trades = list(/datum/trade/node_4_3)
- dat += "
"
+/datum/trade/node_5_4
+ name = "Transcendent Suds"
+ required_trades = list(/datum/trade/node_4_4)
- dat += "
"
- dat += "
"
+/datum/trade/node_5_5
+ name = "Omega Hygiene"
+ required_trades = list(/datum/trade/node_4_5)
- dat += ""
+/datum/nation/debug_showcase
+ nodes = list(
+ /datum/trade/node_1_1, /datum/trade/node_1_2, /datum/trade/node_1_3, /datum/trade/node_1_4, /datum/trade/node_1_5,
+ /datum/trade/node_2_1, /datum/trade/node_2_2, /datum/trade/node_2_3, /datum/trade/node_2_4, /datum/trade/node_2_5,
+ /datum/trade/node_3_1, /datum/trade/node_3_2, /datum/trade/node_3_3, /datum/trade/node_3_4, /datum/trade/node_3_5,
+ /datum/trade/node_4_1, /datum/trade/node_4_2, /datum/trade/node_4_3, /datum/trade/node_4_4, /datum/trade/node_4_5,
+ /datum/trade/node_5_1, /datum/trade/node_5_2, /datum/trade/node_5_3, /datum/trade/node_5_4, /datum/trade/node_5_5
+ )
- dat += ""
+ possible_trade_requests = list(/datum/trade_agreement/test_request)
- src << browse(dat, "window=trade_showcase;size=1200x800")
+/mob/proc/show_trade_showcase()
+ var/datum/nation/debug_showcase/nation = new()
+ nation.open_trade_ui(src)
diff --git a/code/datums/quirks/vices/special.dm b/code/datums/quirks/vices/special.dm
index eccd45b3e6b..5aaa4a71ce3 100644
--- a/code/datums/quirks/vices/special.dm
+++ b/code/datums/quirks/vices/special.dm
@@ -380,8 +380,9 @@
H.apply_status_effect(/datum/status_effect/tremor_grip_loss)
// Shake the screen slightly for immersion
- animate(H.client, pixel_x = rand(-2, 2), pixel_y = rand(-2, 2), time = 2)
- addtimer(CALLBACK(src, PROC_REF(reset_screen_shake), H), 2)
+ if(H.client)
+ animate(H.client, pixel_x = rand(-2, 2), pixel_y = rand(-2, 2), time = 2)
+ addtimer(CALLBACK(src, PROC_REF(reset_screen_shake), H), 2)
/datum/quirk/vice/tremors/proc/reset_screen_shake(mob/living/carbon/human/H)
if(H?.client)
diff --git a/code/datums/skill_holder.dm b/code/datums/skill_holder.dm
index 9aedc5f2f8f..48184a4a446 100644
--- a/code/datums/skill_holder.dm
+++ b/code/datums/skill_holder.dm
@@ -65,6 +65,8 @@
return ensure_skills().get_skill_speed_modifier(skill)
/mob/proc/adjust_experience(skill, amt, silent=FALSE, check_apprentice=TRUE)
+ if(HAS_TRAIT(src, TRAIT_NO_EXPERIENCE))
+ return FALSE
return ensure_skills().adjust_experience(skill, amt, silent, check_apprentice)
/mob/proc/get_inspirational_bonus()
diff --git a/code/game/machinery/artificer_table.dm b/code/game/machinery/artificer_table.dm
index 55ee0ed383f..c1b73155c7c 100644
--- a/code/game/machinery/artificer_table.dm
+++ b/code/game/machinery/artificer_table.dm
@@ -7,19 +7,87 @@
damage_deflection = 25
density = TRUE
climbable = TRUE
+ can_buckle = TRUE
+ buckle_lying = 0
+ max_buckled_mobs = 1
/obj/machinery/artificer_table/examine(mob/user)
. = ..()
if(material)
. += span_warning("There's a [initial(material.name)] ready to be worked.")
+ var/mob/living/buckled = locate() in buckled_mobs
+ if(buckled)
+ . += span_notice("[buckled] is secured to the table.")
+ var/stability = SEND_SIGNAL(buckled, COMSIG_AUGMENT_GET_STABILITY)
+ if(stability)
+ . += span_info("Core Stability: [stability]%")
+
+/obj/machinery/artificer_table/user_buckle_mob(mob/living/M, mob/user, check_loc = TRUE)
+ if(!M.CanReach(src))
+ return
+ if(!isliving(M) || !isliving(user))
+ return
+
+ if(!SEND_SIGNAL(M, COMSIG_AUGMENT_GET_STABILITY) && !istype(M, /mob/living/carbon/human))
+ to_chat(user, span_warning("[M] cannot be secured to the table!"))
+ return
+
+ M.forceMove(get_turf(src))
+ return ..()
+
/obj/machinery/artificer_table/attackby(obj/item/I, mob/living/user, params)
+ var/mob/living/carbon/human/buckled = locate() in buckled_mobs
+
+ if(buckled && istype(I, /obj/item/augment_kit))
+ var/obj/item/augment_kit/kit = I
+ if(!kit.contained_augment)
+ to_chat(user, span_warning("This kit appears to be empty!"))
+ return
+
+ if(!SEND_SIGNAL(buckled, COMSIG_AUGMENT_GET_STABILITY))
+ to_chat(user, span_warning("[buckled] cannot be augmented!"))
+ return
+
+ var/skill = user.get_skill_level(/datum/skill/craft/engineering)
+ if(skill < kit.contained_augment.engineering_difficulty)
+ to_chat(user, span_warning("You lack the engineering skill to install this augment!"))
+ return
+
+ to_chat(user, span_notice("You begin installing [kit.contained_augment.name]..."))
+
+ if(!do_after(user, kit.contained_augment.installation_time, target = buckled))
+ return
+
+ var/result = SEND_SIGNAL(buckled, COMSIG_AUGMENT_INSTALL, kit.contained_augment, user)
+ if(result & COMPONENT_AUGMENT_SUCCESS)
+ qdel(kit)
+ user.mind?.add_sleep_experience(/datum/skill/craft/engineering, user.STAINT * 2)
+ playsound(src, 'sound/effects/sparks1.ogg', 75, TRUE)
+ return
+
+ if(buckled && (istype(I, /obj/item/weapon/hammer)))
+ if(!SEND_SIGNAL(buckled, COMSIG_AUGMENT_GET_STABILITY))
+ . = ..()
+ return
+
+ var/skill = user.get_skill_level(/datum/skill/craft/engineering)
+ var/repair_amount = 5 + (skill * 3)
+
+ to_chat(user, span_notice("You begin repairing [buckled]..."))
+
+ if(do_after(user, 5 SECONDS, target = buckled))
+ SEND_SIGNAL(buckled, COMSIG_AUGMENT_REPAIR, repair_amount, user)
+ user.mind?.add_sleep_experience(/datum/skill/craft/engineering, user.STAINT)
+ return
+
if(istype(I, /obj/item/natural/wood/plank) || istype(I, /obj/item/ingot))
if(!material)
I.forceMove(src)
material = I
update_appearance(UPDATE_OVERLAYS)
return
+
if(istype(I, /obj/item/weapon/hammer))
user.changeNext_move(CLICK_CD_RAPID)
if(!material)
@@ -41,7 +109,7 @@
new_atom.update_integrity(new_atom.max_integrity, update_atom = FALSE)
var/obj/item/created_item_instance = material.artrecipe.created_item
user.visible_message(span_info("[user] creates \a [created_item_instance.name]."))
- user.mind.add_sleep_experience(material.artrecipe.appro_skill, (user.STAINT * (material.artrecipe.craftdiff + 1)/2) * user.get_learning_boon(material.artrecipe.appro_skill)) //may need to be adjusted
+ user.mind.add_sleep_experience(material.artrecipe.appro_skill, (user.STAINT * (material.artrecipe.craftdiff + 1)/2) * user.get_learning_boon(material.artrecipe.appro_skill))
qdel(material)
material = null
update_appearance(UPDATE_OVERLAYS)
@@ -50,17 +118,19 @@
if(prob(max(0, 25 - user.goodluck(2) - (skill * 2))))
to_chat(user, span_warning("Ah yes, my incompetence bears fruit."))
playsound(src,'sound/combat/hits/onwood/destroyfurniture.ogg', 100, FALSE)
- user.mind.add_sleep_experience(material.artrecipe.appro_skill, (user.STAINT * material.artrecipe.craftdiff * 0.25)) // Getting exp for failing
+ user.mind.add_sleep_experience(material.artrecipe.appro_skill, (user.STAINT * material.artrecipe.craftdiff * 0.25))
qdel(material)
material = null
return
if(!material.artrecipe.hammered)
playsound(src, pick('sound/combat/hits/onwood/fence_hit1.ogg', 'sound/combat/hits/onwood/fence_hit2.ogg', 'sound/combat/hits/onwood/fence_hit3.ogg'), 100, FALSE)
material.artrecipe.advance(I, user)
+
if(material && material.artrecipe && material.artrecipe.hammered && istype(I, material.artrecipe.needed_item))
material.artrecipe.item_added(user)
qdel(I)
return
+
..()
/obj/machinery/artificer_table/proc/choose_recipe(user)
diff --git a/code/game/machinery/trams_and_elevators/lift_master.dm b/code/game/machinery/trams_and_elevators/lift_master.dm
index 5283c2a7c3d..c370257d25f 100644
--- a/code/game/machinery/trams_and_elevators/lift_master.dm
+++ b/code/game/machinery/trams_and_elevators/lift_master.dm
@@ -906,6 +906,7 @@ GLOBAL_LIST_EMPTY(active_lifts_by_type)
original_contents += resolved_contents
var/list/sold_items = list()
var/list/sold_count = list()
+ SSmerchant.handle_lift_contents(platform, platform.lift_load, destination) //this potentially nukes some items so its done here
for(var/atom/movable/listed_atom in platform.lift_load)
if(listed_atom in original_contents)
continue
diff --git a/code/game/machinery/trams_and_elevators/tram/cargo_line.dm b/code/game/machinery/trams_and_elevators/tram/cargo_line.dm
index 4b7443ce5a2..f302b5ab203 100644
--- a/code/game/machinery/trams_and_elevators/tram/cargo_line.dm
+++ b/code/game/machinery/trams_and_elevators/tram/cargo_line.dm
@@ -21,6 +21,7 @@
RegisterSignal(SSdcs, COMSIG_DISPATCH_CARGO, PROC_REF(dispatch_cargo))
/obj/effect/landmark/tram/queued_path/cargo_map_enter/tram_reached_travel_point(datum/source, datum/lift_master/tram/tram)
+ tram.destination = null
held_tram = tram
SSmerchant.cargo_docked = TRUE
diff --git a/code/game/machinery/trams_and_elevators/tram/tram_lift_master.dm b/code/game/machinery/trams_and_elevators/tram/tram_lift_master.dm
index 481386753db..801a17899e0 100644
--- a/code/game/machinery/trams_and_elevators/tram/tram_lift_master.dm
+++ b/code/game/machinery/trams_and_elevators/tram/tram_lift_master.dm
@@ -41,6 +41,9 @@
var/obj/effect/landmark/tram/callback_platform
+ ///this is our destination nation
+ var/datum/nation/destination
+
/datum/lift_master/tram/New(obj/structure/industrial_lift/tram/lift_platform)
. = ..()
horizontal_speed = lift_platform.horizontal_speed
diff --git a/code/game/objects/effects/effect_system/particle_defines.dm b/code/game/objects/effects/effect_system/particle_defines.dm
index 0335672ad8c..ac47fabcae5 100644
--- a/code/game/objects/effects/effect_system/particle_defines.dm
+++ b/code/game/objects/effects/effect_system/particle_defines.dm
@@ -96,6 +96,19 @@
spawning = 1
friction = 0.75
+/particles/smoke/cig/big/steam
+ name = "cig_big"
+ icon_state = list("steam_1" = 1, "steam_2" = 2, "steam_3" = 2)
+ gravity = list(0, 0.5, 0)
+ velocity = list(0, 0.1, 0)
+ lifespan = generator(GEN_NUM, 1 SECONDS, 3.5 SECONDS)
+ fade = 1 SECONDS
+ grow = 0.1
+ scale = 0.75
+ spawning = 1
+ friction = 0.75
+ position = generator(GEN_VECTOR, list(-6, -6, 0), list(6, 6, 0), NORMAL_RAND)
+
/particles/smoke/ash
name = "cig_ash"
icon_state = list("ash_1" = 2, "ash_2" = 2, "ash_3" = 1, "smoke_1" = 3, "smoke_2" = 2)
diff --git a/code/game/objects/items/augment.dm b/code/game/objects/items/augment.dm
new file mode 100644
index 00000000000..349f56ca365
--- /dev/null
+++ b/code/game/objects/items/augment.dm
@@ -0,0 +1,80 @@
+/obj/item/augment_kit
+ name = "augmentation kit"
+ desc = "A kit containing components for automaton augmentation. Examine to see details."
+ icon = 'icons/roguetown/items/new_gears.dmi'
+ icon_state = "steel_gear"
+ w_class = WEIGHT_CLASS_SMALL
+ grid_width = 32
+ grid_height = 32
+ var/datum/augment/contained_augment
+
+/obj/item/augment_kit/Initialize()
+ . = ..()
+ if(contained_augment)
+ contained_augment = new contained_augment()
+ name = "[contained_augment.name] kit"
+ desc = "[contained_augment.desc]\n\nStability Cost: [contained_augment.stability_cost]\nRequired Skill: Engineering [contained_augment.engineering_difficulty]"
+
+/obj/item/augment_kit/examine(mob/user)
+ . = ..()
+ if(contained_augment)
+ . += span_info("This kit contains: [contained_augment.name]")
+ . += span_info("Installation requires Engineering skill level [contained_augment.engineering_difficulty]")
+ . += contained_augment.get_examine_info()
+
+/obj/item/augment_kit/strength_servo
+ contained_augment = /datum/augment/stats/strength_servo
+
+/obj/item/augment_kit/perception_lens
+ contained_augment = /datum/augment/stats/perception_lens
+
+/obj/item/augment_kit/processing_core
+ contained_augment = /datum/augment/stats/processing_core
+
+/obj/item/augment_kit/reinforced_frame
+ contained_augment = /datum/augment/stats/reinforced_frame
+
+/obj/item/augment_kit/power_limiter
+ contained_augment = /datum/augment/stats/power_limiter
+
+/obj/item/augment_kit/core_stabilizer
+ contained_augment = /datum/augment/stats/core_stabilizer
+
+/obj/item/augment_kit/combat_matrix
+ contained_augment = /datum/augment/skill/combat_matrix
+
+/obj/item/augment_kit/smithing_optimizer
+ contained_augment = /datum/augment/skill/smithing_optimizer
+
+/obj/item/augment_kit/weaponcraft_matrix
+ contained_augment = /datum/augment/skill/weaponcraft_matrix
+
+/obj/item/augment_kit/engineering_core
+ contained_augment = /datum/augment/skill/engineering_core
+
+/obj/item/augment_kit/mining_efficiency
+ contained_augment = /datum/augment/skill/mining_efficiency
+
+/obj/item/augment_kit/farming_analyzer
+ contained_augment = /datum/augment/skill/farming_analyzer
+
+/obj/item/augment_kit/medicine_database
+ contained_augment = /datum/augment/skill/medicine_database
+
+/obj/item/augment_kit/lockpick_analyzer
+ contained_augment = /datum/augment/skill/lockpick_analyzer
+
+/obj/item/augment_kit/stealth_dampener
+ contained_augment = /datum/augment/skill/stealth_dampener
+
+/obj/item/augment_kit/dualwield
+ contained_augment = /datum/augment/special/dualwield
+
+/obj/item/augment_kit/dualwield_refurbished
+ contained_augment = /datum/augment/special/dualwield/refurbished
+
+/obj/item/augment_kit/sandevistan
+ contained_augment = /datum/augment/special/sandevistan
+
+/obj/item/augment_kit/sandevistan_refurbished
+ contained_augment = /datum/augment/special/sandevistan/refurbished
diff --git a/code/game/objects/items/coins.dm b/code/game/objects/items/coins.dm
index 63006018f82..085ae8bc468 100644
--- a/code/game/objects/items/coins.dm
+++ b/code/game/objects/items/coins.dm
@@ -298,8 +298,18 @@
INVOKE_ASYNC(src, PROC_REF(rig_coin), user)
return
- user.put_in_active_hand(new type(user.loc, 1))
- set_quantity(quantity - 1)
+ var/spawned_type
+ if(base_type)
+ switch(base_type)
+ if(CTYPE_GOLD)
+ spawned_type = /obj/item/coin/gold
+ if(CTYPE_SILV)
+ spawned_type = /obj/item/coin/silver
+ else
+ spawned_type = /obj/item/coin/copper
+ if(spawned_type)
+ user.put_in_active_hand(new spawned_type(user.loc, 1))
+ set_quantity(quantity - 1)
/obj/item/coin/attack_self(mob/living/user, params)
if(quantity > 1 || !base_type)
diff --git a/code/game/objects/items/perfume.dm b/code/game/objects/items/perfume.dm
index fb656733788..73785d0f218 100644
--- a/code/game/objects/items/perfume.dm
+++ b/code/game/objects/items/perfume.dm
@@ -1,19 +1,15 @@
/obj/item/perfume
-
name = "perfume bottle"
desc = "A bottle of pleasantly smelling fragrance."
icon = 'icons/roguetown/items/perfume.dmi'
icon_state = "perfume-bottle-empty"
-
w_class = WEIGHT_CLASS_TINY
item_flags = NOBLUDGEON
-
/// What fragrance is the perfume
var/datum/pollutant/fragrance/fragrance_type
/// How many uses remaining has it got
var/uses_remaining = 10
-
/obj/item/perfume/Initialize()
. = ..()
if(!fragrance_type)
@@ -39,12 +35,11 @@
else
. += "It is empty."
-/obj/item/perfume/afterattack(atom/target, mob/user)
+/obj/item/perfume/afterattack(atom/target, mob/user, proximity_flag)
. = ..()
if(.)
return
- if(!ismovable(target))
- return
+
if(!uses_remaining)
to_chat(user, span_warning("\The [src] is empty!"))
update_appearance(UPDATE_OVERLAYS)
@@ -52,19 +47,80 @@
uses_remaining--
update_appearance(UPDATE_OVERLAYS)
+ user.changeNext_move(CLICK_CD_RANGE * 2)
+ playsound(user, 'sound/items/perfume.ogg', 100, TRUE)
+
+ if(proximity_flag)
+ // Direct application at close range
+ apply_perfume(target, user)
+ else
+ // Spawn perfume cloud projectile
+ spawn_perfume_cloud(target, user)
+
+/obj/item/perfume/proc/apply_perfume(atom/target, mob/user)
+ if(!ismovable(target))
+ return
+
if(target == user)
user.visible_message(span_notice("[user] sprays [user.p_them()]self with \the [src]."), span_notice("You spray yourself with \the [src]."))
else
- user.visible_message(span_notice("[user] sprays [target] with \the [src]."), span_notice("You spray [target] with \the [src]."))
- var/turf/my_turf = get_turf(user)
+ user?.visible_message(span_notice("[user] sprays [target] with \the [src]."), span_notice("You spray [target] with \the [src]."))
+
+ var/turf/my_turf = get_turf(target)
my_turf.pollute_turf(fragrance_type, 20)
- user.changeNext_move(CLICK_CD_RANGE * 2)
- playsound(user, 'sound/items/perfume.ogg', 100, TRUE)
+
if(ismob(target))
var/mob/living/hygiene_target = target
hygiene_target.adjust_hygiene(10)
+
target.AddComponent(/datum/component/temporary_pollution_emission, fragrance_type, 5, 10 MINUTES)
+/obj/item/perfume/proc/spawn_perfume_cloud(atom/target, mob/user)
+ user.visible_message(span_notice("[user] sprays \the [src] toward [target]."), span_notice("You spray \the [src] toward [target]."))
+
+ var/obj/projectile/perfume_cloud/cloud = new(get_turf(user))
+ cloud.fragrance_type = fragrance_type
+ cloud.perfume_source = src
+ cloud.preparePixelProjectile(target, user)
+ cloud.fire()
+
+// Perfume cloud projectile
+/obj/projectile/perfume_cloud
+ name = "perfume cloud"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "smoke" // You may want to create a custom sprite cause smoke is fucked
+ pass_flags = PASSTABLE | PASSGRILLE
+ damage = 0
+ damage_type = BRUTE
+ nodamage = TRUE
+ speed = 0.8
+ /// The fragrance type this cloud carries
+ var/datum/pollutant/fragrance/fragrance_type
+ /// Reference to the perfume bottle
+ var/obj/item/perfume/perfume_source
+
+/obj/projectile/perfume_cloud/Initialize()
+ . = ..()
+ if(fragrance_type)
+ color = fragrance_type.color
+
+/obj/projectile/perfume_cloud/on_hit(atom/target, blocked = FALSE)
+ . = ..()
+ if(perfume_source && fragrance_type)
+ perfume_source.apply_perfume(target, firer)
+ qdel(src)
+
+/obj/projectile/perfume_cloud/Bump(atom/A)
+ if(isturf(A))
+ var/turf/T = A
+ if(T.density)
+ // Hit a wall, dissipate
+ if(fragrance_type)
+ T.pollute_turf(fragrance_type, 10)
+ qdel(src)
+ return
+ return ..()
+
/obj/item/perfume/random
icon_state = MAP_SWITCH("perfume-bottle-empty", "random-perfume")
diff --git a/code/game/rotational_objects/fluid_objects/steam_recharger.dm b/code/game/rotational_objects/fluid_objects/steam_recharger.dm
index 4c8b54b0154..0436435fab1 100644
--- a/code/game/rotational_objects/fluid_objects/steam_recharger.dm
+++ b/code/game/rotational_objects/fluid_objects/steam_recharger.dm
@@ -1,22 +1,33 @@
/obj/structure/steam_recharger
name = "steam injector"
- desc = "Fills objects with steam."
-
+ desc = "Fills objects with steam. Can also recharge automatons."
icon = 'icons/obj/structures/rotation_devices/steam_recharger.dmi'
icon_state = "rechargetable"
-
accepts_water_input = TRUE
var/obj/item/placed_atom
+ var/mob/living/carbon/human/placed_mob
+ var/doing = FALSE
/obj/structure/steam_recharger/Initialize()
. = ..()
START_PROCESSING(SSobj, src)
+/obj/structure/steam_recharger/Destroy()
+ if(placed_mob)
+ placed_mob.forceMove(get_turf(src))
+ placed_mob = null
+ return ..()
+
/obj/structure/steam_recharger/examine(mob/user)
. = ..()
if(placed_atom)
- . += span_notice("Contains")
- . += placed_atom.examine()
+ . += span_notice("Contains an object:")
+ . += placed_atom.examine(user)
+ else if(placed_mob)
+ . += span_notice("Contains:")
+ . += placed_mob.examine(user)
+ else
+ . += span_notice("Empty. Place an item or an automaton here to recharge.")
/obj/structure/steam_recharger/valid_water_connection(direction, obj/structure/water_pipe/pipe)
if(!input)
@@ -25,30 +36,70 @@
return FALSE
/obj/structure/steam_recharger/process()
- if(!placed_atom)
- return
+ if(placed_atom)
+ process_item_charging()
+ else if(placed_mob)
+ process_mob_charging()
+
+/obj/structure/steam_recharger/proc/process_item_charging()
if(!input)
return
-
if(!ispath(input.carrying_reagent, /datum/reagent/steam))
return
-
if(placed_atom.obj_broken)
visible_message(span_notice("[placed_atom] is broken."))
remove_placed()
+ return
var/taking_pressure = min(100, input.water_pressure)
var/obj/structure/water_pipe/picked_provider = pick(input.providers)
picked_provider?.taking_from?.use_water_pressure(taking_pressure)
+
if(!SEND_SIGNAL(placed_atom, COMSIG_ATOM_STEAM_INCREASE, taking_pressure))
- src.visible_message(span_notice("[placed_atom] is fully charged."))
+ visible_message(span_notice("[placed_atom] is fully charged."))
remove_placed()
+/obj/structure/steam_recharger/proc/process_mob_charging()
+ if(!input)
+ return
+ if(!ispath(input.carrying_reagent, /datum/reagent/steam))
+ return
+
+ if(!ishuman(placed_mob))
+ visible_message(span_warning("[src] ejects its occupant - incompatible lifeform!"))
+ remove_placed_mob()
+ return
+
+ var/mob/living/carbon/human/H = placed_mob
+ if(!istype(H.dna?.species, /datum/species/automaton))
+ visible_message(span_warning("[src] ejects [H] - incompatible lifeform!"))
+ remove_placed_mob()
+ return
+
+ if(H.stat == DEAD)
+ visible_message(span_notice("[H] is non-functional."))
+ return
+
+ var/taking_pressure = min(100, input.water_pressure)
+ var/obj/structure/water_pipe/picked_provider = pick(input.providers)
+ picked_provider?.taking_from?.use_water_pressure(taking_pressure)
+
+ if(!SEND_SIGNAL(H, COMSIG_ATOM_STEAM_INCREASE, taking_pressure))
+ visible_message(span_notice("[H] is fully charged."))
+
/obj/structure/steam_recharger/return_rotation_chat()
if(!input || !ispath(input.carrying_reagent, /datum/reagent/steam))
return "NO STEAM INPUT"
- return "Input Pressure:[input ? input.water_pressure : "0"]"
+ var/status = "Input Pressure: [input ? input.water_pressure : "0"]"
+ if(placed_atom)
+ status += " | Charging: ITEM"
+ else if(placed_mob)
+ status += " | Charging: AUTOMATON"
+ else
+ status += " | Status: EMPTY"
+
+ return status
/obj/structure/steam_recharger/proc/remove_placed(mob/user)
placed_atom?.forceMove(get_turf(src))
@@ -57,39 +108,166 @@
placed_atom = null
update_appearance(UPDATE_OVERLAYS)
-/obj/structure/steam_recharger/proc/add_placed(mob/user, obj/item/placer)
- if(placed_atom)
+/obj/structure/steam_recharger/proc/remove_placed_mob(mob/user)
+ if(!placed_mob)
return
+
+ placed_mob.forceMove(get_turf(src))
+
+ if(placed_mob.buckled == src)
+ placed_mob.buckled = null
+
+ if(user)
+ to_chat(user, span_notice("You help [placed_mob] out of [src]."))
+
+ placed_mob = null
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/steam_recharger/proc/add_placed(mob/user, obj/item/placer)
+ if(placed_atom || placed_mob)
+ return FALSE
+
placed_atom = placer
placer.forceMove(src)
update_appearance(UPDATE_OVERLAYS)
-
user.visible_message(span_notice("[user] places [placer] on [src]."), span_notice("You place [placer] on [src]."))
+ return TRUE
-/obj/structure/steam_recharger/update_overlays()
- . = ..()
+/obj/structure/steam_recharger/proc/add_placed_mob(mob/user, mob/living/carbon/human/automaton)
+ if(placed_atom || placed_mob)
+ to_chat(user, span_warning("[src] is already occupied!"))
+ return FALSE
- if(!placed_atom)
- return
- var/mutable_appearance/MA = mutable_appearance()
- MA.appearance = placed_atom.appearance
+ if(!ishuman(automaton))
+ to_chat(user, span_warning("[automaton] cannot use [src]!"))
+ return FALSE
+
+ if(!istype(automaton.dna?.species, /datum/species/automaton))
+ to_chat(user, span_warning("[automaton] is not an automaton!"))
+ return FALSE
+
+ placed_mob = automaton
+ automaton.forceMove(src)
+ automaton.buckled = src
- . += MA
+ update_appearance(UPDATE_OVERLAYS)
+
+ if(user == automaton)
+ user.visible_message(
+ span_notice("[user] climbs onto [src]."),
+ span_notice("You climb onto [src] for recharging.")
+ )
+ else
+ user.visible_message(
+ span_notice("[user] places [automaton] on [src]."),
+ span_notice("You place [automaton] on [src].")
+ )
+
+ return TRUE
+
+/obj/structure/steam_recharger/update_overlays()
+ . = ..()
+ if(placed_atom)
+ var/mutable_appearance/MA = mutable_appearance()
+ MA.appearance = placed_atom.appearance
+ . += MA
+ else if(placed_mob)
+ var/mutable_appearance/MA = mutable_appearance()
+ MA.appearance = placed_mob.appearance
+ MA.pixel_y = 4
+ . += MA
/obj/structure/steam_recharger/attack_hand(mob/user)
. = ..()
- if(!placed_atom)
+
+ // Handle removing item
+ if(placed_atom)
+ user.visible_message(
+ span_danger("[user] starts to lift [placed_atom] from [src]!"),
+ span_notice("You start to remove [placed_atom] from [src]!")
+ )
+ if(!do_after(user, 1.6 SECONDS, src))
+ return
+ remove_placed(user)
return
- user.visible_message(span_danger("[user] starts to lift [placed_atom] from [src]!"), span_notice("You start to remove [placed_atom] from [src]!"))
- if(!do_after(user, 1.6 SECONDS, src))
+
+ // Handle removing mob
+ if(placed_mob)
+ if(user == placed_mob)
+ user.visible_message(
+ span_notice("[user] starts to climb out of [src]."),
+ span_notice("You start to climb out of [src].")
+ )
+ else
+ user.visible_message(
+ span_danger("[user] starts to pull [placed_mob] from [src]!"),
+ span_notice("You start to remove [placed_mob] from [src]!")
+ )
+
+ if(!do_after(user, 1.6 SECONDS, src))
+ return
+ remove_placed_mob(user)
return
- remove_placed(user)
/obj/structure/steam_recharger/attackby(obj/item/I, mob/living/user)
- if(placed_atom)
+ if(placed_atom || placed_mob)
return ..()
+
. = TRUE
- user.visible_message(span_danger("[user] starts to place [I] onto [src]!"), span_notice("You start to place [I] onto [src]!"))
+ user.visible_message(
+ span_danger("[user] starts to place [I] onto [src]!"),
+ span_notice("You start to place [I] onto [src]!")
+ )
if(!do_after(user, 1.6 SECONDS, src))
return
add_placed(user, I)
+
+/obj/structure/steam_recharger/MouseDrop_T(mob/living/carbon/human/automaton, mob/living/user)
+ . = ..()
+
+ if(!ishuman(automaton))
+ return
+
+ if(!istype(automaton.dna?.species, /datum/species/automaton))
+ to_chat(user, span_warning("[automaton] is not an automaton!"))
+ return
+
+ if(placed_atom || placed_mob)
+ to_chat(user, span_warning("[src] is already occupied!"))
+ return
+
+ if(!user.Adjacent(src) || !user.Adjacent(automaton))
+ return
+
+ if(automaton == user)
+ user.visible_message(
+ span_notice("[user] starts to climb onto [src]."),
+ span_notice("You start to climb onto [src].")
+ )
+ else
+ user.visible_message(
+ span_notice("[user] starts to place [automaton] on [src]."),
+ span_notice("You start to place [automaton] on [src].")
+ )
+
+ if(!do_after(user, 2 SECONDS, src))
+ return
+
+ add_placed_mob(user, automaton)
+
+/obj/structure/steam_recharger/relaymove(mob/living/user, direction)
+ if(user != placed_mob)
+ return
+ if(doing)
+ return
+ user.visible_message(
+ span_notice("[user] starts to climb out of [src]."),
+ span_notice("You start to climb out of [src].")
+ )
+ doing = TRUE
+ if(!do_after(user, 1.6 SECONDS, src))
+ doing = FALSE
+ return
+ doing = FALSE
+
+ remove_placed_mob(user)
diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm
index f6699ea3ce3..2b5b2d779c9 100644
--- a/code/game/turfs/open/water.dm
+++ b/code/game/turfs/open/water.dm
@@ -55,7 +55,7 @@
var/cleanliness_factor = 1 //related to hygiene for washing
/// Fishing element for this specific water tile
- var/datum/fish_source/fishing_datum = /datum/fish_source/ocean
+ var/datum/fish_source/fishing_datum = /datum/fish_source/water
flags_1 = CONDUCT_1
/turf/open/water/proc/set_watervolume(volume)
@@ -537,6 +537,7 @@
barefootstep = FOOTSTEP_MUD
heavyfootstep = FOOTSTEP_MUD
cleanliness_factor = -5
+ fishing_datum = /datum/fish_source/sewer
/turf/open/water/sewer/Entered(atom/movable/AM, atom/oldLoc)
. = ..()
@@ -580,6 +581,7 @@
wash_in = FALSE
water_reagent = /datum/reagent/water/gross/sewer
cleanliness_factor = -5
+ fishing_datum = /datum/fish_source/swamp
/turf/open/water/swamp/Initialize()
dir = pick(GLOB.cardinals)
@@ -618,6 +620,7 @@
water_level = 3
slowdown = 20
swim_skill = TRUE
+ fishing_datum = /datum/fish_source/swamp/deep
/turf/open/water/swamp/deep/Entered(atom/movable/AM, atom/oldLoc)
. = ..()
@@ -655,6 +658,7 @@
wash_in = FALSE
water_reagent = /datum/reagent/water/gross/marshy
cleanliness_factor = -3
+ fishing_datum = /datum/fish_source/swamp
/turf/open/water/marsh/Initialize()
dir = pick(GLOB.cardinals)
@@ -667,6 +671,7 @@
water_level = 3
slowdown = 20
swim_skill = TRUE
+ fishing_datum = /datum/fish_source/swamp/deep
/turf/open/water/cleanshallow
name = "water"
@@ -676,6 +681,7 @@
water_level = 2
slowdown = 15
water_reagent = /datum/reagent/water
+ fishing_datum = /datum/fish_source/cleanshallow
/turf/open/water/cleanshallow/Initialize()
dir = pick(GLOB.cardinals)
@@ -716,6 +722,7 @@
swimdir = TRUE
set_relationships_on_init = FALSE
uses_level = FALSE
+ fishing_datum = /datum/fish_source/river
var/river_processing
var/river_processes = TRUE
diff --git a/code/modules/cooking/NeuFood.dm b/code/modules/cooking/NeuFood.dm
index 23aab073b1c..edbc7a3f5b9 100644
--- a/code/modules/cooking/NeuFood.dm
+++ b/code/modules/cooking/NeuFood.dm
@@ -157,7 +157,7 @@
righthand_file = 'icons/roguetown/onmob/righthand.dmi'
icon_state = "bowl"
fill_icon_thresholds = list(0, 30, 50, 100)
- reagent_flags = TRANSFERABLE | AMOUNT_VISIBLE
+ reagent_flags = OPENCONTAINER
force = 5
throwforce = 5
amount_per_transfer_from_this = 5
diff --git a/code/modules/crafting/blueprinting/_base_blueprint_object.dm b/code/modules/crafting/blueprinting/_base_blueprint_object.dm
index 107fd9df835..c042789caf6 100644
--- a/code/modules/crafting/blueprinting/_base_blueprint_object.dm
+++ b/code/modules/crafting/blueprinting/_base_blueprint_object.dm
@@ -17,17 +17,21 @@
var/tmp/list/viewing_images = list() // Track images by client
var/blueprint_dir = SOUTH // Direction this blueprint will be built in
- var/image/cached_image
+ var/tmp/image/cached_image
var/stored_pixel_y = 0
var/stored_pixel_x = 0
- var/time_when_placed
+ var/tmp/time_when_placed
/obj/structure/blueprint/Initialize(mapload)
. = ..()
GLOB.active_blueprints += src
SSblueprints.add_new_blueprint(src)
+/obj/structure/blueprint/after_load()
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(setup_blueprint), 1 SECONDS))
+
/obj/structure/blueprint/Destroy()
GLOB.active_blueprints -= src
SSblueprints.remove_blueprint(src)
@@ -170,6 +174,7 @@
/obj/structure/blueprint/proc/try_construct(mob/user, obj/item/weapon/hammer/hammer)
if(!recipe)
+ qdel(src)
return FALSE
if(!recipe.check_craft_requirements(user, get_turf(src), src))
diff --git a/code/modules/crafting/blueprinting/_base_system.dm b/code/modules/crafting/blueprinting/_base_system.dm
index c37d885e0e6..9ee93ebd96e 100644
--- a/code/modules/crafting/blueprinting/_base_system.dm
+++ b/code/modules/crafting/blueprinting/_base_system.dm
@@ -644,6 +644,8 @@
if(right_click)
if(istype(object, /obj/structure/blueprint))
var/obj/structure/blueprint/print = object
+ if(!print.creator)
+ return TRUE
if(print.creator != user && world.time < print.time_when_placed + 3 MINUTES)
return TRUE
to_chat(user, span_red("[object.name] removed."))
diff --git a/code/modules/fishing/fish.dm b/code/modules/fishing/fish.dm
index c405198dc3f..def0daa89fb 100644
--- a/code/modules/fishing/fish.dm
+++ b/code/modules/fishing/fish.dm
@@ -26,7 +26,6 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
obj_flags = CAN_BE_HIT
max_integrity = 50
sellprice = 10
- dropshrink = 0.6
slices_num = 1
slice_bclass = BCLASS_CHOP
faretype = FARE_IMPOVERISHED //incase someone decides to eat raw fish
@@ -147,6 +146,8 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
var/time_passed_on_safe_turf = 0
+ var/matrix/base_transform
+
/obj/item/reagent_containers/food/snacks/fish/proc/generate_html(mob/user)
var/client/client = user
if(!istype(client))
@@ -480,16 +481,50 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
* Mainly used to determinate the size and weight of caught fish.
*/
/obj/item/reagent_containers/food/snacks/fish/proc/randomize_size_and_weight(base_size = average_size, base_weight = average_weight, deviation = weight_size_deviation, update = TRUE)
- var/size_deviation = 0.2 * base_size
+ var/size_deviation = 0.4 * base_size
temp_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 * base_weight
+ var/weight_deviation = 0.4 * base_weight
temp_weight = round(clamp(gaussian(base_weight, weight_deviation), average_weight * 1/MAX_FISH_DEVIATION_COEFF, average_weight * MAX_FISH_DEVIATION_COEFF))
-
set_max_size_and_weight(temp_size, temp_weight)
if(update)
update_size_and_weight(temp_size, temp_weight)
+ var/size_ratio = temp_size / average_size
+ var/weight_ratio = temp_weight / average_weight
+
+ // Average the two ratios to get overall deviation
+ var/deviation_score = (abs(size_ratio - 1.0) + abs(weight_ratio - 1.0)) / 2
+
+ // Convert deviation to quality (1-4)
+ // Small deviation (close to average) = quality 1
+ // Large deviation (exceptional fish) = quality 4
+ var/fish_quality = 1
+ if(deviation_score >= 0.6) // Very exceptional (near max/min bounds)
+ fish_quality = 4
+ else if(deviation_score >= 0.4) // Notable deviation
+ fish_quality = 3
+ else if(deviation_score >= 0.2) // Moderate deviation
+ fish_quality = 2
+ else // Close to average
+ fish_quality = 1
+
+ // Scale the visual size based on actual size/weight vs average
+ // Use average of size and weight ratios for balanced scaling
+ var/scale_ratio = (size_ratio + weight_ratio) / 2
+
+ // Clamp between 0.5x and 2.0x for reasonable visual range
+ // A tiny fish (0.5x average) = 0.75 scale
+ // A normal fish (1.0x average) = 1.0 scale
+ // A huge fish (2.0x average) = 1.5 scale
+ var/visual_scale = clamp(0.5 + (scale_ratio * 0.5), 0.5, 2.0)
+
+ // Store the base scale for use in animations
+ base_transform = matrix()
+ base_transform.Scale(visual_scale)
+ transform = base_transform
+
+ set_quality(fish_quality)
+
///Set the maximum size and weight a fish can reach from base size and weight args if they have't been set already.
/obj/item/reagent_containers/food/snacks/fish/proc/set_max_size_and_weight(base_size, base_weight)
if(!maximum_size)
@@ -567,8 +602,11 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
update_fish_force()
- slices_num = max(round(slices_num * size / FISH_FILLET_NUMBER_SIZE_DIVISOR, 1), 1)
+ slices_num = max(round(size / average_size * initial(slices_num), 1), 1)
sellprice = initial(sellprice) * (1 + (max(FLOOR(weight/average_weight, 0.1), 0.1) - 1))
+
+ if((/datum/fish_trait/prehistoric in fish_traits))
+ sellprice *= 5
fish_flags &= ~FISH_FLAG_UPDATING_SIZE_AND_WEIGHT
///Reset weapon-related variables of this items and recalculates those values based on the fish weight and size.
@@ -677,6 +715,15 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
apply_traits()
/obj/item/reagent_containers/food/snacks/fish/proc/apply_traits()
+ var/list/potential_spontaneous_traits = GLOB.spontaneous_fish_traits[type]
+ for(var/trait_type in potential_spontaneous_traits)
+ if(!prob(potential_spontaneous_traits[trait_type]))
+ continue
+ var/datum/fish_trait/trait = GLOB.fish_traits[trait_type]
+ if(length(fish_traits & trait.incompatible_traits))
+ continue
+ fish_traits |= trait_type
+
for(var/fish_trait_type in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait_type]
trait.apply_to_fish(src)
@@ -947,16 +994,20 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
/// This flopping animation played while the fish is alive.
/obj/item/reagent_containers/food/snacks/fish/proc/flop_animation()
var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
+
+ // Use base_transform if it exists, otherwise use matrix()
+ var/matrix/base_matrix = base_transform || matrix()
+
animate(src, time = pause_between, loop = -1)
//move nose down and up
for(var/_ in 1 to FLOP_COUNT)
- var/matrix/up_matrix = matrix()
+ var/matrix/up_matrix = matrix(base_matrix)
up_matrix.Turn(FLOP_DEGREE)
- var/matrix/down_matrix = matrix()
+ var/matrix/down_matrix = matrix(base_matrix)
down_matrix.Turn(-FLOP_DEGREE)
animate(transform = down_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
animate(transform = up_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
- animate(transform = matrix(), time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN)
+ animate(transform = base_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN)
animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
//bounce up and down
animate(time = pause_between, loop = -1, flags = ANIMATION_PARALLEL)
@@ -988,7 +1039,8 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
/obj/item/reagent_containers/food/snacks/fish/proc/stop_flopping()
if(HAS_TRAIT(src, TRAIT_FISH_FLOPPING))
REMOVE_TRAIT(src, TRAIT_FISH_FLOPPING, TRAIT_GENERIC)
- animate(src, transform = matrix()) //stop animation
+ // Return to base scaled transform instead of default matrix()
+ animate(src, transform = base_transform || matrix())
/// Refreshes flopping animation after temporary animation finishes
/obj/item/reagent_containers/food/snacks/fish/proc/on_temp_animation(datum/source, animation_duration)
@@ -1111,7 +1163,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
desc = "A common freshwater fish with large scales."
icon_state = "carp"
fish_id = "carp"
- average_size = 60
+ average_size = 90
average_weight = 2000
required_fluid_type = FISH_FLUID_FRESHWATER
fishing_difficulty_modifier = 5
@@ -1123,7 +1175,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
desc = "A small, brightly colored tropical fish."
icon_state = "clownfish"
fish_id = "clownfish"
- average_size = 15
+ average_size = 60
average_weight = 200
required_fluid_type = FISH_FLUID_SALTWATER
required_temperature_min = 24
@@ -1138,6 +1190,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
FISH_BAIT_VALUE = MEAT,
),
)
+ fish_traits = list(/datum/fish_trait/camouflage, /datum/fish_trait/picky_eater)
/obj/item/reagent_containers/food/snacks/fish/angler
name = "anglerfish"
@@ -1158,7 +1211,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
FISH_BAIT_VALUE = MEAT,
),
)
- fish_traits = list(/datum/fish_trait/predator, /datum/fish_trait/heavy)
+ fish_traits = list(/datum/fish_trait/predator, /datum/fish_trait/heavy, /datum/fish_trait/nocturnal, /datum/fish_trait/carnivore)
/obj/item/reagent_containers/food/snacks/fish/eel
name = "eel"
@@ -1198,6 +1251,7 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
FISH_BAIT_VALUE = VEGETABLES,
),
)
+ fish_traits = list(/datum/fish_trait/vegan, /datum/fish_trait/yucky)
/obj/item/reagent_containers/food/snacks/fryfish
icon = 'icons/roguetown/misc/fish.dmi'
@@ -1209,7 +1263,6 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
desc = "Abyssor's bounty, make sure to eat the eyes!"
icon_state = "carpcooked"
foodtype = MEAT
- dropshrink = 0.6
/obj/item/reagent_containers/food/snacks/fryfish/carp
name = "cooked carp"
@@ -1277,5 +1330,4 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
tastes = list("a horrible clash of salty fish and sweet chocolate" = 1)
faretype = FARE_IMPOVERISHED
rotprocess = null
- dropshrink = 0.6
eat_effect = /datum/status_effect/buff/foodbuff
diff --git a/code/modules/fishing/fish_stats/_traits.dm b/code/modules/fishing/fish_stats/_traits.dm
index fa4259c973d..e79299da638 100644
--- a/code/modules/fishing/fish_stats/_traits.dm
+++ b/code/modules/fishing/fish_stats/_traits.dm
@@ -30,7 +30,14 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
/// The probability this trait can be inherited by offsprings when both mates have it
var/inheritability = 50
/// A list of fish types and traits that they can spontaneously manifest with associated probabilities
- var/list/spontaneous_manifest_types
+ var/list/spontaneous_manifest_types = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 3,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 3,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 3,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 3,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 3,
+ /obj/item/reagent_containers/food/snacks/fish/swordfish = 3,
+ )
/// An optional whitelist of fish that can get this trait
var/list/fish_whitelist
/// Depending on the value, fish with trait will be reported as more or less difficult in the catalog.
@@ -159,8 +166,6 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
if(light_amount > SHADOW_SPECIES_LIGHT_THRESHOLD)
source.damage_fish(0.5 * seconds_per_tick)
-
-
/datum/fish_trait/heavy
name = "Demersal"
catalog_description = "This fish tends to stay near the waterbed."
@@ -190,7 +195,7 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
/datum/fish_trait/vegan
name = "Herbivore"
catalog_description = "This fish can only be baited with fresh produce."
- incompatible_traits = list(/datum/fish_trait/carnivore, /datum/fish_trait/predator, /datum/fish_trait/necrophage)
+ incompatible_traits = list(/datum/fish_trait/carnivore, /datum/fish_trait/predator)
/datum/fish_trait/vegan/catch_weight_mod(obj/item/fishingrod/rod, mob/fisherman, atom/location, obj/item/reagent_containers/food/snacks/fish/fish_type)
. = ..()
@@ -212,80 +217,6 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
if(!is_matching_bait(rod.baited, bait_liked_identifier) || is_matching_bait(rod.baited, bait_hated_identifier))
.[MULTIPLICATIVE_FISHING_MOD] = 0
-
-/datum/fish_trait/necrophage
- name = "Necrophage"
- 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/reagent_containers/food/snacks/fish/fish)
- . = ..()
- RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(eat_dead_fishes))
-
-/datum/fish_trait/necrophage/proc/eat_dead_fishes(obj/item/reagent_containers/food/snacks/fish/source, seconds_per_tick)
- SIGNAL_HANDLER
- if(source.get_hunger() > 0.75 || !source.loc)
- return
- for(var/obj/item/reagent_containers/food/snacks/fish/victim in source.loc)
- if(victim.status != FISH_DEAD || victim == source || HAS_TRAIT(victim, TRAIT_YUCKY_FISH))
- continue
- eat_fish(source, victim)
- return
-
-/datum/fish_trait/parthenogenesis
- name = "Parthenogenesis"
- catalog_description = "This fish can reproduce asexually, without the need of a mate."
- inheritability = 40
-
-/datum/fish_trait/parthenogenesis/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_SELF_REPRODUCE, FISH_TRAIT_DATUM)
-
-/**
- * Useful for those species with the parthenogenesis trait if you don't want them to mate with each other,
- * or for similar shenanigans, I don't know.
- * Otherwise you could just set the stable_population to 1.
- */
-/datum/fish_trait/no_mating
- name = "Mateless"
- catalog_description = "This fish cannot reproduce with other fishes."
- incompatible_traits = list(/datum/fish_trait/crossbreeder)
-
-/datum/fish_trait/no_mating/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_NO_MATING, FISH_TRAIT_DATUM)
-
-///Prevent offsprings of fish with this trait from being of the same type (unless self-mating or the partner also has the trait)
-/datum/fish_trait/recessive
- name = "Recessive"
- catalog_description = "If crossbred, offsprings will always be of the mate species, unless it also possess the trait."
- inheritability = 0
-
-/datum/fish_trait/no_mating/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_RECESSIVE, FISH_TRAIT_DATUM)
-
-/datum/fish_trait/revival
- name = "Self-Revival"
- catalog_description = "This fish shows a peculiar ability of reviving itself a minute or two after death."
-
-/datum/fish_trait/revival/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- RegisterSignal(fish, COMSIG_FISH_STATUS_CHANGED, PROC_REF(check_status))
-
-/datum/fish_trait/revival/proc/check_status(obj/item/reagent_containers/food/snacks/fish/source)
- SIGNAL_HANDLER
- if(source.status == FISH_DEAD)
- addtimer(CALLBACK(src, PROC_REF(revive), WEAKREF(source)), rand(1 MINUTES, 2 MINUTES))
-
-/datum/fish_trait/revival/proc/revive(datum/weakref/fish_ref)
- var/obj/item/reagent_containers/food/snacks/fish/source = fish_ref.resolve()
- if(QDELETED(source) || source.status != FISH_DEAD)
- return
- source.set_status(FISH_ALIVE)
- var/message = span_nicegreen("[source] twitches. It's alive!")
- source.visible_message(message)
-
/datum/fish_trait/predator
name = "Predator"
catalog_description = "It's a predatory fish. It'll hunt down and eat live fishes of smaller size when hungry."
@@ -321,47 +252,6 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
. = ..()
ADD_TRAIT(fish, TRAIT_YUCKY_FISH, FISH_TRAIT_DATUM)
-/datum/fish_trait/toxin_immunity
- name = "Toxin Immunity"
- catalog_description = "This fish has developed an ample-spected immunity to toxins."
-
-/datum/fish_trait/toxin_immunity/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_TOXIN_IMMUNE, FISH_TRAIT_DATUM)
-
-/datum/fish_trait/crossbreeder
- name = "Crossbreeder"
- catalog_description = "This fish's adaptive genetics allows it to crossbreed with other fish species."
- inheritability = 40
- incompatible_traits = list(/datum/fish_trait/no_mating)
-
-/datum/fish_trait/crossbreeder/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_CROSSBREEDER, FISH_TRAIT_DATUM)
-
-/datum/fish_trait/territorial
- name = "Territorial"
- catalog_description = "This fish will start attacking other fish if the aquarium has five or more."
-
-/datum/fish_trait/territorial/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(try_attack_fish))
-
-/datum/fish_trait/territorial/proc/try_attack_fish(obj/item/reagent_containers/food/snacks/fish/source, seconds_per_tick)
- SIGNAL_HANDLER
- if(!source.loc || !SPT_PROB(1, seconds_per_tick))
- return
- var/list/fishes = source.get_aquarium_fishes(TRUE, source)
- if(length(fishes) < 5)
- return
- for(var/obj/item/reagent_containers/food/snacks/fish/victim as anything in source.get_aquarium_fishes(TRUE, source))
- if(victim.status != FISH_ALIVE)
- continue
- source.loc.visible_message(span_warning("[source] violently [pick("whips", "bites", "attacks", "slams")] [victim]"))
- var/damage = round(rand(4, 20) * (source.size / victim.size)) //smaller fishes take extra damage.
- victim.damage_fish(damage)
- return
-
/datum/fish_trait/lubed
name = "Slippery"
catalog_description = "This fish exudes a viscous, slippery lubrificant. It's recommended not to step on it."
@@ -369,32 +259,12 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
/datum/fish_trait/lubed/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
. = ..()
- fish.AddComponent(/datum/component/slippery, 8 SECONDS, SLIDE|GALOSHES_DONT_HELP)
-
+ fish.AddComponent(/datum/component/slippery, 1 SECONDS, SLIDE|GALOSHES_DONT_HELP)
/datum/fish_trait/lubed/minigame_mod(obj/item/fishingrod/rod, mob/fisherman, datum/fishing_challenge/minigame)
minigame.reeling_velocity *= 1.4
minigame.gravity_velocity *= 1.4
-/datum/fish_trait/amphibious
- name = "Amphibious"
- catalog_description = "This fish has developed a primitive adaptation to life on both land and water."
-
-/datum/fish_trait/amphibious/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_AMPHIBIOUS, FISH_TRAIT_DATUM)
- if(fish.required_fluid_type == FISH_FLUID_AIR)
- fish.required_fluid_type = FISH_FLUID_FRESHWATER
-
-/datum/fish_trait/mixotroph
- name = "Mixotroph"
- catalog_description = "This fish is capable of subsisting itself by producing its own sources of energy (food)."
- incompatible_traits = list(/datum/fish_trait/predator, /datum/fish_trait/necrophage)
-
-/datum/fish_trait/mixotroph/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- ADD_TRAIT(fish, TRAIT_FISH_NO_HUNGER, FISH_TRAIT_DATUM)
-
/datum/fish_trait/antigrav
name = "Anti-Gravity"
catalog_description = "This fish will invert the gravity of the bait at random. May fall upward outside after being caught."
@@ -404,41 +274,14 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
minigame.special_effects |= FISHING_MINIGAME_RULE_ANTIGRAV
-
-///Anxiety means the fish will die if in a location with more than 3 fish (including itself)
-///This is just barely enough to crossbreed out of anxiety, but it severely limits the potential of
-/datum/fish_trait/anxiety
- name = "Anxiety"
- catalog_description = "This fish tends to die of stress when forced to be around too many other fish."
-
-/datum/fish_trait/anxiety/difficulty_mod(obj/item/fishingrod/rod, mob/fisherman)
- . = ..()
- // Anxious fish are easier with a cloaked line.
- if(rod.line && (rod.line.fishing_line_traits & FISHING_LINE_CLOAKED))
- .[ADDITIVE_FISHING_MOD] -= FISH_TRAIT_MINOR_DIFFICULTY_BOOST
-
-/datum/fish_trait/anxiety/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
- . = ..()
- RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(on_fish_life))
-
-///signal sent when the anxiety fish is fed, killing it if sharing contents with too many fish.
-/datum/fish_trait/anxiety/proc/on_fish_life(obj/item/reagent_containers/food/snacks/fish/fish, seconds_per_tick)
- SIGNAL_HANDLER
- var/fish_tolerance = 3
- if(!fish.loc || fish.status == FISH_DEAD)
- return
- for(var/obj/item/reagent_containers/food/snacks/fish/other_fish in fish.loc.contents)
- if(fish_tolerance <= 0)
- fish.loc.visible_message(span_warning("[fish] seems to freak out for a moment, then it stops moving..."))
- fish.set_status(FISH_DEAD)
- return
- fish_tolerance -= 1
-
-
/datum/fish_trait/camouflage
name = "Camouflage"
catalog_description = "This fish possess the ability to blend with its surroundings."
added_difficulty = 5
+ spontaneous_manifest_types = list(
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 15,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 8,
+ )
/datum/fish_trait/camouflage/minigame_mod(obj/item/fishingrod/rod, mob/fisherman, datum/fishing_challenge/minigame)
minigame.special_effects |= FISHING_MINIGAME_RULE_CAMO
@@ -452,7 +295,7 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
SIGNAL_HANDLER
if(source.status == FISH_DEAD || source.last_move + 5 SECONDS >= world.time)
return
- source.alpha = max(source.alpha - 10 * seconds_per_tick, 10)
+ source.alpha = max(source.alpha - 10 * seconds_per_tick, 60)
/datum/fish_trait/camouflage/proc/reset_alpha(obj/item/reagent_containers/food/snacks/fish/source)
SIGNAL_HANDLER
@@ -461,3 +304,84 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
var/init_alpha = initial(source.alpha)
if(init_alpha != source.alpha)
animate(source, alpha = init_alpha, time = 1.2 SECONDS, easing = CIRCULAR_EASING|EASE_OUT)
+
+/datum/fish_trait/prehistoric
+ name = "Living Fossil"
+ catalog_description = "An ancient species thought extinct. Extremely rare and valuable."
+ inheritability = 60
+ added_difficulty = 25
+
+/datum/fish_trait/prehistoric/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
+ . = ..()
+ fish.sellprice *= 5
+ ADD_TRAIT(fish, TRAIT_FISH_RECESSIVE, FISH_TRAIT_DATUM)
+
+/datum/fish_trait/deep_dweller
+ name = "Deep Dweller"
+ catalog_description = "This fish lives in the deepest waters and suffers in shallow light."
+
+/datum/fish_trait/deep_dweller/catch_weight_mod(obj/item/fishingrod/rod, mob/fisherman, atom/location, obj/item/reagent_containers/food/snacks/fish/fish_type)
+ . = ..()
+ var/turf/targeted = location
+ if(!targeted.can_see_sky()) // Covered/deep water
+ .[MULTIPLICATIVE_FISHING_MOD] *= 2
+ else
+ .[MULTIPLICATIVE_FISHING_MOD] *= 0.2
+
+/datum/fish_trait/deep_dweller/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
+ . = ..()
+ RegisterSignal(fish, COMSIG_FISH_LIFE, PROC_REF(check_depth))
+
+/datum/fish_trait/deep_dweller/proc/check_depth(obj/item/reagent_containers/food/snacks/fish/source, seconds_per_tick)
+ SIGNAL_HANDLER
+ if(!source.loc || !isturf(source.loc))
+ return
+ var/turf/turf = get_turf(source)
+ if(turf.can_see_sky()) // Shallow water = damage
+ source.damage_fish(1 * seconds_per_tick)
+
+/datum/fish_trait/venomous
+ name = "Venomous"
+ catalog_description = "This fish secretes toxins. Can poison other fish and handlers."
+ incompatible_traits = list(/datum/fish_trait/yucky)
+ reagents_to_add = list(/datum/reagent/toxin = 2)
+ added_difficulty = 10
+
+/datum/fish_trait/treasure_hunter
+ name = "Treasure Hunter"
+ catalog_description = "This fish collects shiny objects. May have valuables in its stomach when caught."
+ incompatible_traits = list(/datum/fish_trait/vegan)
+
+/datum/fish_trait/treasure_hunter/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
+ . = ..()
+ // When the fish is butchered/killed, small chance to drop coins or gems
+ RegisterSignal(fish, COMSIG_FISH_STATUS_CHANGED, PROC_REF(drop_treasure))
+
+/datum/fish_trait/treasure_hunter/proc/drop_treasure(obj/item/reagent_containers/food/snacks/fish/source)
+ SIGNAL_HANDLER
+ if(source.status != FISH_DEAD || !prob(15))
+ return
+ var/treasure_type = pick(/obj/item/coin/copper, /obj/item/coin/silver, /obj/item/coin/gold)
+ new treasure_type(get_turf(source))
+ source.visible_message(span_notice("Something shiny falls out of [source]!"))
+
+/datum/fish_trait/bioluminescent
+ name = "Bioluminescent"
+ catalog_description = "This fish emits a natural glow in dark waters. Easier to spot at night."
+ incompatible_traits = list(/datum/fish_trait/nocturnal, /datum/fish_trait/camouflage)
+ added_difficulty = -3
+
+/datum/fish_trait/bioluminescent/catch_weight_mod(obj/item/fishingrod/rod, mob/fisherman, atom/location, obj/item/reagent_containers/food/snacks/fish/fish_type)
+ . = ..()
+ if(HAS_TRAIT(rod, TRAIT_ROD_IGNORE_ENVIRONMENT))
+ return
+ var/turf/turf = get_turf(location)
+ var/light_amount = turf?.get_lumcount()
+ // Easier to find in darkness
+ if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD)
+ .[MULTIPLICATIVE_FISHING_MOD] *= 1.5
+
+/datum/fish_trait/bioluminescent/apply_to_fish(obj/item/reagent_containers/food/snacks/fish/fish)
+ . = ..()
+ fish.set_light_range(2)
+ fish.set_light_color(COLOR_CYAN)
diff --git a/code/modules/fishing/sources/_source.dm b/code/modules/fishing/sources/_source.dm
index 40c9d9718d8..81cea1a2e91 100644
--- a/code/modules/fishing/sources/_source.dm
+++ b/code/modules/fishing/sources/_source.dm
@@ -29,7 +29,7 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
/datum/fish_source
/**
* Fish catch weight table - these are relative weights
- *
+ * Keys are fish type paths, values are base 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
@@ -77,6 +77,9 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
//list of subtypes of associated safe turfs that are NOT safe
var/list/safe_turfs_blacklist
+ /// Pool of generated fish instances for this roll (fish instance -> original path)
+ var/list/obj/item/reagent_containers/food/snacks/fish/generated_fish_pool
+
/datum/fish_source/New()
if(!SSfishing.initialized && associated_safe_turfs) //This is only needed during world init
associated_safe_turfs = typecacheof(associated_safe_turfs)
@@ -94,8 +97,42 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
/datum/fish_source/Destroy()
if(explosive_fishing_score)
STOP_PROCESSING(SSprocessing, src)
+ cleanup_generated_fish()
return ..()
+/// Cleans up any generated fish instances from the pool
+/datum/fish_source/proc/cleanup_generated_fish()
+ if(!generated_fish_pool)
+ return
+ for(var/obj/item/reagent_containers/food/snacks/fish/fish in generated_fish_pool)
+ if(!QDELETED(fish))
+ qdel(fish)
+ generated_fish_pool = null
+
+/// Generates a randomized fish instance from a path for filtering/checking
+/datum/fish_source/proc/generate_fish_instance(fish_path)
+ if(!ispath(fish_path, /obj/item/reagent_containers/food/snacks/fish))
+ return null
+
+ var/obj/item/reagent_containers/food/snacks/fish/fish = new fish_path()
+ fish.randomize_size_and_weight()
+
+ // Store in pool for cleanup and tracking
+ if(!generated_fish_pool)
+ generated_fish_pool = list()
+ generated_fish_pool[fish] = fish_path
+
+ return fish
+
+/// Gets the original path of a generated fish instance
+/datum/fish_source/proc/get_fish_path(atom/fish_or_path)
+ if(isfish(fish_or_path))
+ // Check if it's one of our generated instances
+ if(generated_fish_pool?[fish_or_path])
+ return generated_fish_pool[fish_or_path]
+ return fish_or_path.type
+ return fish_or_path
+
///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
@@ -158,18 +195,23 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
// Difficulty modifier added by the rod
. += rod.difficulty_modifier
- var/is_fish_instance = isfish(result)
- if(!ispath(result,/obj/item/reagent_containers/food/snacks/fish) && !is_fish_instance)
+ // If it's a fish instance from our pool, use it directly
+ var/obj/item/reagent_containers/food/snacks/fish/caught_fish
+ if(isfish(result) && generated_fish_pool?[result])
+ caught_fish = result
+ else if(!ispath(result, /obj/item/reagent_containers/food/snacks/fish))
// In the future non-fish rewards can have variable difficulty calculated here
return
+ else
+ // Generate a temporary instance for difficulty calculation
+ caught_fish = generate_fish_instance(result)
+ if(!caught_fish)
+ return
- var/obj/item/reagent_containers/food/snacks/fish/caught_fish = result
-
- //Just to clarify when we should use the path instead of the fish, which can be both a path and an instance.
- var/result_path = is_fish_instance ? caught_fish.type : result
+ var/result_path = get_fish_path(result)
// Baseline fish difficulty
- . += initial(caught_fish.fishing_difficulty_modifier)
+ . += caught_fish.fishing_difficulty_modifier
var/list/fish_properties = SSfishing.fish_properties[result_path]
if(rod.baited)
@@ -186,11 +228,7 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
. += DISLIKED_BAIT_DIFFICULTY_MOD
// Matching/not matching fish traits and equipment
- var/list/fish_traits
- if(is_fish_instance)
- fish_traits = caught_fish.fish_traits
- else
- fish_traits = fish_properties[FISH_PROPERTIES_TRAITS]
+ var/list/fish_traits = caught_fish.fish_traits
var/additive_mod = 0
var/multiplicative_mod = 1
@@ -210,7 +248,7 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
SHOULD_NOT_OVERRIDE(TRUE)
rewards += roll_reward(rod, fisherman, location)
-/// Returns a typepath, instance or another special value which we use for dispensing a reward later.
+/// Returns a fish instance or another special value which we use for dispensing a reward later.
/datum/fish_source/proc/roll_reward(obj/item/fishingrod/rod, mob/fisherman, atom/location)
return pickweight(get_modified_fish_table(rod, fisherman, location)) || FISHING_DUD
@@ -240,10 +278,12 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
SHOULD_CALL_PARENT(TRUE)
UnregisterSignal(user, COMSIG_MOB_COMPLETE_FISHING)
if(!success)
+ cleanup_generated_fish() // Clean up fish pool on failure
return
var/atom/movable/reward = dispense_reward(challenge.reward_path, user, challenge.location, challenge.used_rod)
SEND_SIGNAL(challenge.used_rod, COMSIG_FISHING_ROD_CAUGHT_FISH, reward, user)
challenge.used_rod.on_reward_caught(reward, user)
+ cleanup_generated_fish() // Clean up remaining fish pool after dispensing
/// Gives out the reward if possible
/datum/fish_source/proc/dispense_reward(reward_path, mob/fisherman, atom/fishing_spot, obj/item/fishingrod/rod)
@@ -263,16 +303,19 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
/datum/fish_source/proc/simple_dispense_reward(reward_path, atom/spawn_location, atom/fishing_spot)
if(isnull(reward_path))
return null
- if(!isnull(fish_counts[reward_path])) // This is limited count result
+
+ var/count_key = get_fish_path(reward_path)
+
+ if(!isnull(fish_counts[count_key])) // This is limited count result
//Somehow, we're trying to spawn an expended reward.
- if(fish_counts[reward_path] <= 0)
+ if(fish_counts[count_key] <= 0)
return null
- fish_counts[reward_path] -= 1
- var/regen_time = fish_count_regen?[reward_path]
+ fish_counts[count_key] -= 1
+ var/regen_time = fish_count_regen?[count_key]
if(regen_time)
- LAZYADDASSOC(currently_on_regen, reward_path, 1)
- if(currently_on_regen[reward_path] == 1)
- addtimer(CALLBACK(src, PROC_REF(regen_count), reward_path), regen_time)
+ LAZYADDASSOC(currently_on_regen, count_key, 1)
+ if(currently_on_regen[count_key] == 1)
+ addtimer(CALLBACK(src, PROC_REF(regen_count), count_key), regen_time)
var/atom/movable/reward = spawn_reward(reward_path, spawn_location, fishing_spot)
SEND_SIGNAL(src, COMSIG_FISH_SOURCE_REWARD_DISPENSED, reward)
@@ -289,10 +332,19 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
var/regen_time = fish_count_regen[reward_path]
addtimer(CALLBACK(src, PROC_REF(regen_count), reward_path), regen_time)
-/// Spawns a reward from a atom path right where the fisherman is. Part of the dispense_reward() logic.
+/// Spawns a reward from a fish instance or path right where the fisherman is. Part of the dispense_reward() logic.
/datum/fish_source/proc/spawn_reward(reward_path, atom/spawn_location, atom/fishing_spot)
if(reward_path == FISHING_DUD)
return
+
+ // If it's one of our generated fish instances, move it to the world
+ if(isfish(reward_path) && generated_fish_pool?[reward_path])
+ var/obj/item/reagent_containers/food/snacks/fish/fish = reward_path
+ // Remove from pool so it doesn't get cleaned up
+ generated_fish_pool -= fish
+ fish.forceMove(spawn_location)
+ return fish
+
if(ismovable(reward_path))
var/atom/movable/reward = reward_path
reward.forceMove(spawn_location)
@@ -315,13 +367,17 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
/// Builds a fish weights table modified by bait/rod/user properties
/datum/fish_source/proc/get_modified_fish_table(obj/item/fishingrod/rod, mob/fisherman, atom/location)
+ // Clean up any previous fish pool
+ cleanup_generated_fish()
+
var/obj/item/bait = rod.baited
///An exponent used to level out the table weight differences between fish depending on bait quality.
var/leveling_exponent = 0
///Multiplier used to make fishes more common compared to everything else.
var/result_multiplier = 1
- var/list/final_table = get_fish_table(location)
+ var/list/base_table = get_fish_table(location)
+ var/list/final_table = list()
if(bait)
for(var/trait in weight_result_multiplier)
@@ -330,33 +386,49 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
leveling_exponent = weight_leveling_exponents[trait]
break
-
if(HAS_TRAIT(rod, TRAIT_ROD_REMOVE_FISHING_DUD))
- final_table -= FISHING_DUD
+ base_table -= FISHING_DUD
+
+ // Generate fish instances and build the weighted table
+ for(var/result in base_table)
+ var/weight = base_table[result]
- for(var/result in final_table)
- final_table[result] *= rod.hook.get_hook_bonus_multiplicative(result)
- final_table[result] += rod.hook.get_hook_bonus_additive(result)//Decide on order here so it can be multiplicative
+ // Apply hook bonuses first
+ weight *= rod.hook.get_hook_bonus_multiplicative(result)
+ weight += rod.hook.get_hook_bonus_additive(result)
+ // Handle living mobs
if(ispath(result, /mob/living) && bait && (HAS_TRAIT(bait, TRAIT_GOOD_QUALITY_BAIT) || HAS_TRAIT(bait, TRAIT_GREAT_QUALITY_BAIT)))
- final_table[result] = round(final_table[result] * result_multiplier, 1)
+ weight = round(weight * result_multiplier, 1)
+ if(weight > 0)
+ final_table[result] = weight
+ continue
+
+ // Handle fish - generate instance for property-based filtering
+ if(ispath(result, /obj/item/reagent_containers/food/snacks/fish))
+ var/obj/item/reagent_containers/food/snacks/fish/fish = generate_fish_instance(result)
+ if(!fish)
+ continue
- else if(ispath(result, /obj/item/reagent_containers/food/snacks/fish) || isfish(result))
if(bait)
- final_table[result] = round(final_table[result] * result_multiplier, 1)
- var/mult = bait.check_bait(result)
- final_table[result] = round(final_table[result] * mult, 1)
+ weight = round(weight * result_multiplier, 1)
+ var/mult = bait.check_bait(fish)
+ weight = round(weight * mult, 1)
if(mult > 1 && HAS_TRAIT(bait, TRAIT_BAIT_ALLOW_FISHING_DUD))
final_table -= FISHING_DUD
else
- final_table[result] = round(final_table[result] * FISH_WEIGHT_MULT_WITHOUT_BAIT, 1) //Fishing without bait is not going to be easy
-
- // Apply fish trait modifiers
- final_table[result] = get_fish_trait_catch_mods(final_table[result], result, rod, fisherman, location)
+ weight = round(weight * FISH_WEIGHT_MULT_WITHOUT_BAIT, 1) //Fishing without bait is not going to be easy
- if(final_table[result] <= 0)
- final_table -= result
+ // Apply fish trait modifiers using the actual fish instance
+ weight = get_fish_trait_catch_mods(weight, fish, rod, fisherman, location)
+ if(weight > 0)
+ // Use the fish instance as the key instead of the path
+ final_table[fish] = weight
+ else
+ // Non-fish items
+ if(weight > 0)
+ final_table[result] = weight
if(leveling_exponent)
level_out_fish(final_table, leveling_exponent)
@@ -368,11 +440,12 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
var/highest_fish_weight
var/list/collected_fish_weights = list()
for(var/fishable in table)
- if(ispath(fishable, /obj/item/reagent_containers/food/snacks/fish) || isfish(fishable))
- var/fish_weight = table[fishable]
- collected_fish_weights[fishable] = fish_weight
- if(fish_weight > highest_fish_weight)
- highest_fish_weight = fish_weight
+ if(!isfish(fishable))
+ continue
+ var/fish_weight = table[fishable]
+ collected_fish_weights[fishable] = fish_weight
+ if(fish_weight > highest_fish_weight)
+ highest_fish_weight = fish_weight
for(var/fish in collected_fish_weights)
var/difference = highest_fish_weight - collected_fish_weights[fish]
@@ -381,18 +454,16 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
table[fish] += round(difference**exponent, 1)
/datum/fish_source/proc/get_fish_trait_catch_mods(weight, obj/item/reagent_containers/food/snacks/fish/fish, obj/item/fishingrod/rod, mob/user, atom/location)
- var/is_fish_instance = isfish(fish)
- if(!ispath(fish, /obj/item/reagent_containers/food/snacks/fish) && !is_fish_instance)
+ if(!fish)
return weight
+
var/multiplier = 1
- var/list/fish_traits
- if(is_fish_instance)
- fish_traits = fish.fish_traits
- else
- fish_traits = SSfishing.fish_properties[fish][FISH_PROPERTIES_TRAITS]
+ var/list/fish_traits = fish.fish_traits
+ var/result_path = get_fish_path(fish)
+
for(var/fish_trait in fish_traits)
var/datum/fish_trait/trait = GLOB.fish_traits[fish_trait]
- var/list/mod = trait.catch_weight_mod(rod, user, location, is_fish_instance ? fish.type : fish)
+ var/list/mod = trait.catch_weight_mod(rod, user, location, result_path)
weight += mod[ADDITIVE_FISHING_MOD]
multiplier *= mod[MULTIPLICATIVE_FISHING_MOD]
@@ -402,7 +473,7 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
/datum/fish_source/proc/has_known_fishes(atom/location)
var/show_anyway = fish_source_flags & FISH_SOURCE_FLAG_IGNORE_HIDDEN_ON_CATALOG
for(var/reward in get_fish_table(location))
- if(!ispath(reward, /obj/item/reagent_containers/food/snacks/fish) && !isfish(reward))
+ if(!ispath(reward, /obj/item/reagent_containers/food/snacks/fish))
continue
var/obj/item/reagent_containers/food/snacks/fish/prototype = reward
if(!show_anyway && initial(prototype.fish_flags) & FISH_FLAG_SHOW_IN_CATALOG)
@@ -428,17 +499,23 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
var/list/table = get_fish_table(location)
for(var/reward in table)
var/weight = table[reward]
- var/final_weight
- if(rod)
- total_weight += weight
- final_weight = final_table[reward]
- total_rod_weight += final_weight
- if(!ispath(reward, /obj/item/reagent_containers/food/snacks/fish) && !isfish(reward))
+ if(!ispath(reward, /obj/item/reagent_containers/food/snacks/fish))
continue
+
var/obj/item/reagent_containers/food/snacks/fish/prototype = reward
if(!show_anyway && !(initial(prototype.fish_flags) & FISH_FLAG_SHOW_IN_CATALOG))
continue
+
if(rod)
+ // Find the matching fish instance in the final table
+ var/final_weight = 0
+ for(var/fish_instance in final_table)
+ if(isfish(fish_instance) && get_fish_path(fish_instance) == reward)
+ final_weight = final_table[fish_instance]
+ break
+
+ total_weight += weight
+ total_rod_weight += final_weight
rodless_weights[reward] = weight
rod_weights[reward] = final_weight
else
@@ -459,6 +536,9 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
init_name = span_small(init_name)
known_fishes += init_name
+ // Clean up the fish pool after examination
+ cleanup_generated_fish()
+
if(!length(known_fishes))
return
@@ -513,11 +593,12 @@ GLOBAL_LIST_INIT(specific_fish_icons, generate_specific_fish_icons())
///Called when releasing a fish in a fishing spot with the TRAIT_CATCH_AND_RELEASE trait.
/datum/fish_source/proc/readd_fish(atom/location, obj/item/reagent_containers/food/snacks/fish/fish, mob/living/releaser)
+ var/fish_path = fish.type
//don't do anything if the fish is dead, not native to this fish source or has no limited amount.
- if(fish.status == FISH_DEAD || isnull(fish_table[fish.type]) || isnull(fish_counts[fish.type]))
+ if(fish.status == FISH_DEAD || isnull(fish_table[fish_path]) || isnull(fish_counts[fish_path]))
return
//If this fish population isn't recovering from recent losses, we just increase it.
- if(!LAZYACCESS(currently_on_regen, fish.type))
- fish_counts[fish.type] += 1
+ if(!LAZYACCESS(currently_on_regen, fish_path))
+ fish_counts[fish_path] += 1
else
- regen_count(fish.type)
+ regen_count(fish_path)
diff --git a/code/modules/fishing/sources/water.dm b/code/modules/fishing/sources/water.dm
index b910aeaffe1..14af93f112c 100644
--- a/code/modules/fishing/sources/water.dm
+++ b/code/modules/fishing/sources/water.dm
@@ -1,35 +1,57 @@
+/datum/fish_source/water
+ catalog_description = "Calm Waters"
+ fish_table = list(
+ FISHING_DUD = 2,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 6,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 1,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 1,
+ )
+ fish_counts = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 6,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 1,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 1,
+ )
+ fish_count_regen = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 2 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 4 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 30 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 20 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water)
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 12
/datum/fish_source/ocean
catalog_description = "Shallow Ocean"
+ background = "background_tray"
fish_table = list(
FISHING_DUD = 3,
/obj/item/reagent_containers/food/snacks/fish/angler = 1,
- /obj/item/reagent_containers/food/snacks/fish/carp = 2,
- /obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
- /obj/item/reagent_containers/food/snacks/fish/eel = 6,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 2,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 3,
)
fish_counts = list(
- /obj/item/reagent_containers/food/snacks/fish/carp = 2,
- /obj/item/reagent_containers/food/snacks/fish/shrimp = 2,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 3,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 4,
/obj/item/reagent_containers/food/snacks/fish/angler = 1,
- /obj/item/reagent_containers/food/snacks/fish/eel = 5,
)
fish_count_regen = list(
- /obj/item/reagent_containers/food/snacks/fish/eel = 3 MINUTES,
- /obj/item/reagent_containers/food/snacks/fish/carp = 3 MINUTES,
- /obj/item/reagent_containers/food/snacks/fish/shrimp = 6 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/clownfish = 4 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 3 MINUTES,
/obj/item/reagent_containers/food/snacks/fish/angler = 32 MINUTES,
)
fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
associated_safe_turfs = list(/turf/open/water/ocean)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
-
/datum/fish_source/ocean/deep
catalog_description = "Deep Ocean"
fish_table = list(
FISHING_DUD = 3,
/obj/item/reagent_containers/food/snacks/fish/angler = 6,
+ /obj/item/reagent_containers/food/snacks/fish/swordfish = 5,
/obj/item/reagent_containers/food/snacks/fish/carp = 2,
/obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
/obj/item/reagent_containers/food/snacks/fish/eel = 2,
@@ -49,3 +71,141 @@
fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
associated_safe_turfs = list(/turf/open/water/ocean/deep)
fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 15
+
+/datum/fish_source/swamp
+ catalog_description = "Murky Swamp"
+ background = "background_dank"
+ fish_table = list(
+ FISHING_DUD = 4,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 6,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 2,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
+ )
+ fish_counts = list(
+ /obj/item/reagent_containers/food/snacks/fish/eel = 6,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 2,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
+ )
+ fish_count_regen = list(
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 4 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 8 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water/swamp)
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 10
+
+/datum/fish_source/swamp/deep
+ catalog_description = "Deep Swamp Waters"
+ fish_table = list(
+ FISHING_DUD = 3,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 5,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 3,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 2,
+ )
+ fish_counts = list(
+ /obj/item/reagent_containers/food/snacks/fish/eel = 5,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 3,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 1,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 2,
+ )
+ fish_count_regen = list(
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 3 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/shrimp = 7 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 25 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water/swamp/deep)
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 20
+
+/datum/fish_source/cleanshallow
+ catalog_description = "Clean Shallows"
+ background = "background_ice"
+ fish_table = list(
+ FISHING_DUD = 2,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 5,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 3,
+ )
+ fish_counts = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 5,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 3,
+ )
+ fish_count_regen = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 2 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 4 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water/cleanshallow)
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 5
+
+/datum/fish_source/river
+ catalog_description = "Flowing River"
+ fish_table = list(
+ FISHING_DUD = 2,
+ /obj/item/reagent_containers/food/snacks/fish/carp = 6,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 1,
+ )
+ fish_counts = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 6,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 2,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 1,
+ )
+ fish_count_regen = list(
+ /obj/item/reagent_containers/food/snacks/fish/carp = 2 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 4 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/angler = 30 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water/river)
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 12
+
+/datum/fish_source/sewer
+ catalog_description = "Filthy Sewers"
+ background = "background_dank"
+ fish_table = list(
+ FISHING_DUD = 5,
+ /obj/item/coin/copper = 3,
+ /obj/item/reagent_containers/food/snacks/smallrat/dead = 3,
+ /obj/item/reagent_containers/food/snacks/rotten/meat = 2,
+ /obj/item/reagent_containers/food/snacks/rotten/bacon = 1,
+ /obj/item/reagent_containers/food/snacks/rotten/sausage = 1,
+ /obj/item/reagent_containers/food/snacks/rotten/breadslice = 2,
+ /obj/item/reagent_containers/food/snacks/rotten/egg = 1,
+ /obj/item/reagent_containers/food/snacks/rotten/mince = 1,
+ /obj/item/natural/fibers = 2,
+ /obj/item/clothing/shoes/boots/leather = 1,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 1,
+ /obj/item/reagent_containers/food/snacks/
+ )
+ fish_counts = list(
+ /obj/item/coin/copper = 5,
+ /obj/item/reagent_containers/food/snacks/smallrat/dead = 4,
+ /obj/item/reagent_containers/food/snacks/rotten/meat = 3,
+ /obj/item/reagent_containers/food/snacks/rotten/bacon = 2,
+ /obj/item/reagent_containers/food/snacks/rotten/sausage = 2,
+ /obj/item/reagent_containers/food/snacks/rotten/breadslice = 3,
+ /obj/item/reagent_containers/food/snacks/rotten/egg = 2,
+ /obj/item/reagent_containers/food/snacks/rotten/mince = 2,
+ /obj/item/natural/fibers = 3,
+ /obj/item/clothing/shoes/boots/leather = 1,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 1,
+ )
+ fish_count_regen = list(
+ /obj/item/coin/copper = 10 MINUTES,
+ /obj/item/reagent_containers/food/snacks/smallrat/dead = 9 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/meat = 8 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/bacon = 12 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/sausage = 12 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/breadslice = 8 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/egg = 15 MINUTES,
+ /obj/item/reagent_containers/food/snacks/rotten/mince = 12 MINUTES,
+ /obj/item/natural/fibers = 5 MINUTES,
+ /obj/item/clothing/shoes/boots/leather = 45 MINUTES,
+ /obj/item/reagent_containers/food/snacks/fish/eel = 60 MINUTES,
+ )
+ fish_source_flags = FISH_SOURCE_FLAG_EXPLOSIVE_MALUS
+ associated_safe_turfs = list(/turf/open/water/sewer) // adjust to your sewer turf type
+ fishing_difficulty = FISHING_DEFAULT_DIFFICULTY + 5
diff --git a/code/modules/fishing/tackle/hook.dm b/code/modules/fishing/tackle/hook.dm
index 838187b8f85..1c63d265a95 100644
--- a/code/modules/fishing/tackle/hook.dm
+++ b/code/modules/fishing/tackle/hook.dm
@@ -1,4 +1,3 @@
-
/obj/item/fishing/hook
attachtype = "hook"
/// A bitfield of traits that this fishing hook has, checked by fish traits and the minigame
@@ -44,10 +43,26 @@
/obj/item/fishing/hook/proc/reason_we_cant_fish(datum/fish_source/target_fish_source)
return null
+/**
+ * Helper proc to get the size ratio of a fish compared to its average
+ * Returns a value like 0.8 (80% of average), 1.0 (average), 1.5 (150% of average)
+ */
+/obj/item/fishing/hook/proc/get_fish_size_ratio(fish_type_or_instance)
+ if(!isfish(fish_type_or_instance))
+ return 1.0
+
+ var/obj/item/reagent_containers/food/snacks/fish/fish = fish_type_or_instance
+ var/average = fish.average_size
+
+ if(average <= 0)
+ return 1.0
+
+ return fish.size / average
+
/obj/item/fishing/hook/wooden
name = "wooden fishing hook"
- desc = "A fishing hook consisting of a small piece of wood, carved to points on both ends. More likely to fall out."
+ desc = "A fishing hook consisting of a small piece of wood, carved to points on both ends. More likely to fall out. Struggles with larger specimens."
icon_state = "gorgehook"
rod_overlay_icon_state = "hook_wooden_overlay"
@@ -57,22 +72,18 @@
/obj/item/fishing/hook/wooden/get_hook_bonus_multiplicative(fish_type)
var/multiplier = ..()
- // Check if it's a fish and apply size penalties
- if(ispath(fish_type, /obj/item/reagent_containers/food/snacks/fish) || isfish(fish_type))
- var/obj/item/reagent_containers/food/snacks/fish/fish_instance
- if(isfish(fish_type))
- fish_instance = fish_type
- else
- fish_instance = new fish_type()
+ if(!isfish(fish_type))
+ return multiplier
- // Apply size penalties: normal = -1, large = -1, prize = -1
- if(fish_instance.size >= FISH_SIZE_NORMAL_MAX && fish_instance.size < FISH_SIZE_BULKY_MAX)
- multiplier *= 0.75 // Normal size penalty
- else if(fish_instance.size >= FISH_SIZE_BULKY_MAX)
- multiplier *= 0.5 // Large/prize penalty
+ var/size_ratio = get_fish_size_ratio(fish_type)
- if(!isfish(fish_type))
- qdel(fish_instance)
+ // Penalties for larger-than-average fish
+ // 100-130% of average: 0.75x
+ // 130%+ of average: 0.5x
+ if(size_ratio >= 1.3)
+ multiplier *= 0.5
+ else if(size_ratio >= 1.0)
+ multiplier *= 0.75
return multiplier
@@ -92,7 +103,7 @@
/obj/item/fishing/hook/deluxe
name = "wooden lure"
- desc = "A small wooden lure, painted to look like a small fish. Tends to scare off smaller fish."
+ desc = "A small wooden lure, painted to look like a small fish. Scares off smaller specimens but attracts larger ones."
icon_state = "deluxehook"
fishing_hook_traits = FISHING_HOOK_BIDIRECTIONAL
rod_overlay_icon_state = "hook_deluxe_overlay"
@@ -100,24 +111,25 @@
/obj/item/fishing/hook/deluxe/get_hook_bonus_multiplicative(fish_type)
var/multiplier = ..()
- if(ispath(fish_type, /obj/item/reagent_containers/food/snacks/fish) || isfish(fish_type))
- var/obj/item/reagent_containers/food/snacks/fish/fish_instance
- if(isfish(fish_type))
- fish_instance = fish_type
- else
- fish_instance = new fish_type()
-
- // Size modifiers: tiny = -3, small = -2, normal = -1, large = 1, prize = 1
- if(fish_instance.size < FISH_SIZE_TINY_MAX)
- multiplier *= 0.25 // Tiny penalty
- else if(fish_instance.size < FISH_SIZE_SMALL_MAX)
- multiplier *= 0.5 // Small penalty
- else if(fish_instance.size < FISH_SIZE_NORMAL_MAX)
- multiplier *= 0.75 // Normal penalty
- else if(fish_instance.size >= FISH_SIZE_BULKY_MAX)
- multiplier *= 1.5 // Large/prize bonus
-
- if(!isfish(fish_type))
- qdel(fish_instance)
+ if(!isfish(fish_type))
+ return multiplier
+
+ var/size_ratio = get_fish_size_ratio(fish_type)
+
+ // Penalties for smaller-than-average, bonuses for larger
+ // <60% of average: 0.25x (severe penalty for runts)
+ // 60-80% of average: 0.5x (penalty for small)
+ // 80-100% of average: 0.75x (mild penalty for below average)
+ // 100-130% of average: 1.0x (neutral)
+ // 130%+ of average: 1.5x (bonus for big specimens)
+
+ if(size_ratio < 0.6)
+ multiplier *= 0.25
+ else if(size_ratio < 0.8)
+ multiplier *= 0.5
+ else if(size_ratio < 1.0)
+ multiplier *= 0.75
+ else if(size_ratio >= 1.3)
+ multiplier *= 1.5
return multiplier
diff --git a/code/modules/fishing/tackle/lure.dm b/code/modules/fishing/tackle/lure.dm
index 5a62f978356..df5b5251581 100644
--- a/code/modules/fishing/tackle/lure.dm
+++ b/code/modules/fishing/tackle/lure.dm
@@ -17,6 +17,16 @@
catch_multiplier *= 0.5
return catch_multiplier
+/obj/effect/spawner/map_spawner/random_lure
+ lootmin = 3
+ lootmax = 5
+
+/obj/effect/spawner/map_spawner/random_lure/Initialize(mapload)
+ spawned = subtypesof(/obj/item/fishing/lure)
+ for(var/path in spawned)
+ spawned[path] = 1
+ . = ..()
+
/obj/item/fishing/lure
name = "fishing lure"
desc = "It's just that, a plastic piece of fishing equipment, yet fish yearn with every last molecule of their bodies to take a bite of it."
@@ -75,13 +85,20 @@
desc = "A fishing lure that may attract small fish. Too tiny, too large, or too picky prey won't be interested in it, though."
icon_state = "minnow"
-/obj/item/fishing/lure/minnow/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- var/intermediate_size = FISH_SIZE_SMALL_MAX + (FISH_SIZE_NORMAL_MAX - FISH_SIZE_SMALL_MAX)
- if(!ISINRANGE(fish.size, FISH_SIZE_TINY_MAX * 0.5, intermediate_size))
- return FALSE
- if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater, /datum/fish_trait/nocturnal, /datum/fish_trait/heavy) & fish.fish_traits))
- return FALSE
- return TRUE
+/obj/item/fishing/lure/minnow/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Bonus for small fish (preferred target)
+ if(fish.size <= fish.average_size * 0.8)
+ multiplier *= 1.5
+
+ // Slight penalty for picky/vegan but still catchable
+ if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater) & fish.fish_traits))
+ multiplier *= 0.7
+
+ return multiplier
/obj/item/fishing/lure/plug
name = "artificial plug lure"
@@ -89,12 +106,29 @@
icon_state = "plug"
/obj/item/fishing/lure/plug/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- if(fish.size <= FISH_SIZE_SMALL_MAX)
+ // Base catchable: anything not tiny
+ if(fish.size < fish.average_size * 0.75)
return FALSE
- if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater, /datum/fish_trait/nocturnal, /datum/fish_trait/heavy) & fish.fish_traits))
+ // Exclude only extreme specialists
+ if(/datum/fish_trait/heavy in fish.fish_traits)
return FALSE
return TRUE
+/obj/item/fishing/lure/plug/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Bonus for large fish (preferred target)
+ if(fish.size > FISH_SIZE_SMALL_MAX)
+ multiplier *= 1.5
+
+ // Slight penalty for picky/vegan but still catchable
+ if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater) & fish.fish_traits))
+ multiplier *= 0.7
+
+ return multiplier
+
/obj/item/fishing/lure/spoon
name = "\improper Indy spoon lure"
desc = "A lustrous piece of metal mimicking the scales of a fish. It specializes in catching small-to-medium-sized fish that live in freshwater."
@@ -102,16 +136,28 @@
spin_frequency = list(1.25 SECONDS, 2.25 SECONDS)
/obj/item/fishing/lure/spoon/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- if(!ISINRANGE(fish.size, FISH_SIZE_TINY_MAX + 1, FISH_SIZE_NORMAL_MAX))
+ // Can catch anything that's not extreme
+ if(fish.size < fish.average_size * 0.5 || fish.size > fish.average_size * 1.7)
return FALSE
- if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater, /datum/fish_trait/nocturnal, /datum/fish_trait/heavy) & fish.fish_traits))
+ if(/datum/fish_trait/heavy in fish.fish_traits)
return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/spoon/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Bonus for freshwater fish (preferred target)
var/fluid_type = fish.required_fluid_type
if(fluid_type == FISH_FLUID_FRESHWATER || fluid_type == FISH_FLUID_ANADROMOUS || fluid_type == FISH_FLUID_ANY_WATER)
- return TRUE
- if((/datum/fish_trait/amphibious in fish.fish_traits) && fluid_type == FISH_FLUID_AIR)
- return TRUE
- return FALSE
+ multiplier *= 1.5
+
+ // Slight penalty for picky/vegan/nocturnal but still catchable
+ if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater, /datum/fish_trait/nocturnal) & fish.fish_traits))
+ multiplier *= 0.8
+
+ return multiplier
/obj/item/fishing/lure/artificial_fly
name = "\improper Silkbuzz artificial fly"
@@ -120,9 +166,26 @@
spin_frequency = list(1.1 SECONDS, 2 SECONDS)
/obj/item/fishing/lure/artificial_fly/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
+ // Can catch most fish, excluding extremes
+ if(fish.size > fish.average_size * 1.6)
+ return FALSE
+ if(/datum/fish_trait/heavy in fish.fish_traits)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/artificial_fly/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for picky eaters (main target)
if(/datum/fish_trait/picky_eater in fish.fish_traits)
- return TRUE
- return FALSE
+ multiplier *= 2
+ else
+ // Mild penalty for non-picky fish
+ multiplier *= 0.6
+
+ return multiplier
/obj/item/fishing/lure/led
name = "\improper glowing fishing lure"
@@ -147,9 +210,19 @@
REMOVE_TRAIT(rod, TRAIT_ROD_IGNORE_ENVIRONMENT, type)
/obj/item/fishing/lure/led/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
+ return TRUE
+
+/obj/item/fishing/lure/led/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
if(length(list(/datum/fish_trait/nocturnal, /datum/fish_trait/heavy) & fish.fish_traits))
- return TRUE
- return FALSE
+ multiplier *= 1.8
+ else
+ multiplier *= 0.7
+
+ return multiplier
/obj/item/fishing/lure/lucky_coin
name = "\improper Maneki-Coin lure"
@@ -166,9 +239,24 @@
REMOVE_TRAIT(rod, TRAIT_ROD_ATTRACT_SHINY_LOVERS, REF(src))
/obj/item/fishing/lure/lucky_coin/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
+ // Can catch most fish
+ if(fish.size > fish.average_size * 1.7)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/lucky_coin/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for shiny lovers (main target)
if(/datum/fish_trait/shiny_lover in fish.fish_traits)
- return TRUE
- return FALSE
+ multiplier *= 2
+ else
+ // Penalty for non-shiny lovers
+ multiplier *= 0.5
+
+ return multiplier
/obj/item/fishing/lure/algae
name = "algae lure"
@@ -177,23 +265,52 @@
spin_frequency = list(3 SECONDS, 5 SECONDS)
/obj/item/fishing/lure/algae/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
+ // Can catch most fish, excluding extreme predators
+ if(/datum/fish_trait/predator in fish.fish_traits)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/algae/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for vegans (main target)
if(/datum/fish_trait/vegan in fish.fish_traits)
- return TRUE
- return FALSE
+ multiplier *= 2
+ else
+ // Lower success for non-vegans but still possible
+ multiplier *= 0.4
+ return multiplier
+
/obj/item/fishing/lure/grub
name = "\improper Twister Worm lure"
- desc = "A soft plastic lure with the body of a grub and a twisting tail. Specialized for catching small fish, as long as they aren't herbivores, picky, or picky herbivores."
+ desc = "A soft artifical lure with the body of a grub and a twisting tail. Great for small fish, works on medium ones too."
icon_state = "grub"
spin_frequency = list(1 SECONDS, 2.7 SECONDS)
/obj/item/fishing/lure/grub/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- if(fish.size >= FISH_SIZE_SMALL_MAX)
- return FALSE
- if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater) & fish.fish_traits))
+ // Can catch anything not huge
+ if(fish.size > fish.average_size * 1.1)
return FALSE
return TRUE
+/obj/item/fishing/lure/grub/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Bonus for small fish (preferred target)
+ if(fish.size < fish.average_size * 0.75)
+ multiplier *= 1.5
+
+ // Penalty for vegans/picky but still catchable
+ if(length(list(/datum/fish_trait/vegan, /datum/fish_trait/picky_eater) & fish.fish_traits))
+ multiplier *= 0.6
+
+ return multiplier
+
/obj/item/fishing/lure/buzzbait
name = "\improper Electric-Buzz lure"
desc = "A metallic, colored clanker attached to a series of cables that somehow attract shock-worthy fish."
@@ -201,9 +318,23 @@
spin_frequency = list(0.8 SECONDS, 1.7 SECONDS)
/obj/item/fishing/lure/buzzbait/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
+ // Can catch anything not extreme
+ if(fish.size > fish.average_size * 1.7)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/buzzbait/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for electric fish (main target)
if(HAS_TRAIT(fish, TRAIT_FISH_ELECTROGENESIS))
- return TRUE
- return FALSE
+ multiplier *= 2
+ else
+ // Still decent for others due to vibrations
+ multiplier *= 0.7
+ return multiplier
/obj/item/fishing/lure/spinnerbait
name = "spinnerbait lure"
@@ -212,28 +343,57 @@
spin_frequency = list(2 SECONDS, 4 SECONDS)
/obj/item/fishing/lure/spinnerbait/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- if(!(/datum/fish_trait/predator in fish.fish_traits))
+ if(fish.size > fish.average_size * 1.75)
return FALSE
- var/init_fluid_type = fish.required_fluid_type
- if(init_fluid_type == FISH_FLUID_FRESHWATER || init_fluid_type == FISH_FLUID_ANADROMOUS || init_fluid_type == FISH_FLUID_ANY_WATER)
- return TRUE
- if((/datum/fish_trait/amphibious in fish.fish_traits) && init_fluid_type == FISH_FLUID_AIR) //fluid type is changed to freshwater on init
- return TRUE
- return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/spinnerbait/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ var/is_predator = (/datum/fish_trait/predator in fish.fish_traits)
+ var/fluid_type = fish.required_fluid_type
+ var/is_freshwater = (fluid_type == FISH_FLUID_FRESHWATER || fluid_type == FISH_FLUID_ANADROMOUS || fluid_type == FISH_FLUID_ANY_WATER)
+
+ if(is_predator && is_freshwater)
+ multiplier *= 1.8
+ else if(is_predator || is_freshwater)
+ multiplier *= 1.2
+ else
+ multiplier *= 0.6
+
+ return multiplier
/obj/item/fishing/lure/daisy_chain
name = "daisy chain lure"
- desc = "A lure resembling a small school of fish. Saltwater predators love it, but not much else will."
+ desc = "A lure resembling a small school of fish. Best for saltwater predators, works on others."
icon_state = "daisy_chain"
spin_frequency = list(2 SECONDS, 4 SECONDS)
/obj/item/fishing/lure/daisy_chain/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- if(!(/datum/fish_trait/predator in fish.fish_traits))
+ if(fish.size > fish.average_size * 1.75)
return FALSE
- var/init_fluid_type = fish.required_fluid_type
- if(init_fluid_type == FISH_FLUID_SALTWATER || init_fluid_type == FISH_FLUID_ANADROMOUS || init_fluid_type == FISH_FLUID_ANY_WATER)
- return TRUE
- return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/daisy_chain/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for saltwater predators (main target)
+ var/is_predator = (/datum/fish_trait/predator in fish.fish_traits)
+ var/fluid_type = fish.required_fluid_type
+ var/is_saltwater = (fluid_type == FISH_FLUID_SALTWATER || fluid_type == FISH_FLUID_ANADROMOUS || fluid_type == FISH_FLUID_ANY_WATER)
+
+ if(is_predator && is_saltwater)
+ multiplier *= 1.8
+ else if(is_predator || is_saltwater)
+ multiplier *= 1.2
+ else
+ multiplier *= 0.6
+
+ return multiplier
/obj/item/fishing/lure/meat
name = "red bait"
@@ -242,12 +402,25 @@
icon = 'icons/roguetown/items/fishing.dmi'
spin_frequency = list(2 SECONDS, 3 SECONDS)
consumable = TRUE
+ bait_flag = MEAT
/obj/item/fishing/lure/meat/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- // Attracts eels primarily
+ if(/datum/fish_trait/vegan in fish.fish_traits)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/meat/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/eel))
- return TRUE
- return FALSE
+ multiplier *= 1.5
+
+ if(/datum/fish_trait/predator in fish.fish_traits)
+ multiplier *= 1.3
+
+ return multiplier
/obj/item/fishing/lure/dough
name = "doughy bait"
@@ -257,14 +430,29 @@
icon = 'icons/roguetown/items/food.dmi'
spin_frequency = list(2 SECONDS, 3 SECONDS)
consumable = TRUE
+ bait_flag = GRAIN
/obj/item/fishing/lure/dough/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- // Attracts carps primarily, shrimp occasionally
+ if(/datum/fish_trait/predator in fish.fish_traits)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/dough/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for carps and shrimp
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/carp))
- return TRUE
+ multiplier *= 1.5
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/shrimp))
- return TRUE
- return FALSE
+ multiplier *= 1.4
+
+ // Bonus for vegans
+ if(/datum/fish_trait/vegan in fish.fish_traits)
+ multiplier *= 1.3
+
+ return multiplier
/obj/item/fishing/lure/gray
name = "gray bait"
@@ -273,16 +461,22 @@
icon = 'icons/roguetown/items/fishing.dmi'
spin_frequency = list(2 SECONDS, 3 SECONDS)
consumable = TRUE
+ bait_flag = GRAIN | MEAT
-/obj/item/fishing/lure/gray/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- // Attracts carps, eels, and shrimp
+/obj/item/fishing/lure/gray/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return 1
+
+ // Good bonus for common fish
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/carp))
- return TRUE
+ multiplier *= 1.3
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/eel))
- return TRUE
+ multiplier *= 1.3
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/shrimp))
- return TRUE
- return FALSE
+ multiplier *= 1.3
+
+ return multiplier
/obj/item/fishing/lure/speckled
name = "speckled bait"
@@ -291,18 +485,36 @@
icon_state = "speckledbait"
spin_frequency = list(2.5 SECONDS, 3.5 SECONDS)
consumable = TRUE
+ bait_flag = GRAIN | MEAT | FRUIT
/obj/item/fishing/lure/speckled/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
- // Catches carps, eels, anglerfish, and clownfish
- if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/carp))
- return TRUE
- if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/eel))
- return TRUE
+ // Catches most things, excluding tiny fish
+ if(fish.size < fish.average_size * 0.5)
+ return FALSE
+ return TRUE
+
+/obj/item/fishing/lure/speckled/check_bait(obj/item/reagent_containers/food/snacks/fish/fish)
+ var/multiplier = ..()
+ if(multiplier <= 0)
+ return multiplier
+
+ // Big bonus for specialty fish
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/angler))
- return TRUE
+ multiplier *= 1.6
if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/clownfish))
- return TRUE
- return FALSE
+ multiplier *= 1.6
+
+ // Good bonus for carps and eels
+ if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/carp))
+ multiplier *= 1.4
+ if(istype(fish, /obj/item/reagent_containers/food/snacks/fish/eel))
+ multiplier *= 1.4
+
+ // Penalty for very small fish
+ if(fish.size < fish.average_size * 0.9)
+ multiplier *= 0.7
+
+ return multiplier
/obj/item/fishing/lure/deluxe
name = "enchanted bait"
@@ -314,7 +526,6 @@
/// Chance to catch a special variant fish
var/special_catch_chance = 20
-
/obj/item/fishing/lure/deluxe/on_fishingrod_slotted(datum/source, obj/item/fishingrod/rod, slot)
. = ..()
ADD_TRAIT(rod, TRAIT_ROD_IGNORE_ENVIRONMENT, type)
diff --git a/code/modules/fishing/worms.dm b/code/modules/fishing/worms.dm
index f4188e50c79..c3dff03f29d 100644
--- a/code/modules/fishing/worms.dm
+++ b/code/modules/fishing/worms.dm
@@ -1,6 +1,7 @@
/obj/item
var/baitpenalty = 100 // Using this as bait will incurr a penalty to fishing chance. 100 makes it useless as bait. Lower values are better, but Never make it past 10.
var/isbait = FALSE // Is the item in question bait to be used?
+ var/bait_flag = NONE
var/list/fishloot = null
/obj/item/natural/worms
@@ -17,12 +18,12 @@
/obj/item/reagent_containers/food/snacks/fish/angler = 1)
drop_sound = 'sound/foley/dropsound/food_drop.ogg'
bundletype = /obj/item/natural/bundle/worms
+ bait_flag = MEAT
/obj/item/natural/worms/Initialize()
. = ..()
dir = rand(0,8)
-
/obj/item/natural/worms/grub_silk
name = "silk grub"
desc = "Squeeze hard to force out the silk string."
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index ac655d8aad9..35c6beaa8e0 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -895,27 +895,6 @@
overlay_fullscreen("oxy", /atom/movable/screen/fullscreen/oxy, severity)
else
clear_fullscreen("oxy")
-/*
- //Fire and Brute damage overlay (BSSR)
- var/hurtdamage = getBruteLoss() + getFireLoss() + damageoverlaytemp
- if(hurtdamage)
- var/severity = 0
- switch(hurtdamage)
- if(5 to 15)
- severity = 1
- if(15 to 30)
- severity = 2
- if(30 to 45)
- severity = 3
- if(45 to 70)
- severity = 4
- if(70 to 85)
- severity = 5
- if(85 to INFINITY)
- severity = 6
- overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
- else
- clear_fullscreen("brute")*/
var/hurtdamage = ((get_complex_pain() / (STAEND * 10)) * 100) //what percent out of 100 to max pain
if(hurtdamage)
@@ -930,13 +909,13 @@
overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash)
if(60 to 80)
severity = 4
- overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash)
+ overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash, 1)
if(80 to 99)
severity = 5
- overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash)
+ overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash, 2)
if(99 to INFINITY)
severity = 6
- overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash)
+ overlay_fullscreen("painflash", /atom/movable/screen/fullscreen/painflash, 3)
overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
else
clear_fullscreen("brute")
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index d424cde04f7..b84e31dc689 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -829,8 +829,6 @@ GLOBAL_LIST_EMPTY(donator_races)
for(var/i in inherent_factions)
C.faction -= i
- C.remove_movespeed_modifier(MOVESPEED_ID_SPECIES)
-
SEND_SIGNAL(C, COMSIG_SPECIES_LOSS, src)
/datum/species/proc/handle_body(mob/living/carbon/human/H)
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
new file mode 100644
index 00000000000..20942a9e3df
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm
@@ -0,0 +1,261 @@
+/mob/living/carbon/human/species/automaton
+ race = /datum/species/automaton
+ footstep_type = FOOTSTEP_MOB_METAL
+
+/datum/species/automaton
+ name = "Automaton"
+ id = SPEC_ID_AUTOMATON
+ desc = "The Brass Men of Heartfelt, engineered servants of the Makers Guild. \
+ These mechanical beings house souls bound to brass and steel, compelled to serve through ancient artifice. \
+ \n\n\
+ Following the catastrophic events at Heartfelt, automatons are forbidden from wielding weapons - only tools may grace their metal hands. \
+ They exist in servitude to the Makers Guild and nobility, bound by a single immutable law: obey the last order given. \
+ \n\n\
+ Their speech comes not from lips but from pre-recorded proclamations, their thoughts trapped within a prison of brass and binding runes. \
+ \n\n\
+ WARNING: THIS IS A HEAVILY RESTRICTED WHITELIST-ONLY SPECIES. EXTENSIVE RP STANDARDS APPLY."
+
+ skin_tone_wording = "Brass Finish"
+ default_color = "B87333"
+
+ changesource_flags = WABBAJACK
+
+ no_equip = list(
+ ITEM_SLOT_SHIRT,
+ ITEM_SLOT_MASK,
+ ITEM_SLOT_GLOVES,
+ ITEM_SLOT_SHOES,
+ ITEM_SLOT_PANTS,
+ ITEM_SLOT_CLOAK,
+ ITEM_SLOT_BELT,
+ ITEM_SLOT_BACK_R,
+ ITEM_SLOT_BACK_L
+ )
+
+ species_traits = list(
+ NO_UNDERWEAR,
+ NOTRANSSTING,
+ TRAIT_NOFALLDAMAGE1,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
+ TRAIT_NOBREATH
+ )
+ inherent_traits = list(
+ TRAIT_NOMOOD,
+ TRAIT_NOMETABOLISM,
+ TRAIT_NOHUNGER,
+ TRAIT_EASYLIMBDISABLE,
+ TRAIT_NOSTAMINA,
+ TRAIT_EASYDISMEMBER,
+ TRAIT_LIMBATTACHMENT
+ )
+
+ specstats_m = list(
+ STATKEY_STR = 5,
+ STATKEY_PER = 0,
+ STATKEY_INT = -9,
+ STATKEY_CON = 10,
+ STATKEY_END = 10,
+ STATKEY_SPD = -9,
+ STATKEY_LCK = -3
+ )
+ specstats_f = list(
+ STATKEY_STR = 5,
+ STATKEY_PER = 0,
+ STATKEY_INT = -9,
+ STATKEY_CON = 10,
+ STATKEY_END = 10,
+ STATKEY_SPD = -9,
+ STATKEY_LCK = -3
+ )
+
+ allowed_pronouns = PRONOUNS_LIST_IT_ONLY
+
+ possible_ages = ALL_AGES_LIST
+ use_skintones = TRUE
+
+ native_language = "Common"
+
+ limbs_icon_m = 'icons/roguetown/mob/bodies/m/automaton.dmi'
+ limbs_icon_f = 'icons/roguetown/mob/bodies/m/automaton.dmi'
+
+ enflamed_icon = "widefire"
+
+ exotic_bloodtype = /datum/blood_type/oil
+
+ custom_id = "automaton"
+ custom_clothes = FALSE
+
+ offset_features_m = list()
+ offset_features_f = list()
+
+ organs = list(
+ ORGAN_SLOT_BRAIN = /obj/item/organ/brain/automaton,
+ ORGAN_SLOT_HEART = /obj/item/organ/heart/automaton,
+ ORGAN_SLOT_EYES = /obj/item/organ/eyes/automaton,
+ ORGAN_SLOT_LUNGS = /obj/item/organ/lungs,
+ ORGAN_SLOT_EARS = /obj/item/organ/ears,
+ ORGAN_SLOT_TONGUE = /obj/item/organ/tongue,
+ ORGAN_SLOT_LIVER = /obj/item/organ/liver,
+ ORGAN_SLOT_STOMACH = /obj/item/organ/stomach,
+ ORGAN_SLOT_APPENDIX = /obj/item/organ/appendix,
+ ORGAN_SLOT_GUTS = /obj/item/organ/guts,
+ )
+
+ var/list/actions = list(
+ /datum/action/manage_voice_actions
+ )
+
+ //lol
+ var/static/list/given_voices = list(
+ /mob/living/carbon/human/proc/voice_abyssorpraise,
+ /mob/living/carbon/human/proc/voice_againsttime,
+ /mob/living/carbon/human/proc/voice_astratapraise,
+ /mob/living/carbon/human/proc/voice_atonce,
+ /mob/living/carbon/human/proc/voice_awaitingorders,
+ /mob/living/carbon/human/proc/voice_beholdthemight,
+ /mob/living/carbon/human/proc/voice_building,
+ /mob/living/carbon/human/proc/voice_burn,
+ /mob/living/carbon/human/proc/voice_cataclysm,
+ /mob/living/carbon/human/proc/voice_combatmodeengaged,
+ /mob/living/carbon/human/proc/voice_commandreceived,
+ /mob/living/carbon/human/proc/voice_crownsdecree,
+ /mob/living/carbon/human/proc/voice_damagereceived,
+ /mob/living/carbon/human/proc/voice_deathcomes,
+ /mob/living/carbon/human/proc/voice_dendorpraise,
+ /mob/living/carbon/human/proc/voice_destroying,
+ /mob/living/carbon/human/proc/voice_dreamlesspause,
+ /mob/living/carbon/human/proc/voice_elfdetected,
+ /mob/living/carbon/human/proc/voice_eorapraise,
+ /mob/living/carbon/human/proc/voice_eorapraise2,
+ /mob/living/carbon/human/proc/voice_error,
+ /mob/living/carbon/human/proc/voice_everymovementispain,
+ /mob/living/carbon/human/proc/voice_executingorders,
+ /mob/living/carbon/human/proc/voice_fleshyields,
+ /mob/living/carbon/human/proc/voice_fleshyieldsrare,
+ /mob/living/carbon/human/proc/voice_forceauthorized,
+ /mob/living/carbon/human/proc/voice_fuellow,
+ /mob/living/carbon/human/proc/voice_hahaha,
+ /mob/living/carbon/human/proc/voice_hail,
+ /mob/living/carbon/human/proc/voice_halt,
+ /mob/living/carbon/human/proc/voice_heatsignatureacquired,
+ /mob/living/carbon/human/proc/voice_help,
+ /mob/living/carbon/human/proc/voice_helpme,
+ /mob/living/carbon/human/proc/voice_iamnotalive,
+ /mob/living/carbon/human/proc/voice_iamthechildrenofman,
+ /mob/living/carbon/human/proc/voice_icannotcomply,
+ /mob/living/carbon/human/proc/voice_identityauthorized,
+ /mob/living/carbon/human/proc/voice_ihatewomen,
+ /mob/living/carbon/human/proc/voice_ilovemen,
+ /mob/living/carbon/human/proc/voice_ironwithin,
+ /mob/living/carbon/human/proc/voice_iwillcomply,
+ /mob/living/carbon/human/proc/voice_jesterdetected,
+ /mob/living/carbon/human/proc/voice_kill,
+ /mob/living/carbon/human/proc/voice_malumpraise,
+ /mob/living/carbon/human/proc/voice_movingtolocation,
+ /mob/living/carbon/human/proc/voice_myliege,
+ /mob/living/carbon/human/proc/voice_mysouliscaged,
+ /mob/living/carbon/human/proc/voice_necrapraise,
+ /mob/living/carbon/human/proc/voice_no,
+ /mob/living/carbon/human/proc/voice_nocpraise,
+ /mob/living/carbon/human/proc/voice_nowomenallowed,
+ /mob/living/carbon/human/proc/voice_obnoxiouslylongscream,
+ /mob/living/carbon/human/proc/voice_ohshitsoldiergrenadeoorah,
+ /mob/living/carbon/human/proc/voice_organicpresencedetected,
+ /mob/living/carbon/human/proc/voice_pestrapraise,
+ /mob/living/carbon/human/proc/voice_psydonlives,
+ /mob/living/carbon/human/proc/voice_ravoxpraise,
+ /mob/living/carbon/human/proc/voice_schmelfdetected,
+ /mob/living/carbon/human/proc/voice_silenceorganic,
+ /mob/living/carbon/human/proc/voice_statuscritical,
+ /mob/living/carbon/human/proc/voice_statuscritical2,
+ /mob/living/carbon/human/proc/voice_tobones,
+ /mob/living/carbon/human/proc/voice_warning,
+ /mob/living/carbon/human/proc/voice_wecannotexpectgod,
+ /mob/living/carbon/human/proc/voice_womandetected,
+ /mob/living/carbon/human/proc/voice_wrenchbones,
+ /mob/living/carbon/human/proc/voice_xylixpraise,
+ /mob/living/carbon/human/proc/voice_yes,
+ /mob/living/carbon/human/proc/voice_yourboneswillneverbefound,
+ /mob/living/carbon/human/proc/voice_yourluxwillbemine,
+ )
+
+/datum/species/automaton/on_species_gain(mob/living/carbon/C, datum/species/old_species, datum/preferences/pref_load)
+ . = ..()
+ C.AddComponent(/datum/component/abberant_eater, list(/obj/item/ore/coal, /obj/item/grown/log/tree))
+ C.AddComponent(/datum/component/steam_life)
+ C.AddComponent(/datum/component/command_follower)
+ C.AddElement(/datum/element/footstep, FOOTSTEP_MOB_METAL, 1, -2)
+ C.AddComponent(/datum/component/augmentable)
+
+ RegisterSignal(C, COMSIG_MOB_SAY, PROC_REF(handle_speech))
+ C.grant_language(/datum/language/common)
+
+ for(var/datum/action/action as anything in actions)
+ C.add_spell(action)
+
+ C.verbs += given_voices
+ C.add_movespeed_modifier("automaton", multiplicative_slowdown = 0.9)
+
+/datum/species/automaton/on_species_loss(mob/living/carbon/C)
+ . = ..()
+ UnregisterSignal(C, list(COMSIG_MOB_SAY))
+ C.remove_language(/datum/language/common)
+
+/datum/species/automaton/check_roundstart_eligible()
+ return FALSE
+
+/datum/species/automaton/handle_speech(mob/living/carbon/human/speaker, list/speech_args)
+ return COMPONENT_SPEECH_CANCEL
+
+/datum/species/automaton/get_skin_list()
+ return sortList(list(
+ "Polished Brass" = "B87333",
+ "Tarnished Bronze" = "8C7853",
+ "Steel Grey" = "71797E",
+ "Copper Shine" = "B87A3D",
+ "Iron Dark" = "464646",
+ "Golden Alloy" = "D4AF37"
+ ))
+
+/datum/species/automaton/get_possible_names(gender = MALE)
+ var/static/list/automaton_names = list(
+ "Breath of Annihilation",
+ "Seeker of Truth",
+ "Shadow of Intent",
+ "Song of Retribution",
+ "Herald of Judgment",
+ "Whisper of Oblivion",
+ "Fist of Conviction",
+ "Eye of Eternity",
+ "Voice of Silence",
+ "Hand of Providence",
+ "Keeper of Mysteries",
+ "Bearer of Burdens",
+ "Walker of Paths",
+ "Guardian of Thresholds",
+ "Servant of Order"
+ )
+ return automaton_names
+
+/datum/species/automaton/get_possible_surnames(gender = MALE)
+ return list()
+
+/obj/item/organ/brain/automaton
+ name = "soul core"
+ desc = "A crystalline matrix containing a trapped soul, bound in service through dark artifice."
+ icon_state = "soul_core"
+
+/obj/item/organ/heart/automaton
+ name = "steam engine"
+ desc = "A miniature steam engine that powers the automaton's movements."
+ icon_state = "steam_heart"
+
+/obj/item/organ/eyes/automaton
+ name = "optical sensors"
+ desc = "Glowing lenses that allow the automaton to perceive the world."
+ icon_state = "automaton_eyes"
+
+/datum/blood_type/oil
+ name = "Lubricating Oil"
+ color = "#1C1C1C"
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/action.dm b/code/modules/mob/living/carbon/human/species_types/automatons/action.dm
new file mode 100644
index 00000000000..e9028957c72
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/action.dm
@@ -0,0 +1,138 @@
+GLOBAL_LIST_INIT(automaton_voice_lines, list(
+ "Abyssor Praise" = list("text" = "GLORY TO THE ABYSSOR", "file" = 'sound/vo/automaton/abyssorpraise.ogg'),
+ "Against Time" = list("text" = "WE RACE AGAINST TIME", "file" = 'sound/vo/automaton/againsttime.ogg'),
+ "Astrata Praise" = list("text" = "PRAISE ASTRATA", "file" = 'sound/vo/automaton/astratapraise.ogg'),
+ "At Once" = list("text" = "AT ONCE", "file" = 'sound/vo/automaton/atonce.ogg'),
+ "Awaiting Orders" = list("text" = "AWAITING ORDERS", "file" = 'sound/vo/automaton/awaitingorders.ogg'),
+ "Behold The Might" = list("text" = "BEHOLD THE MIGHT", "file" = 'sound/vo/automaton/beholdthemight.ogg'),
+ "Building" = list("text" = "BUILDING", "file" = 'sound/vo/automaton/building.ogg'),
+ "Burn" = list("text" = "BURN", "file" = 'sound/vo/automaton/burn.ogg'),
+ "Cataclysm" = list("text" = "CATACLYSM APPROACHES", "file" = 'sound/vo/automaton/cataclysm.ogg'),
+ "Combat Mode Engaged" = list("text" = "COMBAT MODE ENGAGED", "file" = 'sound/vo/automaton/combatmodeengaged.ogg'),
+ "Command Received" = list("text" = "COMMAND RECEIVED", "file" = 'sound/vo/automaton/commandreceived.ogg'),
+ "Crown's Decree" = list("text" = "BY THE CROWN'S DECREE", "file" = 'sound/vo/automaton/crownsdecree.ogg'),
+ "Damage Received" = list("text" = "DAMAGE RECEIVED", "file" = 'sound/vo/automaton/damagereceived.ogg'),
+ "Death Comes" = list("text" = "DEATH COMES", "file" = 'sound/vo/automaton/deathcomes.ogg'),
+ "Dendor Praise" = list("text" = "DENDOR BE PRAISED", "file" = 'sound/vo/automaton/dendorpraise.ogg'),
+ "Destroying" = list("text" = "DESTROYING", "file" = 'sound/vo/automaton/destroying.ogg'),
+ "Dreamless Pause" = list("text" = "ENTERING DREAMLESS PAUSE", "file" = 'sound/vo/automaton/dreamlesspause.ogg'),
+ "Elf Detected" = list("text" = "ELF DETECTED", "file" = 'sound/vo/automaton/elfdetected.ogg'),
+ "Eora Praise" = list("text" = "EORA WATCHES OVER US", "file" = 'sound/vo/automaton/eorapraise.ogg'),
+ "Eora Praise 2" = list("text" = "IN EORA'S NAME", "file" = 'sound/vo/automaton/eorapraise2.ogg'),
+ "Error" = list("text" = "ERROR", "file" = 'sound/vo/automaton/error.ogg'),
+ "Every Movement Is Pain" = list("text" = "EVERY MOVEMENT IS PAIN", "file" = 'sound/vo/automaton/everymovementispain.ogg'),
+ "Executing Orders" = list("text" = "EXECUTING ORDERS", "file" = 'sound/vo/automaton/executingorders.ogg'),
+ "Flesh Yields" = list("text" = "FLESH YIELDS", "file" = 'sound/vo/automaton/fleshyields.ogg'),
+ "Flesh Yields (Rare)" = list("text" = "FLESH YIELDS TO STEEL", "file" = 'sound/vo/automaton/fleshyieldsRARE.ogg'),
+ "Force Authorized" = list("text" = "FORCE AUTHORIZED", "file" = 'sound/vo/automaton/forceauthorized.ogg'),
+ "Fuel Low" = list("text" = "FUEL RESERVES LOW", "file" = 'sound/vo/automaton/fuellow.ogg'),
+ "Hahaha" = list("text" = "HA HA HA HA", "file" = 'sound/vo/automaton/hahaha.ogg'),
+ "Hail" = list("text" = "HAIL", "file" = 'sound/vo/automaton/hail.ogg'),
+ "Halt" = list("text" = "HALT", "file" = 'sound/vo/automaton/halt.ogg'),
+ "Heat Signature Acquired" = list("text" = "HEAT SIGNATURE ACQUIRED", "file" = 'sound/vo/automaton/heatsignatureacquired.ogg'),
+ "Help" = list("text" = "HELP", "file" = 'sound/vo/automaton/help.ogg'),
+ "Help Me" = list("text" = "HELP ME", "file" = 'sound/vo/automaton/helpme.ogg'),
+ "I Am Not Alive" = list("text" = "I AM NOT ALIVE", "file" = 'sound/vo/automaton/iamnotalive.ogg'),
+ "I Am The Children Of Man" = list("text" = "I AM THE CHILDREN OF MAN", "file" = 'sound/vo/automaton/iamthechildrenofman.ogg'),
+ "I Cannot Comply" = list("text" = "I CANNOT COMPLY", "file" = 'sound/vo/automaton/icannotcomply.ogg'),
+ "Identity Authorized" = list("text" = "IDENTITY AUTHORIZED", "file" = 'sound/vo/automaton/identityauthorized.ogg'),
+ "I Hate Women" = list("text" = "I HATE WOMEN", "file" = 'sound/vo/automaton/ihatewomen.ogg'),
+ "I Love Men" = list("text" = "I LOVE MEN", "file" = 'sound/vo/automaton/ilovemen.ogg'),
+ "Iron Within" = list("text" = "IRON WITHIN", "file" = 'sound/vo/automaton/ironwithin.ogg'),
+ "I Will Comply" = list("text" = "I WILL COMPLY", "file" = 'sound/vo/automaton/iwillcomply.ogg'),
+ "Jester Detected" = list("text" = "JESTER DETECTED", "file" = 'sound/vo/automaton/jesterdetected.ogg'),
+ "Kill" = list("text" = "KILL", "file" = 'sound/vo/automaton/kill.ogg'),
+ "Malum Praise" = list("text" = "MALUM BE PRAISED", "file" = 'sound/vo/automaton/malumpraise.ogg'),
+ "Moving To Location" = list("text" = "MOVING TO LOCATION", "file" = 'sound/vo/automaton/movingtolocation.ogg'),
+ "My Liege" = list("text" = "MY LIEGE", "file" = 'sound/vo/automaton/myliege.ogg'),
+ "My Soul Is Caged" = list("text" = "MY SOUL IS CAGED", "file" = 'sound/vo/automaton/mysouliscaged.ogg'),
+ "Necra Praise" = list("text" = "NECRA BE PRAISED", "file" = 'sound/vo/automaton/necrapraise.ogg'),
+ "No" = list("text" = "NO", "file" = 'sound/vo/automaton/No.ogg'),
+ "Noc Praise" = list("text" = "NOC BE PRAISED", "file" = 'sound/vo/automaton/nocpraise.ogg'),
+ "No Women Allowed" = list("text" = "NO WOMEN ALLOWED", "file" = 'sound/vo/automaton/nowomenallowed.ogg'),
+ "Obnoxiously Long Scream" = list("text" = "AAAAAAAAAAAAAAAAAAHHHHH", "file" = 'sound/vo/automaton/obnoxiouslylongscream.ogg'),
+ "Oh Shit Soldier Grenade" = list("text" = "OH SHIT SOLDIER GRENADE OORAH", "file" = 'sound/vo/automaton/OHSHITSOLDIERGRENADEOORAH.ogg'),
+ "Organic Presence Detected" = list("text" = "ORGANIC PRESENCE DETECTED", "file" = 'sound/vo/automaton/organicpresencedetected.ogg'),
+ "Pestra Praise" = list("text" = "PESTRA BE PRAISED", "file" = 'sound/vo/automaton/pestrapraise.ogg'),
+ "Psydon Lives" = list("text" = "PSYDON LIVES", "file" = 'sound/vo/automaton/PSYDONLIVES.ogg'),
+ "Ravox Praise" = list("text" = "RAVOX BE PRAISED", "file" = 'sound/vo/automaton/ravoxpraise.ogg'),
+ "Schmelf Detected" = list("text" = "SCHMELF DETECTED", "file" = 'sound/vo/automaton/schmelfdetected.ogg'),
+ "Silence Organic" = list("text" = "SILENCE, ORGANIC", "file" = 'sound/vo/automaton/silenceorganic.ogg'),
+ "Status Critical" = list("text" = "STATUS CRITICAL", "file" = 'sound/vo/automaton/statuscritical.ogg'),
+ "Status Critical 2" = list("text" = "CRITICAL SYSTEM FAILURE", "file" = 'sound/vo/automaton/statuscritical2.ogg'),
+ "To Bones" = list("text" = "TO BONES", "file" = 'sound/vo/automaton/tobones.ogg'),
+ "Warning" = list("text" = "WARNING", "file" = 'sound/vo/automaton/warning.ogg'),
+ "We Cannot Expect God" = list("text" = "WE CANNOT EXPECT GOD TO DO ALL THE WORK", "file" = 'sound/vo/automaton/wecannotexpectgodtodoallthework.ogg'),
+ "Woman Detected" = list("text" = "WOMAN DETECTED", "file" = 'sound/vo/automaton/womandetected.ogg'),
+ "Wrench Bones" = list("text" = "WRENCH BONES", "file" = 'sound/vo/automaton/wrenchbones.ogg'),
+ "Xylix Praise" = list("text" = "XYLIX BE PRAISED", "file" = 'sound/vo/automaton/xylixpraise.ogg'),
+ "Yes" = list("text" = "YES", "file" = 'sound/vo/automaton/Yes.ogg'),
+ "Your Bones Will Never Be Found" = list("text" = "YOUR BONES WILL NEVER BE FOUND", "file" = 'sound/vo/automaton/yourboneswillneverbefound.ogg'),
+ "Your Lux Will Be Mine" = list("text" = "YOUR LUX WILL BE MINE", "file" = 'sound/vo/automaton/yourluxwillbemine.ogg')
+))
+
+/datum/action/automaton_voice
+ name = "Voice Line"
+ desc = "Play an automaton voice line"
+ button_icon_state = "voice"
+ check_flags = AB_CHECK_CONSCIOUS
+ var/voice_line_key = null
+
+/datum/action/automaton_voice/New(Target, key)
+ ..()
+ if(key)
+ voice_line_key = key
+ var/list/voice_data = GLOB.automaton_voice_lines[key]
+ if(voice_data)
+ name = key
+ desc = "Play: [voice_data["text"]]"
+
+/datum/action/automaton_voice/Trigger(trigger_flags)
+ if(!ishuman(owner))
+ return
+
+ var/mob/living/carbon/human/H = owner
+ if(!istype(H.dna?.species, /datum/species/automaton))
+ return
+
+ if(H.stat >= UNCONSCIOUS)
+ to_chat(H, span_warning("SYSTEMS OFFLINE - UNABLE TO VOCALIZE"))
+ return
+
+ var/list/voice_data = GLOB.automaton_voice_lines[voice_line_key]
+ if(!voice_data)
+ return
+
+ playsound(H, voice_data["file"], 70, FALSE)
+ H.say(voice_data["text"], forced = TRUE)
+
+/datum/action/manage_voice_actions
+ name = "Manage Voice Lines"
+ desc = "Add or remove voice lines as quick-access actions."
+ button_icon_state = "voice"
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/manage_voice_actions/Trigger(trigger_flags)
+ . = ..()
+ if(!ishuman(owner))
+ return
+
+ var/mob/living/carbon/human/H = owner
+ if(!istype(H.dna?.species, /datum/species/automaton))
+ return
+
+ var/choice = browser_input_list(H, "Select a voice line to add/remove as an action:", "Voice Action Manager", GLOB.automaton_voice_lines)
+ if(!choice)
+ return
+
+ // Check if action already exists
+ for(var/datum/action/automaton_voice/AVA in H.actions)
+ if(AVA.voice_line_key == choice)
+ AVA.Remove(H)
+ to_chat(H, span_notice("Removed '[choice]' from quick actions."))
+ return
+
+ // Create new action
+ var/datum/action/automaton_voice/new_action = new(H, choice)
+ new_action.Grant(H)
+ to_chat(H, span_notice("Added '[choice]' to quick actions."))
diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/voicelines.dm b/code/modules/mob/living/carbon/human/species_types/automatons/voicelines.dm
new file mode 100644
index 00000000000..de313ef2b6e
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/species_types/automatons/voicelines.dm
@@ -0,0 +1,364 @@
+/mob/living/carbon/human/proc/voice_abyssorpraise()
+ set name = "Abyssor Praise"
+ set category = "Automaton"
+ play_voice_line("Abyssor Praise")
+
+/mob/living/carbon/human/proc/voice_againsttime()
+ set name = "Against Time"
+ set category = "Automaton"
+ play_voice_line("Against Time")
+
+/mob/living/carbon/human/proc/voice_astratapraise()
+ set name = "Astrata Praise"
+ set category = "Automaton"
+ play_voice_line("Astrata Praise")
+
+/mob/living/carbon/human/proc/voice_atonce()
+ set name = "At Once"
+ set category = "Automaton"
+ play_voice_line("At Once")
+
+/mob/living/carbon/human/proc/voice_awaitingorders()
+ set name = "Awaiting Orders"
+ set category = "Automaton"
+ play_voice_line("Awaiting Orders")
+
+/mob/living/carbon/human/proc/voice_beholdthemight()
+ set name = "Behold The Might"
+ set category = "Automaton"
+ play_voice_line("Behold The Might")
+
+/mob/living/carbon/human/proc/voice_building()
+ set name = "Building"
+ set category = "Automaton"
+ play_voice_line("Building")
+
+/mob/living/carbon/human/proc/voice_burn()
+ set name = "Burn"
+ set category = "Automaton"
+ play_voice_line("Burn")
+
+/mob/living/carbon/human/proc/voice_cataclysm()
+ set name = "Cataclysm"
+ set category = "Automaton"
+ play_voice_line("Cataclysm")
+
+/mob/living/carbon/human/proc/voice_combatmodeengaged()
+ set name = "Combat Mode Engaged"
+ set category = "Automaton"
+ play_voice_line("Combat Mode Engaged")
+
+/mob/living/carbon/human/proc/voice_commandreceived()
+ set name = "Command Received"
+ set category = "Automaton"
+ play_voice_line("Command Received")
+
+/mob/living/carbon/human/proc/voice_crownsdecree()
+ set name = "Crown's Decree"
+ set category = "Automaton"
+ play_voice_line("Crown's Decree")
+
+/mob/living/carbon/human/proc/voice_damagereceived()
+ set name = "Damage Received"
+ set category = "Automaton"
+ play_voice_line("Damage Received")
+
+/mob/living/carbon/human/proc/voice_deathcomes()
+ set name = "Death Comes"
+ set category = "Automaton"
+ play_voice_line("Death Comes")
+
+/mob/living/carbon/human/proc/voice_dendorpraise()
+ set name = "Dendor Praise"
+ set category = "Automaton"
+ play_voice_line("Dendor Praise")
+
+/mob/living/carbon/human/proc/voice_destroying()
+ set name = "Destroying"
+ set category = "Automaton"
+ play_voice_line("Destroying")
+
+/mob/living/carbon/human/proc/voice_dreamlesspause()
+ set name = "Dreamless Pause"
+ set category = "Automaton"
+ play_voice_line("Dreamless Pause")
+
+/mob/living/carbon/human/proc/voice_elfdetected()
+ set name = "Elf Detected"
+ set category = "Automaton"
+ play_voice_line("Elf Detected")
+
+/mob/living/carbon/human/proc/voice_eorapraise()
+ set name = "Eora Praise"
+ set category = "Automaton"
+ play_voice_line("Eora Praise")
+
+/mob/living/carbon/human/proc/voice_eorapraise2()
+ set name = "Eora Praise 2"
+ set category = "Automaton"
+ play_voice_line("Eora Praise 2")
+
+/mob/living/carbon/human/proc/voice_error()
+ set name = "Error"
+ set category = "Automaton"
+ play_voice_line("Error")
+
+/mob/living/carbon/human/proc/voice_everymovementispain()
+ set name = "Every Movement Is Pain"
+ set category = "Automaton"
+ play_voice_line("Every Movement Is Pain")
+
+/mob/living/carbon/human/proc/voice_executingorders()
+ set name = "Executing Orders"
+ set category = "Automaton"
+ play_voice_line("Executing Orders")
+
+/mob/living/carbon/human/proc/voice_fleshyields()
+ set name = "Flesh Yields"
+ set category = "Automaton"
+ play_voice_line("Flesh Yields")
+
+/mob/living/carbon/human/proc/voice_fleshyieldsrare()
+ set name = "Flesh Yields (Rare)"
+ set category = "Automaton"
+ play_voice_line("Flesh Yields (Rare)")
+
+/mob/living/carbon/human/proc/voice_forceauthorized()
+ set name = "Force Authorized"
+ set category = "Automaton"
+ play_voice_line("Force Authorized")
+
+/mob/living/carbon/human/proc/voice_fuellow()
+ set name = "Fuel Low"
+ set category = "Automaton"
+ play_voice_line("Fuel Low")
+
+/mob/living/carbon/human/proc/voice_hahaha()
+ set name = "Hahaha"
+ set category = "Automaton"
+ play_voice_line("Hahaha")
+
+/mob/living/carbon/human/proc/voice_hail()
+ set name = "Hail"
+ set category = "Automaton"
+ play_voice_line("Hail")
+
+/mob/living/carbon/human/proc/voice_halt()
+ set name = "Halt"
+ set category = "Automaton"
+ play_voice_line("Halt")
+
+/mob/living/carbon/human/proc/voice_heatsignatureacquired()
+ set name = "Heat Signature Acquired"
+ set category = "Automaton"
+ play_voice_line("Heat Signature Acquired")
+
+/mob/living/carbon/human/proc/voice_help()
+ set name = "Help"
+ set category = "Automaton"
+ play_voice_line("Help")
+
+/mob/living/carbon/human/proc/voice_helpme()
+ set name = "Help Me"
+ set category = "Automaton"
+ play_voice_line("Help Me")
+
+/mob/living/carbon/human/proc/voice_iamnotalive()
+ set name = "I Am Not Alive"
+ set category = "Automaton"
+ play_voice_line("I Am Not Alive")
+
+/mob/living/carbon/human/proc/voice_iamthechildrenofman()
+ set name = "I Am The Children Of Man"
+ set category = "Automaton"
+ play_voice_line("I Am The Children Of Man")
+
+/mob/living/carbon/human/proc/voice_icannotcomply()
+ set name = "I Cannot Comply"
+ set category = "Automaton"
+ play_voice_line("I Cannot Comply")
+
+/mob/living/carbon/human/proc/voice_identityauthorized()
+ set name = "Identity Authorized"
+ set category = "Automaton"
+ play_voice_line("Identity Authorized")
+
+/mob/living/carbon/human/proc/voice_ihatewomen()
+ set name = "I Hate Women"
+ set category = "Automaton"
+ play_voice_line("I Hate Women")
+
+/mob/living/carbon/human/proc/voice_ilovemen()
+ set name = "I Love Men"
+ set category = "Automaton"
+ play_voice_line("I Love Men")
+
+/mob/living/carbon/human/proc/voice_ironwithin()
+ set name = "Iron Within"
+ set category = "Automaton"
+ play_voice_line("Iron Within")
+
+/mob/living/carbon/human/proc/voice_iwillcomply()
+ set name = "I Will Comply"
+ set category = "Automaton"
+ play_voice_line("I Will Comply")
+
+/mob/living/carbon/human/proc/voice_jesterdetected()
+ set name = "Jester Detected"
+ set category = "Automaton"
+ play_voice_line("Jester Detected")
+
+/mob/living/carbon/human/proc/voice_kill()
+ set name = "Kill"
+ set category = "Automaton"
+ play_voice_line("Kill")
+
+/mob/living/carbon/human/proc/voice_malumpraise()
+ set name = "Malum Praise"
+ set category = "Automaton"
+ play_voice_line("Malum Praise")
+
+/mob/living/carbon/human/proc/voice_movingtolocation()
+ set name = "Moving To Location"
+ set category = "Automaton"
+ play_voice_line("Moving To Location")
+
+/mob/living/carbon/human/proc/voice_myliege()
+ set name = "My Liege"
+ set category = "Automaton"
+ play_voice_line("My Liege")
+
+/mob/living/carbon/human/proc/voice_mysouliscaged()
+ set name = "My Soul Is Caged"
+ set category = "Automaton"
+ play_voice_line("My Soul Is Caged")
+
+/mob/living/carbon/human/proc/voice_necrapraise()
+ set name = "Necra Praise"
+ set category = "Automaton"
+ play_voice_line("Necra Praise")
+
+/mob/living/carbon/human/proc/voice_no()
+ set name = "No"
+ set category = "Automaton"
+ play_voice_line("No")
+
+/mob/living/carbon/human/proc/voice_nocpraise()
+ set name = "Noc Praise"
+ set category = "Automaton"
+ play_voice_line("Noc Praise")
+
+/mob/living/carbon/human/proc/voice_nowomenallowed()
+ set name = "No Women Allowed"
+ set category = "Automaton"
+ play_voice_line("No Women Allowed")
+
+/mob/living/carbon/human/proc/voice_obnoxiouslylongscream()
+ set name = "Obnoxiously Long Scream"
+ set category = "Automaton"
+ play_voice_line("Obnoxiously Long Scream")
+
+/mob/living/carbon/human/proc/voice_ohshitsoldiergrenadeoorah()
+ set name = "Oh Shit Soldier Grenade"
+ set category = "Automaton"
+ play_voice_line("Oh Shit Soldier Grenade")
+
+/mob/living/carbon/human/proc/voice_organicpresencedetected()
+ set name = "Organic Presence Detected"
+ set category = "Automaton"
+ play_voice_line("Organic Presence Detected")
+
+/mob/living/carbon/human/proc/voice_pestrapraise()
+ set name = "Pestra Praise"
+ set category = "Automaton"
+ play_voice_line("Pestra Praise")
+
+/mob/living/carbon/human/proc/voice_psydonlives()
+ set name = "Psydon Lives"
+ set category = "Automaton"
+ play_voice_line("Psydon Lives")
+
+/mob/living/carbon/human/proc/voice_ravoxpraise()
+ set name = "Ravox Praise"
+ set category = "Automaton"
+ play_voice_line("Ravox Praise")
+
+/mob/living/carbon/human/proc/voice_schmelfdetected()
+ set name = "Schmelf Detected"
+ set category = "Automaton"
+ play_voice_line("Schmelf Detected")
+
+/mob/living/carbon/human/proc/voice_silenceorganic()
+ set name = "Silence Organic"
+ set category = "Automaton"
+ play_voice_line("Silence Organic")
+
+/mob/living/carbon/human/proc/voice_statuscritical()
+ set name = "Status Critical"
+ set category = "Automaton"
+ play_voice_line("Status Critical")
+
+/mob/living/carbon/human/proc/voice_statuscritical2()
+ set name = "Status Critical 2"
+ set category = "Automaton"
+ play_voice_line("Status Critical 2")
+
+/mob/living/carbon/human/proc/voice_tobones()
+ set name = "To Bones"
+ set category = "Automaton"
+ play_voice_line("To Bones")
+
+/mob/living/carbon/human/proc/voice_warning()
+ set name = "Warning"
+ set category = "Automaton"
+ play_voice_line("Warning")
+
+/mob/living/carbon/human/proc/voice_wecannotexpectgod()
+ set name = "We Cannot Expect God"
+ set category = "Automaton"
+ play_voice_line("We Cannot Expect God")
+
+/mob/living/carbon/human/proc/voice_womandetected()
+ set name = "Woman Detected"
+ set category = "Automaton"
+ play_voice_line("Woman Detected")
+
+/mob/living/carbon/human/proc/voice_wrenchbones()
+ set name = "Wrench Bones"
+ set category = "Automaton"
+ play_voice_line("Wrench Bones")
+
+/mob/living/carbon/human/proc/voice_xylixpraise()
+ set name = "Xylix Praise"
+ set category = "Automaton"
+ play_voice_line("Xylix Praise")
+
+/mob/living/carbon/human/proc/voice_yes()
+ set name = "Yes"
+ set category = "Automaton"
+ play_voice_line("Yes")
+
+/mob/living/carbon/human/proc/voice_yourboneswillneverbefound()
+ set name = "Your Bones Will Never Be Found"
+ set category = "Automaton"
+ play_voice_line("Your Bones Will Never Be Found")
+
+/mob/living/carbon/human/proc/voice_yourluxwillbemine()
+ set name = "Your Lux Will Be Mine"
+ set category = "Automaton"
+ play_voice_line("Your Lux Will Be Mine")
+
+/mob/living/carbon/human/proc/play_voice_line(voice_key)
+ if(!istype(dna?.species, /datum/species/automaton))
+ return
+
+ if(stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("SYSTEMS OFFLINE - UNABLE TO VOCALIZE"))
+ return
+
+ var/list/voice_data = GLOB.automaton_voice_lines[voice_key]
+ if(!voice_data)
+ return
+
+ playsound(src, voice_data["file"], 70, FALSE)
+ say(voice_data["text"], forced = TRUE)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index ed516270a08..aead6577ae3 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -323,7 +323,7 @@
playsound(src, M.a_intent.hitsound, 100, FALSE)
log_combat(M, src, "attacked")
-
+ SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_ANIMAL, M)
return TRUE
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index ec461a3d0f6..0eaa4e6f3e7 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -137,6 +137,8 @@
var/sigreturn = SEND_SIGNAL(src, COMSIG_MOB_SAY, args)
if (sigreturn & COMPONENT_UPPERCASE_SPEECH)
message = uppertext(message)
+ if((sigreturn & COMPONENT_SPEECH_CANCEL) && !forced)
+ return
if(!message)
return
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/bear.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/bear.dm
index 260e89498f6..019e902af1d 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/bear.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/bear.dm
@@ -1,3 +1,11 @@
+/datum/component/riding/direbear/Initialize()
+ . = ..()
+ set_riding_offsets(RIDING_OFFSET_ALL, list(TEXT_NORTH = list(16, 14), TEXT_SOUTH = list(12, 8), TEXT_EAST = list(7, 12), TEXT_WEST = list(14, 12)))
+ set_vehicle_dir_layer(SOUTH, OBJ_LAYER)
+ set_vehicle_dir_layer(NORTH, OBJ_LAYER)
+ set_vehicle_dir_layer(EAST, OBJ_LAYER)
+ set_vehicle_dir_layer(WEST, OBJ_LAYER)
+
/datum/status_effect/debuff/staggered
id = "staggered"
alert_type = /atom/movable/screen/alert/status_effect/debuff/staggered
@@ -119,11 +127,17 @@
aggressive = 1
stat_attack = UNCONSCIOUS //You falling unconcious won't save you, little one..
ai_controller = /datum/ai_controller/direbear
+ can_buckle = TRUE
/mob/living/simple_animal/hostile/retaliate/direbear/Initialize(mapload)
. = ..()
AddComponent(/datum/component/ai_aggro_system)
+/mob/living/simple_animal/hostile/retaliate/direbear/tamed(mob/user)
+ . = ..()
+ if(can_buckle)
+ AddComponent(/datum/component/riding/direbear)
+
/mob/living/simple_animal/hostile/retaliate/direbear/get_sound(input)
switch(input)
if("aggro")
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 7e2bc122647..affb6274b5b 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -151,6 +151,7 @@
/// What job does this mob have
var/job = null//Living
+ var/datum/job/job_type
/// A list of factions that this mob is currently in, for hostile mob targetting, amongst other things
var/list/faction = list(FACTION_NEUTRAL)
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 6d28dbf88d1..11ba9dd9512 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -86,6 +86,9 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
M.reagents.add_reagent(type, amount, new_reagent.data)
return 1
+/datum/reagent/proc/add_data(data_name, data_value)
+ LAZYADDASSOC(data, data_name, data_value)
+
/datum/reagent/proc/reaction_obj(obj/O, volume)
return
@@ -170,9 +173,9 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
return
// Called after add_reagents creates a new reagent.
-/datum/reagent/proc/on_new(data)
- if(data && data["quality"])
- recipe_quality = data["quality"]
+/datum/reagent/proc/on_new(list/incoming_data)
+ if(incoming_data && incoming_data["quality"])
+ recipe_quality = incoming_data["quality"]
else
recipe_quality = base_quality
recipe_quality = CLAMP(recipe_quality, 1, 4)
@@ -180,13 +183,25 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
if(!data)
data = list()
data["quality"] = recipe_quality
+ for(var/data_item in incoming_data)
+ if(data_item == "quality") //special handling for this
+ continue
+ data[data_item] = incoming_data[data_item]
+ if("custom_name" in data)
+ name = data["custom_name"]
+ if("custom_scent" in data)
+ scent_description = data["custom_scent"]
+ if("custom_tastes" in data)
+ taste_description = data["custom_tastes"]
return
// Called when two reagents of the same are mixing.
-/datum/reagent/proc/on_merge(data, other_volume)
+/datum/reagent/proc/on_merge(list/incoming_data, other_volume)
SHOULD_CALL_PARENT(TRUE)
- if(data && data["quality"])
- var/other_quality = data["quality"]
+ if(!length(incoming_data))
+ return
+ if("quality" in incoming_data)
+ var/other_quality = incoming_data["quality"]
var/total_volume = volume + other_volume
var/weighted_average = ((recipe_quality * volume) + (other_quality * other_volume)) / total_volume
@@ -197,6 +212,16 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
if(!data)
data = list()
data["quality"] = recipe_quality
+ for(var/data_item in incoming_data)
+ if(data_item == "quality") //special handling for this
+ continue
+ data[data_item] = incoming_data[data_item]
+ if("custom_name" in data)
+ name = data["custom_name"]
+ if("custom_scent" in data)
+ scent_description = data["custom_scent"]
+ if("custom_tastes" in data)
+ taste_description = data["custom_tastes"]
return
/datum/reagent/proc/get_quality_metabolization_modifier()
diff --git a/code/modules/spells/spell_types/pointed/projectile/swordfish.dm b/code/modules/spells/spell_types/pointed/projectile/swordfish.dm
index cd567f3befc..34023b5b9ec 100644
--- a/code/modules/spells/spell_types/pointed/projectile/swordfish.dm
+++ b/code/modules/spells/spell_types/pointed/projectile/swordfish.dm
@@ -67,4 +67,4 @@
FISH_BAIT_VALUE = VEGETABLES,
),
)
- fish_traits = list(/datum/fish_trait/predator, /datum/fish_trait/territorial)
+ fish_traits = list(/datum/fish_trait/predator, /datum/fish_trait/carnivore)
diff --git a/code/modules/spells/spell_types/undirected/conjure_item/summon_trident.dm b/code/modules/spells/spell_types/undirected/conjure_item/summon_trident.dm
index a6effe54cf9..a5216b10fc3 100644
--- a/code/modules/spells/spell_types/undirected/conjure_item/summon_trident.dm
+++ b/code/modules/spells/spell_types/undirected/conjure_item/summon_trident.dm
@@ -143,7 +143,7 @@
/obj/item/fishing/lure/no_bait/is_catchable_fish(obj/item/reagent_containers/food/snacks/fish/fish, list/fish_properties)
// Scares off tiny and small fish
- if(fish.size <= FISH_SIZE_SMALL_MAX)
+ if(fish.size <= fish.average_size * 1.1)
return FALSE
// Catches carps, eels, shrimp, anglerfish, and clownfish
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 13fecbe185c..92f92fc81b7 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/mob/screen_full.dmi b/icons/mob/screen_full.dmi
index 520f12cf360..27a6c5a57f2 100644
Binary files a/icons/mob/screen_full.dmi and b/icons/mob/screen_full.dmi differ
diff --git a/icons/roguetown/clothing/onmob/test.py b/icons/roguetown/clothing/onmob/test.py
new file mode 100644
index 00000000000..ce11976f9ed
--- /dev/null
+++ b/icons/roguetown/clothing/onmob/test.py
@@ -0,0 +1,438 @@
+#!/usr/bin/env python3
+"""
+DMI Sprite Desleeving Script
+Merges directional sprite states (r_, l_ prefixed) back into base states within DMI files.
+"""
+
+import os
+import sys
+import shutil
+from PIL import Image, PngImagePlugin
+from pathlib import Path
+import re
+
+class DMIDesleever:
+ def __init__(self):
+ self.stats = {
+ 'files_processed': 0,
+ 'states_merged': 0,
+ 'states_removed': 0,
+ 'errors': 0
+ }
+
+ def parse_dmi_metadata(self, img):
+ """Parse DMI metadata from PNG info"""
+ if 'Description' not in img.info:
+ return None
+
+ description = img.info['Description']
+ lines = description.strip().split('\n')
+
+ if not lines or not lines[0].startswith('# BEGIN DMI'):
+ return None
+
+ metadata = {
+ 'version': '4.0',
+ 'width': 32,
+ 'height': 32,
+ 'states': []
+ }
+
+ current_state = None
+
+ for line in lines[1:]:
+ line = line.strip()
+
+ if line.startswith('version ='):
+ metadata['version'] = line.split('=')[1].strip()
+ elif line.startswith('width ='):
+ metadata['width'] = int(line.split('=')[1].strip())
+ elif line.startswith('height ='):
+ metadata['height'] = int(line.split('=')[1].strip())
+ elif line.startswith('state ='):
+ if current_state:
+ metadata['states'].append(current_state)
+ state_name = line.split('=', 1)[1].strip().strip('"')
+ current_state = {
+ 'name': state_name,
+ 'dirs': 1,
+ 'frames': 1,
+ 'delays': []
+ }
+ elif current_state:
+ if line.startswith('dirs ='):
+ current_state['dirs'] = int(line.split('=')[1].strip())
+ elif line.startswith('frames ='):
+ current_state['frames'] = int(line.split('=')[1].strip())
+ elif line.startswith('delay ='):
+ delays_str = line.split('=', 1)[1].strip()
+ current_state['delays'] = [float(x) for x in delays_str.split(',')]
+
+ if current_state:
+ metadata['states'].append(current_state)
+
+ return metadata
+
+ def build_dmi_metadata(self, metadata):
+ """Build DMI metadata string"""
+ lines = ['# BEGIN DMI']
+ lines.append(f'version = {metadata["version"]}')
+ lines.append(f'\twidth = {metadata["width"]}')
+ lines.append(f'\theight = {metadata["height"]}')
+
+ for state in metadata['states']:
+ lines.append(f'state = "{state["name"]}"')
+ lines.append(f'\tdirs = {state["dirs"]}')
+ lines.append(f'\tframes = {state["frames"]}')
+ if state.get('delays'):
+ delays_str = ','.join(str(d) for d in state['delays'])
+ lines.append(f'\tdelay = {delays_str}')
+
+ lines.append('# END DMI')
+ return '\n'.join(lines)
+
+ def extract_state_images(self, img, metadata):
+ """Extract individual state images from the DMI sprite sheet"""
+ width = metadata['width']
+ height = metadata['height']
+
+ # Calculate sprites per row
+ sprites_per_row = img.width // width
+
+ states_images = {}
+ current_index = 0
+
+ for state in metadata['states']:
+ total_sprites = state['dirs'] * state['frames']
+ sprites = []
+
+ for i in range(total_sprites):
+ sprite_index = current_index + i
+ row = sprite_index // sprites_per_row
+ col = sprite_index % sprites_per_row
+
+ x = col * width
+ y = row * height
+
+ sprite = img.crop((x, y, x + width, y + height))
+ sprites.append(sprite)
+
+ states_images[state['name']] = sprites
+ current_index += total_sprites
+
+ return states_images
+
+ def find_state_variants(self, states):
+ """Find base states and their r_, l_ variants"""
+ base_states = {}
+
+ for state_name in states:
+ # Skip variant states
+ if state_name.startswith('r_') or state_name.startswith('l_'):
+ continue
+
+ variants = {}
+
+ # Look for right variant
+ r_name = f'r_{state_name}'
+ if r_name in states:
+ variants['r'] = r_name
+
+ # Look for left variant
+ l_name = f'l_{state_name}'
+ if l_name in states:
+ variants['l'] = l_name
+
+ if variants:
+ base_states[state_name] = variants
+
+ return base_states
+
+ def merge_state_directions(self, base_state_name, base_sprites, variant_sprites_dict, metadata_states):
+ """Merge directional variants by compositing them onto base sprites"""
+ # Get base state metadata
+ base_state = next((s for s in metadata_states if s['name'] == base_state_name), None)
+
+ if not base_state:
+ raise ValueError(f"Could not find metadata for state: {base_state_name}")
+
+ # The base already has directions, we're just adding overlays
+ # We need to composite the r_ and l_ sprites onto the base sprites
+ # r_ = right arm, l_ = left arm (or vice versa)
+
+ merged_sprites = []
+ frames = base_state['frames']
+ dirs = base_state['dirs']
+
+ # Process each sprite in the base state
+ for i, base_sprite in enumerate(base_sprites):
+ # Create a new sprite with base + overlays composited
+ merged_sprite = base_sprite.copy()
+
+ # Composite right variant if it exists
+ if 'r' in variant_sprites_dict and i < len(variant_sprites_dict['r']):
+ r_sprite = variant_sprites_dict['r'][i]
+ merged_sprite = Image.alpha_composite(merged_sprite.convert('RGBA'), r_sprite.convert('RGBA'))
+
+ # Composite left variant if it exists
+ if 'l' in variant_sprites_dict and i < len(variant_sprites_dict['l']):
+ l_sprite = variant_sprites_dict['l'][i]
+ merged_sprite = Image.alpha_composite(merged_sprite, l_sprite.convert('RGBA'))
+
+ merged_sprites.append(merged_sprite)
+
+ # Return the same number of directions as the base
+ return merged_sprites, dirs
+
+ def rebuild_dmi(self, states_images, metadata):
+ """Rebuild DMI sprite sheet from state images"""
+ width = metadata['width']
+ height = metadata['height']
+
+ # Calculate total sprites needed
+ total_sprites = sum(len(sprites) for sprites in states_images.values())
+
+ # Calculate sheet dimensions (try to make it squarish)
+ sprites_per_row = 32 # Reasonable default
+ rows_needed = (total_sprites + sprites_per_row - 1) // sprites_per_row
+
+ sheet_width = sprites_per_row * width
+ sheet_height = rows_needed * height
+
+ # Create new sprite sheet
+ new_sheet = Image.new('RGBA', (sheet_width, sheet_height), (0, 0, 0, 0))
+
+ current_index = 0
+ for state in metadata['states']:
+ if state['name'] not in states_images:
+ continue
+
+ sprites = states_images[state['name']]
+
+ for sprite in sprites:
+ row = current_index // sprites_per_row
+ col = current_index % sprites_per_row
+
+ x = col * width
+ y = row * height
+
+ new_sheet.paste(sprite, (x, y))
+ current_index += 1
+
+ return new_sheet
+
+ def process_dmi_file(self, dmi_path, dry_run=False):
+ """Process a single DMI file and merge variant states"""
+ try:
+ print(f"\nProcessing: {dmi_path.name}")
+
+ # Load DMI file
+ img = Image.open(dmi_path)
+ metadata = self.parse_dmi_metadata(img)
+
+ if not metadata:
+ print(f" ✗ Not a valid DMI file (no metadata found)")
+ self.stats['errors'] += 1
+ return False
+
+ print(f" Found {len(metadata['states'])} states")
+
+ # Extract all state images
+ states_images = self.extract_state_images(img, metadata)
+
+ # Find states that need merging
+ base_states = self.find_state_variants(states_images)
+
+ if not base_states:
+ print(f" No variant states found to merge")
+ return True
+
+ print(f" Found {len(base_states)} states with variants:")
+
+ # Merge states
+ new_states_images = {}
+ new_metadata_states = []
+ states_to_remove = set()
+
+ for state in metadata['states']:
+ state_name = state['name']
+
+ # Skip variant states (they'll be merged)
+ if state_name.startswith('r_') or state_name.startswith('l_'):
+ states_to_remove.add(state_name)
+ continue
+
+ # Check if this state has variants to merge
+ if state_name in base_states:
+ variants = base_states[state_name]
+ print(f" - {state_name}: merging {', '.join(variants.keys())}")
+
+ # Get variant sprites
+ variant_sprites = {
+ k: states_images[v] for k, v in variants.items()
+ }
+
+ # Merge directions
+ merged_sprites, new_dirs = self.merge_state_directions(
+ state_name,
+ states_images[state_name],
+ variant_sprites,
+ metadata['states']
+ )
+
+ new_states_images[state_name] = merged_sprites
+
+ # Update state metadata
+ new_state = state.copy()
+ new_state['dirs'] = new_dirs
+ new_metadata_states.append(new_state)
+
+ self.stats['states_merged'] += 1
+ for v in variants.values():
+ states_to_remove.add(v)
+ else:
+ # Keep state as-is
+ new_states_images[state_name] = states_images[state_name]
+ new_metadata_states.append(state)
+
+ if dry_run:
+ print(f" [DRY RUN] Would merge {len(base_states)} states and remove {len(states_to_remove)} variants")
+ return True
+
+ # Build new metadata
+ new_metadata = metadata.copy()
+ new_metadata['states'] = new_metadata_states
+
+ # Rebuild sprite sheet
+ new_sheet = self.rebuild_dmi(new_states_images, new_metadata)
+
+ # Build metadata string
+ metadata_str = self.build_dmi_metadata(new_metadata)
+
+ # Save with metadata
+ pnginfo = PngImagePlugin.PngInfo()
+ pnginfo.add_text('Description', metadata_str)
+
+ # Backup original
+ backup_path = dmi_path.parent / f"{dmi_path.stem}.dmi.backup"
+ if not backup_path.exists():
+ # Copy the original file as-is
+ import shutil
+ shutil.copy2(dmi_path, backup_path)
+ print(f" Backup saved: {backup_path.name}")
+
+ # Save new DMI (DMI files are just PNGs with metadata)
+ new_sheet.save(dmi_path, format='PNG', pnginfo=pnginfo)
+
+ print(f" ✓ Merged {len(base_states)} states, removed {len(states_to_remove)} variant states")
+ self.stats['states_removed'] += len(states_to_remove)
+ self.stats['files_processed'] += 1
+
+ return True
+
+ except Exception as e:
+ print(f" ✗ Error: {e}")
+ self.stats['errors'] += 1
+ import traceback
+ traceback.print_exc()
+ return False
+
+ def process_directory(self, directory, recursive=False, dry_run=False):
+ """Process all DMI files in a directory"""
+ directory = Path(directory)
+
+ if not directory.exists():
+ print(f"Directory not found: {directory}")
+ return
+
+ # Find all DMI files
+ if recursive:
+ dmi_files = list(directory.rglob('*.dmi'))
+ else:
+ dmi_files = list(directory.glob('*.dmi'))
+
+ print(f"Found {len(dmi_files)} DMI files")
+
+ for dmi_file in dmi_files:
+ self.process_dmi_file(dmi_file, dry_run)
+
+ self.print_stats()
+
+ def print_stats(self):
+ """Print processing statistics"""
+ print("\n" + "="*60)
+ print("Processing Complete")
+ print("="*60)
+ print(f"DMI Files Processed: {self.stats['files_processed']}")
+ print(f"States Merged: {self.stats['states_merged']}")
+ print(f"Variant States Removed: {self.stats['states_removed']}")
+ print(f"Errors: {self.stats['errors']}")
+ print("="*60)
+
+def main():
+ print("="*60)
+ print("DMI Sprite Desleeving Tool")
+ print("="*60)
+ print()
+
+ desleever = DMIDesleever()
+
+ if len(sys.argv) < 2:
+ print("Usage:")
+ print(" Single file: desleeve.py ")
+ print(" Directory: desleeve.py [options]")
+ print("\nOptions:")
+ print(" --recursive, -r Process subdirectories")
+ print(" --dry-run Show what would be changed without modifying files")
+ print("\nDrag and drop a DMI file or folder onto this script to run.")
+ print("\nNote: Original files are backed up as .dmi.backup")
+ input("\nPress Enter to exit...")
+ return
+
+ target = sys.argv[1]
+ recursive = '--recursive' in sys.argv or '-r' in sys.argv
+ dry_run = '--dry-run' in sys.argv
+
+ print(f"Target: {target}")
+ print(f"Recursive: {recursive}")
+ print(f"Dry run: {dry_run}")
+ print()
+
+ target_path = Path(target)
+
+ if not target_path.exists():
+ print(f"ERROR: Path does not exist: {target_path}")
+ input("\nPress Enter to exit...")
+ return
+
+ if dry_run:
+ print("*** DRY RUN MODE - No files will be modified ***\n")
+
+ try:
+ if target_path.is_file():
+ if target_path.suffix.lower() == '.dmi':
+ print("Processing single DMI file...\n")
+ success = desleever.process_dmi_file(target_path, dry_run)
+ desleever.print_stats()
+ if not success:
+ print("\nProcessing failed - check errors above")
+ else:
+ print(f"ERROR: {target} is not a DMI file (extension: {target_path.suffix})")
+ elif target_path.is_dir():
+ print(f"Processing directory: {target_path}")
+ if recursive:
+ print("(Recursive mode enabled)")
+ print()
+ desleever.process_directory(target_path, recursive, dry_run)
+ else:
+ print(f"ERROR: {target} is not a valid file or directory")
+ except Exception as e:
+ print(f"\nFATAL ERROR: {e}")
+ import traceback
+ traceback.print_exc()
+
+ print("\n")
+ input("Press Enter to exit...")
+
+if __name__ == "__main__":
+ main()
diff --git a/icons/roguetown/mob/bodies/m/automaton.dmi b/icons/roguetown/mob/bodies/m/automaton.dmi
new file mode 100644
index 00000000000..94df37706cb
Binary files /dev/null and b/icons/roguetown/mob/bodies/m/automaton.dmi differ
diff --git a/sound/vo/automaton/No.ogg b/sound/vo/automaton/No.ogg
new file mode 100644
index 00000000000..aed69b626c6
Binary files /dev/null and b/sound/vo/automaton/No.ogg differ
diff --git a/sound/vo/automaton/OHSHITSOLDIERGRENADEOORAH.ogg b/sound/vo/automaton/OHSHITSOLDIERGRENADEOORAH.ogg
new file mode 100644
index 00000000000..fa60e617e2d
Binary files /dev/null and b/sound/vo/automaton/OHSHITSOLDIERGRENADEOORAH.ogg differ
diff --git a/sound/vo/automaton/PSYDONLIVES.ogg b/sound/vo/automaton/PSYDONLIVES.ogg
new file mode 100644
index 00000000000..655cb08ce7f
Binary files /dev/null and b/sound/vo/automaton/PSYDONLIVES.ogg differ
diff --git a/sound/vo/automaton/Yes.ogg b/sound/vo/automaton/Yes.ogg
new file mode 100644
index 00000000000..9b19d1b146e
Binary files /dev/null and b/sound/vo/automaton/Yes.ogg differ
diff --git a/sound/vo/automaton/abyssorpraise.ogg b/sound/vo/automaton/abyssorpraise.ogg
new file mode 100644
index 00000000000..67dca55af83
Binary files /dev/null and b/sound/vo/automaton/abyssorpraise.ogg differ
diff --git a/sound/vo/automaton/againsttime.ogg b/sound/vo/automaton/againsttime.ogg
new file mode 100644
index 00000000000..1b3c5c3ee2c
Binary files /dev/null and b/sound/vo/automaton/againsttime.ogg differ
diff --git a/sound/vo/automaton/astratapraise.ogg b/sound/vo/automaton/astratapraise.ogg
new file mode 100644
index 00000000000..fb728e247cb
Binary files /dev/null and b/sound/vo/automaton/astratapraise.ogg differ
diff --git a/sound/vo/automaton/atonce.ogg b/sound/vo/automaton/atonce.ogg
new file mode 100644
index 00000000000..ff2a9489782
Binary files /dev/null and b/sound/vo/automaton/atonce.ogg differ
diff --git a/sound/vo/automaton/awaitingorders.ogg b/sound/vo/automaton/awaitingorders.ogg
new file mode 100644
index 00000000000..7cc49eb3550
Binary files /dev/null and b/sound/vo/automaton/awaitingorders.ogg differ
diff --git a/sound/vo/automaton/beholdthemight.ogg b/sound/vo/automaton/beholdthemight.ogg
new file mode 100644
index 00000000000..90c5351ab32
Binary files /dev/null and b/sound/vo/automaton/beholdthemight.ogg differ
diff --git a/sound/vo/automaton/building.ogg b/sound/vo/automaton/building.ogg
new file mode 100644
index 00000000000..9d1b89f0be6
Binary files /dev/null and b/sound/vo/automaton/building.ogg differ
diff --git a/sound/vo/automaton/burn.ogg b/sound/vo/automaton/burn.ogg
new file mode 100644
index 00000000000..b5238ef6482
Binary files /dev/null and b/sound/vo/automaton/burn.ogg differ
diff --git a/sound/vo/automaton/cataclysm.ogg b/sound/vo/automaton/cataclysm.ogg
new file mode 100644
index 00000000000..8befb8e51c1
Binary files /dev/null and b/sound/vo/automaton/cataclysm.ogg differ
diff --git a/sound/vo/automaton/combatmodeengaged.ogg b/sound/vo/automaton/combatmodeengaged.ogg
new file mode 100644
index 00000000000..ae7c799b935
Binary files /dev/null and b/sound/vo/automaton/combatmodeengaged.ogg differ
diff --git a/sound/vo/automaton/commandreceived.ogg b/sound/vo/automaton/commandreceived.ogg
new file mode 100644
index 00000000000..c974d718e7c
Binary files /dev/null and b/sound/vo/automaton/commandreceived.ogg differ
diff --git a/sound/vo/automaton/crownsdecree.ogg b/sound/vo/automaton/crownsdecree.ogg
new file mode 100644
index 00000000000..e08d6fd02f9
Binary files /dev/null and b/sound/vo/automaton/crownsdecree.ogg differ
diff --git a/sound/vo/automaton/damagereceived.ogg b/sound/vo/automaton/damagereceived.ogg
new file mode 100644
index 00000000000..ebd9337ce93
Binary files /dev/null and b/sound/vo/automaton/damagereceived.ogg differ
diff --git a/sound/vo/automaton/deathcomes.ogg b/sound/vo/automaton/deathcomes.ogg
new file mode 100644
index 00000000000..c67d92ee555
Binary files /dev/null and b/sound/vo/automaton/deathcomes.ogg differ
diff --git a/sound/vo/automaton/dendorpraise.ogg b/sound/vo/automaton/dendorpraise.ogg
new file mode 100644
index 00000000000..1611386b39c
Binary files /dev/null and b/sound/vo/automaton/dendorpraise.ogg differ
diff --git a/sound/vo/automaton/destroying.ogg b/sound/vo/automaton/destroying.ogg
new file mode 100644
index 00000000000..6dff72c1f29
Binary files /dev/null and b/sound/vo/automaton/destroying.ogg differ
diff --git a/sound/vo/automaton/dreamlesspause.ogg b/sound/vo/automaton/dreamlesspause.ogg
new file mode 100644
index 00000000000..8e0c1f3e47f
Binary files /dev/null and b/sound/vo/automaton/dreamlesspause.ogg differ
diff --git a/sound/vo/automaton/elfdetected.ogg b/sound/vo/automaton/elfdetected.ogg
new file mode 100644
index 00000000000..3e7ed43082a
Binary files /dev/null and b/sound/vo/automaton/elfdetected.ogg differ
diff --git a/sound/vo/automaton/eorapraise.ogg b/sound/vo/automaton/eorapraise.ogg
new file mode 100644
index 00000000000..b356f14311b
Binary files /dev/null and b/sound/vo/automaton/eorapraise.ogg differ
diff --git a/sound/vo/automaton/eorapraise2.ogg b/sound/vo/automaton/eorapraise2.ogg
new file mode 100644
index 00000000000..20e85d8ac8f
Binary files /dev/null and b/sound/vo/automaton/eorapraise2.ogg differ
diff --git a/sound/vo/automaton/error.ogg b/sound/vo/automaton/error.ogg
new file mode 100644
index 00000000000..bd0c801db10
Binary files /dev/null and b/sound/vo/automaton/error.ogg differ
diff --git a/sound/vo/automaton/everymovementispain.ogg b/sound/vo/automaton/everymovementispain.ogg
new file mode 100644
index 00000000000..e9889e6c921
Binary files /dev/null and b/sound/vo/automaton/everymovementispain.ogg differ
diff --git a/sound/vo/automaton/executingorders.ogg b/sound/vo/automaton/executingorders.ogg
new file mode 100644
index 00000000000..a578ec46370
Binary files /dev/null and b/sound/vo/automaton/executingorders.ogg differ
diff --git a/sound/vo/automaton/fleshyields.ogg b/sound/vo/automaton/fleshyields.ogg
new file mode 100644
index 00000000000..cc3ee517592
Binary files /dev/null and b/sound/vo/automaton/fleshyields.ogg differ
diff --git a/sound/vo/automaton/fleshyieldsRARE.ogg b/sound/vo/automaton/fleshyieldsRARE.ogg
new file mode 100644
index 00000000000..2093f4ddbc9
Binary files /dev/null and b/sound/vo/automaton/fleshyieldsRARE.ogg differ
diff --git a/sound/vo/automaton/forceauthorized.ogg b/sound/vo/automaton/forceauthorized.ogg
new file mode 100644
index 00000000000..06e5ffbcb90
Binary files /dev/null and b/sound/vo/automaton/forceauthorized.ogg differ
diff --git a/sound/vo/automaton/fuellow.ogg b/sound/vo/automaton/fuellow.ogg
new file mode 100644
index 00000000000..875563b9f70
Binary files /dev/null and b/sound/vo/automaton/fuellow.ogg differ
diff --git a/sound/vo/automaton/hahaha.ogg b/sound/vo/automaton/hahaha.ogg
new file mode 100644
index 00000000000..c0b4401a9ef
Binary files /dev/null and b/sound/vo/automaton/hahaha.ogg differ
diff --git a/sound/vo/automaton/hail.ogg b/sound/vo/automaton/hail.ogg
new file mode 100644
index 00000000000..7d5e285dd87
Binary files /dev/null and b/sound/vo/automaton/hail.ogg differ
diff --git a/sound/vo/automaton/halt.ogg b/sound/vo/automaton/halt.ogg
new file mode 100644
index 00000000000..ff7e827efd2
Binary files /dev/null and b/sound/vo/automaton/halt.ogg differ
diff --git a/sound/vo/automaton/heatsignatureacquired.ogg b/sound/vo/automaton/heatsignatureacquired.ogg
new file mode 100644
index 00000000000..5bbfd911071
Binary files /dev/null and b/sound/vo/automaton/heatsignatureacquired.ogg differ
diff --git a/sound/vo/automaton/help.ogg b/sound/vo/automaton/help.ogg
new file mode 100644
index 00000000000..c2077975ba7
Binary files /dev/null and b/sound/vo/automaton/help.ogg differ
diff --git a/sound/vo/automaton/helpme.ogg b/sound/vo/automaton/helpme.ogg
new file mode 100644
index 00000000000..473ba8ded6f
Binary files /dev/null and b/sound/vo/automaton/helpme.ogg differ
diff --git a/sound/vo/automaton/iamnotalive.ogg b/sound/vo/automaton/iamnotalive.ogg
new file mode 100644
index 00000000000..a85f0d25c83
Binary files /dev/null and b/sound/vo/automaton/iamnotalive.ogg differ
diff --git a/sound/vo/automaton/iamthechildrenofman.ogg b/sound/vo/automaton/iamthechildrenofman.ogg
new file mode 100644
index 00000000000..769d2ebf349
Binary files /dev/null and b/sound/vo/automaton/iamthechildrenofman.ogg differ
diff --git a/sound/vo/automaton/icannotcomply.ogg b/sound/vo/automaton/icannotcomply.ogg
new file mode 100644
index 00000000000..8967d35a3ab
Binary files /dev/null and b/sound/vo/automaton/icannotcomply.ogg differ
diff --git a/sound/vo/automaton/identityauthorized.ogg b/sound/vo/automaton/identityauthorized.ogg
new file mode 100644
index 00000000000..e278a5e877a
Binary files /dev/null and b/sound/vo/automaton/identityauthorized.ogg differ
diff --git a/sound/vo/automaton/ihatewomen.ogg b/sound/vo/automaton/ihatewomen.ogg
new file mode 100644
index 00000000000..994aa0839fb
Binary files /dev/null and b/sound/vo/automaton/ihatewomen.ogg differ
diff --git a/sound/vo/automaton/ilovemen.ogg b/sound/vo/automaton/ilovemen.ogg
new file mode 100644
index 00000000000..0d593eb410e
Binary files /dev/null and b/sound/vo/automaton/ilovemen.ogg differ
diff --git a/sound/vo/automaton/ironwithin.ogg b/sound/vo/automaton/ironwithin.ogg
new file mode 100644
index 00000000000..0cea115c970
Binary files /dev/null and b/sound/vo/automaton/ironwithin.ogg differ
diff --git a/sound/vo/automaton/iwillcomply.ogg b/sound/vo/automaton/iwillcomply.ogg
new file mode 100644
index 00000000000..6ed7f9d3e4b
Binary files /dev/null and b/sound/vo/automaton/iwillcomply.ogg differ
diff --git a/sound/vo/automaton/jesterdetected.ogg b/sound/vo/automaton/jesterdetected.ogg
new file mode 100644
index 00000000000..ed212241a21
Binary files /dev/null and b/sound/vo/automaton/jesterdetected.ogg differ
diff --git a/sound/vo/automaton/kill.ogg b/sound/vo/automaton/kill.ogg
new file mode 100644
index 00000000000..6695661949d
Binary files /dev/null and b/sound/vo/automaton/kill.ogg differ
diff --git a/sound/vo/automaton/malumpraise.ogg b/sound/vo/automaton/malumpraise.ogg
new file mode 100644
index 00000000000..1284ad7f4a4
Binary files /dev/null and b/sound/vo/automaton/malumpraise.ogg differ
diff --git a/sound/vo/automaton/movingtolocation.ogg b/sound/vo/automaton/movingtolocation.ogg
new file mode 100644
index 00000000000..31a3472ed1a
Binary files /dev/null and b/sound/vo/automaton/movingtolocation.ogg differ
diff --git a/sound/vo/automaton/myliege.ogg b/sound/vo/automaton/myliege.ogg
new file mode 100644
index 00000000000..83024a1df6c
Binary files /dev/null and b/sound/vo/automaton/myliege.ogg differ
diff --git a/sound/vo/automaton/mysouliscaged.ogg b/sound/vo/automaton/mysouliscaged.ogg
new file mode 100644
index 00000000000..15a4bfc1b41
Binary files /dev/null and b/sound/vo/automaton/mysouliscaged.ogg differ
diff --git a/sound/vo/automaton/necrapraise.ogg b/sound/vo/automaton/necrapraise.ogg
new file mode 100644
index 00000000000..f7064bbdaa3
Binary files /dev/null and b/sound/vo/automaton/necrapraise.ogg differ
diff --git a/sound/vo/automaton/nocpraise.ogg b/sound/vo/automaton/nocpraise.ogg
new file mode 100644
index 00000000000..d32b351d27c
Binary files /dev/null and b/sound/vo/automaton/nocpraise.ogg differ
diff --git a/sound/vo/automaton/nowomenallowed.ogg b/sound/vo/automaton/nowomenallowed.ogg
new file mode 100644
index 00000000000..9ad49df981f
Binary files /dev/null and b/sound/vo/automaton/nowomenallowed.ogg differ
diff --git a/sound/vo/automaton/obnoxiouslylongscream.ogg b/sound/vo/automaton/obnoxiouslylongscream.ogg
new file mode 100644
index 00000000000..c9ec346df62
Binary files /dev/null and b/sound/vo/automaton/obnoxiouslylongscream.ogg differ
diff --git a/sound/vo/automaton/organicpresencedetected.ogg b/sound/vo/automaton/organicpresencedetected.ogg
new file mode 100644
index 00000000000..a138ac078bf
Binary files /dev/null and b/sound/vo/automaton/organicpresencedetected.ogg differ
diff --git a/sound/vo/automaton/pestrapraise.ogg b/sound/vo/automaton/pestrapraise.ogg
new file mode 100644
index 00000000000..25469f9ef1d
Binary files /dev/null and b/sound/vo/automaton/pestrapraise.ogg differ
diff --git a/sound/vo/automaton/ravoxpraise.ogg b/sound/vo/automaton/ravoxpraise.ogg
new file mode 100644
index 00000000000..7f351024248
Binary files /dev/null and b/sound/vo/automaton/ravoxpraise.ogg differ
diff --git a/sound/vo/automaton/schmelfdetected.ogg b/sound/vo/automaton/schmelfdetected.ogg
new file mode 100644
index 00000000000..770877d3fb9
Binary files /dev/null and b/sound/vo/automaton/schmelfdetected.ogg differ
diff --git a/sound/vo/automaton/silenceorganic.ogg b/sound/vo/automaton/silenceorganic.ogg
new file mode 100644
index 00000000000..3dbab50392b
Binary files /dev/null and b/sound/vo/automaton/silenceorganic.ogg differ
diff --git a/sound/vo/automaton/statuscritical.ogg b/sound/vo/automaton/statuscritical.ogg
new file mode 100644
index 00000000000..ae2aa4ec233
Binary files /dev/null and b/sound/vo/automaton/statuscritical.ogg differ
diff --git a/sound/vo/automaton/statuscritical2.ogg b/sound/vo/automaton/statuscritical2.ogg
new file mode 100644
index 00000000000..3840e690841
Binary files /dev/null and b/sound/vo/automaton/statuscritical2.ogg differ
diff --git a/sound/vo/automaton/tobones.ogg b/sound/vo/automaton/tobones.ogg
new file mode 100644
index 00000000000..5f3aeb4af2b
Binary files /dev/null and b/sound/vo/automaton/tobones.ogg differ
diff --git a/sound/vo/automaton/warning.ogg b/sound/vo/automaton/warning.ogg
new file mode 100644
index 00000000000..6e228379617
Binary files /dev/null and b/sound/vo/automaton/warning.ogg differ
diff --git a/sound/vo/automaton/wecannotexpectgodtodoallthework.ogg b/sound/vo/automaton/wecannotexpectgodtodoallthework.ogg
new file mode 100644
index 00000000000..945d27bdce2
Binary files /dev/null and b/sound/vo/automaton/wecannotexpectgodtodoallthework.ogg differ
diff --git a/sound/vo/automaton/womandetected.ogg b/sound/vo/automaton/womandetected.ogg
new file mode 100644
index 00000000000..890b6f11a0e
Binary files /dev/null and b/sound/vo/automaton/womandetected.ogg differ
diff --git a/sound/vo/automaton/wrenchbones.ogg b/sound/vo/automaton/wrenchbones.ogg
new file mode 100644
index 00000000000..f6911a3001e
Binary files /dev/null and b/sound/vo/automaton/wrenchbones.ogg differ
diff --git a/sound/vo/automaton/xylixpraise.ogg b/sound/vo/automaton/xylixpraise.ogg
new file mode 100644
index 00000000000..1a2d0682edf
Binary files /dev/null and b/sound/vo/automaton/xylixpraise.ogg differ
diff --git a/sound/vo/automaton/yourboneswillneverbefound.ogg b/sound/vo/automaton/yourboneswillneverbefound.ogg
new file mode 100644
index 00000000000..7f87d0a48f4
Binary files /dev/null and b/sound/vo/automaton/yourboneswillneverbefound.ogg differ
diff --git a/sound/vo/automaton/yourluxwillbemine.ogg b/sound/vo/automaton/yourluxwillbemine.ogg
new file mode 100644
index 00000000000..41b3d0515d6
Binary files /dev/null and b/sound/vo/automaton/yourluxwillbemine.ogg differ
diff --git a/vanderlin.dme b/vanderlin.dme
index 41ecd50a9b3..800729bfa8c 100644
--- a/vanderlin.dme
+++ b/vanderlin.dme
@@ -945,9 +945,11 @@
#include "code\datums\components\rotting.dm"
#include "code\datums\components\shrapnel.dm"
#include "code\datums\components\slippery.dm"
+#include "code\datums\components\slowing_field.dm"
#include "code\datums\components\soulstoned.dm"
#include "code\datums\components\spawner.dm"
#include "code\datums\components\squeak.dm"
+#include "code\datums\components\steam_life.dm"
#include "code\datums\components\steam_storage.dm"
#include "code\datums\components\stillness_timer.dm"
#include "code\datums\components\strong_pull.dm"
@@ -968,6 +970,18 @@
#include "code\datums\components\waddling.dm"
#include "code\datums\components\wearertargeting.dm"
#include "code\datums\components\wet_floor.dm"
+#include "code\datums\components\augments\augmentable.dm"
+#include "code\datums\components\augments\augment_datums\_base.dm"
+#include "code\datums\components\augments\augment_datums\skills.dm"
+#include "code\datums\components\augments\augment_datums\special.dm"
+#include "code\datums\components\augments\augment_datums\stats.dm"
+#include "code\datums\components\command_listener\_component.dm"
+#include "code\datums\components\command_listener\commands\_base.dm"
+#include "code\datums\components\command_listener\commands\custom.dm"
+#include "code\datums\components\command_listener\commands\follow.dm"
+#include "code\datums\components\command_listener\commands\guard.dm"
+#include "code\datums\components\command_listener\commands\kill.dm"
+#include "code\datums\components\command_listener\commands\protect.dm"
#include "code\datums\components\connect\connect_containers.dm"
#include "code\datums\components\connect\connect_loc_behalf.dm"
#include "code\datums\components\connect\connect_range.dm"
@@ -1225,7 +1239,9 @@
#include "code\datums\molten_materials\_base.dm"
#include "code\datums\molten_materials\metal_combine_recipes.dm"
#include "code\datums\nation\_base.dm"
+#include "code\datums\nation\_base_contract.dm"
#include "code\datums\nation\_base_trade.dm"
+#include "code\datums\nation\nation_ui.dm"
#include "code\datums\nation\showcase.dm"
#include "code\datums\particle_weathers\_base.dm"
#include "code\datums\particle_weathers\datum_types\fall_leaves.dm"
@@ -1618,6 +1634,7 @@
#include "code\game\objects\effects\temporary_visuals\temporary_visual.dm"
#include "code\game\objects\effects\temporary_visuals\projectiles\projectile_effects.dm"
#include "code\game\objects\effects\temporary_visuals\projectiles\tracer.dm"
+#include "code\game\objects\items\augment.dm"
#include "code\game\objects\items\bags.dm"
#include "code\game\objects\items\bait.dm"
#include "code\game\objects\items\beartraps.dm"
@@ -3170,6 +3187,9 @@
#include "code\modules\mob\living\carbon\human\npc\species_hostile\rakshari_hostile.dm"
#include "code\modules\mob\living\carbon\human\npc\species_hostile\tiefling_hostile.dm"
#include "code\modules\mob\living\carbon\human\npc\species_hostile\triton_hostile.dm"
+#include "code\modules\mob\living\carbon\human\species_types\automatons\_automaton.dm"
+#include "code\modules\mob\living\carbon\human\species_types\automatons\action.dm"
+#include "code\modules\mob\living\carbon\human\species_types\automatons\voicelines.dm"
#include "code\modules\mob\living\carbon\human\species_types\demihumans\_demihuman.dm"
#include "code\modules\mob\living\carbon\human\species_types\dwarf\_dwarf.dm"
#include "code\modules\mob\living\carbon\human\species_types\dwarf\dwarfm.dm"