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 += {" + + + + + + +
+

[name] National Information

+
+
REP: [nation_rep]
+
COMPLETED: [length(completed_trades)]
+
ACTIVE: [length(active_agreements)]
+
+
+ +
+
+
+
+
+ +
+
Active Agreements
+
+
+
+
+ +
+ + + +"} + + 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_2_4 + name = "Perfumed Oils" + required_trades = list(/datum/trade/node_1_4) - var/x_start = 500 - var/y_spacing = 350 - var/x_spacing = 400 +/datum/trade/node_2_5 + name = "Bubble Dynamics" + required_trades = list(/datum/trade/node_1_5) - var/list/node_positions = list() - var/tier = 0 - var/tier_count = 0 +/datum/trade/node_3_1 + name = "Heavy Duty Degreaser" + required_trades = list(/datum/trade/node_2_1) - for(var/datum/trade/T in nation.nodes) - var/node_x = x_start - var/node_y = 100 + (tier * y_spacing) +/datum/trade/node_3_2 + name = "Surgical Scrub" + required_trades = list(/datum/trade/node_2_3) - //x - if(T.required_trades && T.required_trades.len) - if(T.required_trades.len > 1) - node_x = x_start - else - node_x = x_start + x_spacing - tier++ - else - node_x = x_start + (tier_count * x_spacing) - tier_count++ - if(tier == 0) - tier = 1 +/datum/trade/node_3_3 + name = "Universal Solvent" + required_trades = list(/datum/trade/node_1_3) - node_positions["[T.type]"] = list("x" = node_x, "y" = node_y, "trade" = T) +/datum/trade/node_3_4 + name = "Luxury Aromatherapy" + required_trades = list(/datum/trade/node_2_4) - for(var/datum/trade/T in nation.nodes) - var/list/pos_data = node_positions["[T.type]"] +/datum/trade/node_3_5 + name = "Soap Carving" + required_trades = list(/datum/trade/node_2_5) - if(T.required_trades && T.required_trades.len) - for(var/req_path in T.required_trades) - if(node_positions["[req_path]"]) - var/list/req_pos = node_positions["[req_path]"] - var/from_x = req_pos["x"] + 150 - var/from_y = req_pos["y"] + 150 - var/to_x = pos_data["x"] + 150 - var/to_y = pos_data["y"] +/datum/trade/node_4_1 + name = "Bio-Clean Protocols" + required_trades = list(/datum/trade/node_3_1, /datum/trade/node_3_2) - dat += "" - dat += "" +/datum/trade/node_4_2 + name = "Antiseptic Mastery" + required_trades = list(/datum/trade/node_3_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"