From bee97d0a50f36450a36bac0f3ef74608b222a205 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:39:58 -0800 Subject: [PATCH 01/73] Update _automaton.dm --- .../living/carbon/human/species_types/automatons/_automaton.dm | 1 + 1 file changed, 1 insertion(+) 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 index a0e7907d1dc..d7a3b686dde 100644 --- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm +++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm @@ -111,6 +111,7 @@ . = ..() 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.AddComponent(/datum/component/augmentable) RegisterSignal(C, COMSIG_MOB_SAY, PROC_REF(handle_speech)) From abdde4c0ce00e3ef9b7dad5f3d32a0d7c1577c0d Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:14:34 -0800 Subject: [PATCH 02/73] adds in a ghost vessel component so that we can create possessable things based on an item attack, adds in craftable automatons --- code/datums/components/ghost_vessel.dm | 89 +++++++++++ .../crafting/quality_of_crafting/books.dm | 1 + .../crafting/slapcrafting/orderless/_base.dm | 5 +- .../species_types/automatons/_automaton.dm | 6 +- .../species_types/automatons/assembly.dm | 145 ++++++++++++++++++ vanderlin.dme | 2 + 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 code/datums/components/ghost_vessel.dm create mode 100644 code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm new file mode 100644 index 00000000000..d778e517abe --- /dev/null +++ b/code/datums/components/ghost_vessel.dm @@ -0,0 +1,89 @@ +/datum/component/ghost_vessel + var/obj/item/vessel_item_type // The item type that "unlocks" the mob + var/mob/living/carbon/human/owner + var/being_offered = FALSE + +/datum/component/ghost_vessel/Initialize(obj/item/item_type) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + owner = parent + vessel_item_type = item_type + + ADD_TRAIT(owner, TRAIT_STASIS, REF(src)) + ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) + ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) + + RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby)) + RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted)) + +/datum/component/ghost_vessel/Destroy() + if(owner) + REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src)) + REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) + REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) + owner = null + return ..() + +/datum/component/ghost_vessel/proc/on_parent_deleted(datum/source) + return + +/datum/component/ghost_vessel/proc/on_attackby(datum/source, obj/item/W, mob/living/user) + SIGNAL_HANDLER + if(!istype(W, vessel_item_type)) + return + if(being_offered) + return + qdel(W) + INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer)) + +/datum/component/ghost_vessel/proc/begin_ghost_offer() + being_offered = TRUE + + var/list/candidates = pollCandidatesForMob( + "A vessel at [owner.loc] awaits a soul. Do you wish to inhabit it?", + null, + null, + null, + 100, + parent, + POLL_IGNORE_GOLEM, + new_players = TRUE + ) + + if(length(candidates)) + var/mob/dead/observer/chosen = candidates[1] + possess_vessel(chosen) + else + owner.balloon_alert_to_viewers("This vessel awaits a soul...") + add_ghost_verb() + +/datum/component/ghost_vessel/proc/possess_vessel(mob/dead/observer/ghost) + if(!ghost?.client) + return + + being_offered = FALSE + REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src)) + REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) + REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) + owner.key = ghost.client.key + qdel(src) + +/datum/component/ghost_vessel/proc/add_ghost_verb() + RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine_by_ghost)) + +/datum/component/ghost_vessel/proc/on_examine_by_ghost(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + if(!istype(user, /mob/dead/observer)) + return + examine_list += span_notice("This vessel is empty. Inhabit it (Orbit Mob)?") + RegisterSignal(user, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(on_ghost_ctrl_click)) // alt: add a verb + +/datum/component/ghost_vessel/proc/on_ghost_ctrl_click(datum/source, mob/living/clicker) + var/option = browser_input_list(clicker, "Do you wish to possess this vessel??", "XYLIX", DEFAULT_INPUT_CHOICES) + if(!option) + return + if(!istype(clicker, /mob/dead/observer)) + return + if(!being_offered) + return + INVOKE_ASYNC(src, PROC_REF(possess_vessel), clicker) diff --git a/code/modules/crafting/quality_of_crafting/books.dm b/code/modules/crafting/quality_of_crafting/books.dm index e6d32899204..6168518c44a 100644 --- a/code/modules/crafting/quality_of_crafting/books.dm +++ b/code/modules/crafting/quality_of_crafting/books.dm @@ -845,6 +845,7 @@ /datum/repeatable_crafting_recipe/engineering, /datum/blueprint_recipe/engineering, /datum/artificer_recipe, + /datum/orderless_slapcraft/automaton, ) /obj/item/recipe_book/masonry diff --git a/code/modules/crafting/slapcrafting/orderless/_base.dm b/code/modules/crafting/slapcrafting/orderless/_base.dm index 62e02eb78cd..0a898aa25ca 100644 --- a/code/modules/crafting/slapcrafting/orderless/_base.dm +++ b/code/modules/crafting/slapcrafting/orderless/_base.dm @@ -16,6 +16,7 @@ var/datum/skill/related_skill var/skill_xp_gained var/action_time = 3 SECONDS + var/process_sound = 'sound/foley/dropsound/food_drop.ogg' ///list of atoms we pass to the output item var/list/atoms_to_pass = list() @@ -71,7 +72,7 @@ continue if(!do_after(user, modified_action_time, hosted_source)) return - playsound(user, 'sound/foley/dropsound/food_drop.ogg', 30, TRUE, -1) + playsound(user, process_sound, 30, TRUE, -1) requirements[requirement]-- if(requirements[requirement] <= 0) requirements -= list(requirement) // See Remove() behavior documentation @@ -87,7 +88,7 @@ if(istype(attacking_item, requirement)) if(!do_after(user, modified_action_time, hosted_source)) return - playsound(user, 'sound/foley/dropsound/food_drop.ogg', 30, TRUE, -1) + playsound(user, process_sound, 30, TRUE, -1) requirements[requirement]-- if(requirements[requirement] <= 0) requirements -= requirement 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 index d7a3b686dde..60e5e8be763 100644 --- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm +++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm @@ -2,6 +2,10 @@ race = /datum/species/automaton footstep_type = FOOTSTEP_MOB_METAL +/mob/living/carbon/human/species/automaton/vessel/LateInitialize() + . = ..() + AddComponent(/datum/component/ghost_vessel, /obj/item/reagent_containers/lux) + /datum/species/automaton name = "Automaton" id = SPEC_ID_AUTOMATON @@ -174,12 +178,10 @@ /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" diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm b/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm new file mode 100644 index 00000000000..601fdab31cf --- /dev/null +++ b/code/modules/mob/living/carbon/human/species_types/automatons/assembly.dm @@ -0,0 +1,145 @@ +/obj/item/automaton_frame + name = "automaton frame" + desc = "An unfinished brass skeleton, waiting to be given purpose. The joints are hollow, the chest cavity empty." + icon = 'icons/roguetown/mob/bodies/m/automaton.dmi' + icon_state = "chest_s" + w_class = WEIGHT_CLASS_HUGE + +/datum/repeatable_crafting_recipe/engineering/automaton_frame + name = "automaton frame" + category = "Automatons" + requirements = list( + /obj/item/ingot/bronze = 8, + /obj/item/ingot/iron = 4, + ) + tool_usage = list( + /obj/item/weapon/hammer = list( + span_notice("starts hammering a brass frame together"), + span_notice("start hammering a brass frame together"), + 'sound/items/bsmith2.ogg' + ), + ) + attacked_atom = /obj/item/ingot/bronze + starting_atom = /obj/item/weapon/hammer + output = /obj/item/automaton_frame + craft_time = 20 SECONDS + +/datum/repeatable_crafting_recipe/engineering/automaton_heart + name = "automaton steam engine" + category = "Automatons" + requirements = list( + /obj/item/ingot/copper = 3, + /obj/item/ingot/iron = 1, + /obj/item/rotation_contraption/boiler = 1, + ) + tool_usage = list( + /obj/item/weapon/hammer = list( + span_notice("starts assembling a miniature steam engine"), + span_notice("start assembling a miniature steam engine"), + 'sound/items/bsmith2.ogg' + ), + ) + attacked_atom = /obj/item/rotation_contraption/boiler + starting_atom = /obj/item/weapon/hammer + output = /obj/item/organ/heart/automaton + craft_time = 12 SECONDS + +/datum/repeatable_crafting_recipe/engineering/automaton_eyes + name = "automaton optical sensors" + category = "Automatons" + requirements = list( + /obj/item/ingot/bronze = 1, + /obj/item/ingot/copper = 1, + ) + tool_usage = list( + /obj/item/weapon/knife = list( + span_notice("starts grinding lenses for optical sensors"), + span_notice("start grinding lenses for optical sensors"), + 'sound/items/wood_sharpen.ogg' + ), + ) + attacked_atom = /obj/item/ingot/bronze + starting_atom = /obj/item/weapon/knife + output = /obj/item/organ/eyes/automaton + craft_time = 10 SECONDS + +/datum/orderless_slapcraft/automaton + name = "Automaton Assembly" + category = "Automatons" + related_skill = /datum/skill/craft/engineering + skill_xp_gained = 50 + action_time = 2 SECONDS + process_sound = 'sound/items/bsmith2.ogg' + + starting_item = /obj/item/automaton_frame + + requirements = list( + /obj/item/ingot/bronze = 5, + /obj/item/ingot/copper = 3, + /obj/item/ingot/iron = 2, + /obj/item/organ/eyes/automaton = 1, + /obj/item/gear/metal/bronze = 4, + ) + finishing_item = /obj/item/organ/heart/automaton + output_item = null // We spawn a mob instead, handled in try_finish + + var/list/installed_parts = list() + var/total_requirements = 0 + +/datum/orderless_slapcraft/automaton/New(loc, _source) + . = ..() + for(var/type in requirements) + total_requirements += requirements[type] + +/datum/orderless_slapcraft/automaton/step_process(mob/user, obj/item/attacking_item) + if(istype(attacking_item, /obj/item/ingot/bronze)) + user.visible_message(span_notice("[user] hammers brass plating onto the frame."), span_notice("You hammer the brass plating into place.")) + else if(istype(attacking_item, /obj/item/ingot/copper)) + user.visible_message(span_notice("[user] threads copper piping through the frame's chest."), span_notice("You thread copper piping through the chest cavity.")) + else if(istype(attacking_item, /obj/item/ingot/iron)) + user.visible_message(span_notice("[user] bolts iron joints into the frame."), span_notice("You bolt the iron joints firmly in place.")) + else if(istype(attacking_item, /obj/item/organ/heart/automaton)) + user.visible_message(span_notice("[user] carefully seats the steam engine into the frame's chest."), span_notice("You lower the steam engine into the chest cavity. It fits with a heavy clunk.")) + else if(istype(attacking_item, /obj/item/organ/eyes/automaton)) + user.visible_message(span_notice("[user] screws the optical sensors into the frame's skull."), span_notice("You screw the optical sensors into place. The lenses catch the light.")) + else if(istype(attacking_item, /obj/item/gear/metal/bronze)) + user.visible_message(span_notice("[user] clicks cogwheels into the frame's joints."), span_notice("You slot the cogwheels into the joint assemblies.")) + update_frame_overlays() + +/datum/orderless_slapcraft/automaton/proc/update_frame_overlays() + var/remaining = 0 + for(var/type in requirements) + remaining += requirements[type] + + var/progress = (total_requirements - remaining) / total_requirements // 0.0 to 1.0 could do 0 to 100 but eh same thing really + + var/list/stage_overlays = list( + "r_leg_s" = 0.15, + "l_leg_s" = 0.30, + "r_arm_s" = 0.50, + "l_arm_s" = 0.65, + "torso_s" = 0.80, + "head_s" = 0.95, + ) + + for(var/overlay in stage_overlays) + if(progress >= stage_overlays[overlay] && !(overlay in installed_parts)) + installed_parts += overlay + hosted_source.add_overlay(mutable_appearance(hosted_source.icon, overlay)) + +/datum/orderless_slapcraft/automaton/process_finishing_item(obj/item/attacking_item, mob/user) + user.visible_message( + span_notice("[user] presses the soul core into the automaton's chest. The runes flare — then go still."), + span_notice("You press the soul core into the chest cavity. The binding runes flare with cold light, then dim. Something stirs within the brass.") + ) + return FALSE + +/datum/orderless_slapcraft/automaton/try_finish(mob/user) + var/turf/T = get_turf(hosted_source) + qdel(hosted_source) + new /mob/living/carbon/human/species/automaton/vessel(T) + user.adjust_experience(related_skill, skill_xp_gained) + to_chat(user, span_notice("The automaton stands complete. It awaits a soul.")) + +/datum/orderless_slapcraft/automaton/handle_output_item(mob/user, obj/item/new_item) + return diff --git a/vanderlin.dme b/vanderlin.dme index b5b79f68b13..4da9f648af3 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -924,6 +924,7 @@ #include "code\datums\components\explodable.dm" #include "code\datums\components\food_burner.dm" #include "code\datums\components\generic_animal_hunger.dm" +#include "code\datums\components\ghost_vessel.dm" #include "code\datums\components\happiness.dm" #include "code\datums\components\happiness_system.dm" #include "code\datums\components\hideous_face.dm" @@ -3226,6 +3227,7 @@ #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\assembly.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" From 144644e68592663957b9db54f0ad20ba2f0eefcc Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:00:44 -0800 Subject: [PATCH 03/73] adds it to the latejoin menu --- code/datums/components/ghost_vessel.dm | 39 +++++++------------ .../modules/mob/dead/new_player/new_player.dm | 24 ++++++++++++ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index d778e517abe..13d7692d836 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -1,22 +1,28 @@ +GLOBAL_LIST_EMPTY(active_ghost_vessels) + /datum/component/ghost_vessel - var/obj/item/vessel_item_type // The item type that "unlocks" the mob + var/obj/item/vessel_item_type var/mob/living/carbon/human/owner var/being_offered = FALSE + var/vessel_id = "Automaton" -/datum/component/ghost_vessel/Initialize(obj/item/item_type) +/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = "Automaton") if(!isliving(parent)) return COMPONENT_INCOMPATIBLE owner = parent vessel_item_type = item_type - + vessel_id = id ADD_TRAIT(owner, TRAIT_STASIS, REF(src)) ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) - RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby)) RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted)) /datum/component/ghost_vessel/Destroy() + if(vessel_id && GLOB.active_ghost_vessels[vessel_id]) + GLOB.active_ghost_vessels[vessel_id] -= owner + if(!length(GLOB.active_ghost_vessels[vessel_id])) + GLOB.active_ghost_vessels -= vessel_id // clean up empty keys if(owner) REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src)) REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) @@ -55,7 +61,10 @@ possess_vessel(chosen) else owner.balloon_alert_to_viewers("This vessel awaits a soul...") - add_ghost_verb() + if(!GLOB.active_ghost_vessels[vessel_id]) + GLOB.active_ghost_vessels[vessel_id] = list() + GLOB.active_ghost_vessels[vessel_id] += owner // store the mob, not the component + /datum/component/ghost_vessel/proc/possess_vessel(mob/dead/observer/ghost) if(!ghost?.client) @@ -67,23 +76,3 @@ REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) owner.key = ghost.client.key qdel(src) - -/datum/component/ghost_vessel/proc/add_ghost_verb() - RegisterSignal(owner, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine_by_ghost)) - -/datum/component/ghost_vessel/proc/on_examine_by_ghost(datum/source, mob/user, list/examine_list) - SIGNAL_HANDLER - if(!istype(user, /mob/dead/observer)) - return - examine_list += span_notice("This vessel is empty. Inhabit it (Orbit Mob)?") - RegisterSignal(user, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(on_ghost_ctrl_click)) // alt: add a verb - -/datum/component/ghost_vessel/proc/on_ghost_ctrl_click(datum/source, mob/living/clicker) - var/option = browser_input_list(clicker, "Do you wish to possess this vessel??", "XYLIX", DEFAULT_INPUT_CHOICES) - if(!option) - return - if(!istype(clicker, /mob/dead/observer)) - return - if(!being_offered) - return - INVOKE_ASYNC(src, PROC_REF(possess_vessel), clicker) diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 007242194bf..2158714728b 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -180,6 +180,19 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt")) to_chat(usr, span_boldwarning("You are in the migrant queue.")) return + if(href_list["PossessVessel"]) + var/id = href_list["PossessVessel"] + var/list/group = GLOB.active_ghost_vessels[id] + if(!length(group)) + to_chat(src, span_warning("No vessels of that type are available.")) + return + var/mob/living/carbon/human/vessel_mob = pick(group) + var/datum/component/ghost_vessel/gc = vessel_mob.GetComponent(/datum/component/ghost_vessel) + if(!gc || !gc.being_offered) + to_chat(src, span_warning("That vessel is no longer available.")) + return + gc.possess_vessel(src) + if(href_list["late_join"]) if(!SSticker?.IsRoundInProgress()) to_chat(usr, "The game is starting. You cannot join yet.") @@ -570,6 +583,17 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt")) column_counter++ if(column_counter > 0 && (column_counter % 4 == 0)) dat += "" + if(length(GLOB.active_ghost_vessels)) + dat += "
" + dat += "Vessels" + for(var/id in GLOB.active_ghost_vessels) + var/count = length(GLOB.active_ghost_vessels[id]) + dat += "Join as [id] ([count] available)" + dat += "

" + column_counter++ + if(column_counter > 0 && (column_counter % 4 == 0)) + dat += "" + dat += "" dat += "" var/datum/browser/popup = new(src, "latechoices", "Choose Class", 720, 580) From a08fe1dfdd3799181a2e97658052cbccc175a68e Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:02:53 -0800 Subject: [PATCH 04/73] Update ghost_vessel.dm --- code/datums/components/ghost_vessel.dm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index 13d7692d836..3cd43a13196 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -15,6 +15,10 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) ADD_TRAIT(owner, TRAIT_STASIS, REF(src)) ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) + if(!vessel_item_type) + INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer)) + return + RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby)) RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted)) From 2164fbda4aa2adcf1dbf09b04c26c28caff90bca Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:20:48 -0800 Subject: [PATCH 05/73] adds whitelist system that we can use for later adds it to vessels --- code/__DEFINES/whitelists.dm | 1 + code/__HELPERS/game.dm | 22 +++ code/datums/components/ghost_vessel.dm | 9 +- code/modules/admin/admin.dm | 1 + code/modules/admin/granual_whitelist.dm | 135 ++++++++++++++++++ code/modules/admin/holder2.dm | 2 + code/modules/admin/topic.dm | 6 + .../modules/mob/dead/new_player/new_player.dm | 25 ++-- vanderlin.dme | 2 + 9 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 code/__DEFINES/whitelists.dm create mode 100644 code/modules/admin/granual_whitelist.dm diff --git a/code/__DEFINES/whitelists.dm b/code/__DEFINES/whitelists.dm new file mode 100644 index 00000000000..2b7c222b83a --- /dev/null +++ b/code/__DEFINES/whitelists.dm @@ -0,0 +1 @@ +#define WHITELIST_AUTOMATON "Automaton" diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index b0d4ebfaff0..b2b6642bc08 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -254,6 +254,22 @@ return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates) + +/proc/pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, new_players = FALSE, whitelist_type) + var/list/candidates = list() + + for(var/mob/dead/observer/G in GLOB.player_list) + if(G.client.is_whitelisted(whitelist_type)) + candidates += G + if(new_players) + for(var/mob/dead/new_player/G as anything in GLOB.new_player_list) + if(!G.client) + continue + candidates += G + + return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates) + + /proc/pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, list/group = null) var/time_passed = world.time if (!Question) @@ -286,6 +302,12 @@ return list() return L +/proc/pollCandidatesForMobWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, mob/M, ignore_category = null, new_players = FALSE, whitelist_type) + var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, wnitelist_type = whitelist_type) + if(!M || QDELETED(M) || !M.loc) + return list() + return L + /proc/pollCandidatesForMobs(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, list/mobs, ignore_category = null) var/list/L = pollGhostCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category) var/i=1 diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index 3cd43a13196..af1ff09f74b 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -4,9 +4,9 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) var/obj/item/vessel_item_type var/mob/living/carbon/human/owner var/being_offered = FALSE - var/vessel_id = "Automaton" + var/vessel_id = WHITELIST_AUTOMATON -/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = "Automaton") +/datum/component/ghost_vessel/Initialize(obj/item/item_type, id = WHITELIST_AUTOMATON) if(!isliving(parent)) return COMPONENT_INCOMPATIBLE owner = parent @@ -49,7 +49,7 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) /datum/component/ghost_vessel/proc/begin_ghost_offer() being_offered = TRUE - var/list/candidates = pollCandidatesForMob( + var/list/candidates = pollCandidatesForMobWhitelisted( "A vessel at [owner.loc] awaits a soul. Do you wish to inhabit it?", null, null, @@ -57,7 +57,8 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) 100, parent, POLL_IGNORE_GOLEM, - new_players = TRUE + new_players = TRUE, + whitelist_type = vessel_id, ) if(length(candidates)) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 326bd5005ed..343b37aa246 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -114,6 +114,7 @@ body += "\[Check Triumphs\] " body += "
" body += "\[Role Ban Panel\] " + body += "Whitelists - " var/patron = "" if(isliving(M)) diff --git a/code/modules/admin/granual_whitelist.dm b/code/modules/admin/granual_whitelist.dm new file mode 100644 index 00000000000..80cae97d622 --- /dev/null +++ b/code/modules/admin/granual_whitelist.dm @@ -0,0 +1,135 @@ +/datum/whitelist_panel + var/datum/admins/holder + var/selected_ckey = null + +/datum/whitelist_panel/New(datum/admins/passed_holder) + holder = passed_holder + return ..() + +/datum/whitelist_panel/proc/show_ui(mob/user, forced_key) + if(forced_key) + selected_ckey = forced_key + + var/list/dat = list() + dat += "
Whitelist Panel

" + dat += "CKEY: [selected_ckey] Change

" + + if(selected_ckey) + dat += "Current Whitelists for [selected_ckey]:
" + var/list/all_whitelists = get_all_whitelist_ids() + for(var/wl_id in all_whitelists) + var/datum/save_manager/SM = get_save_manager(selected_ckey) + var/data = SM ? SM.get_data("whitelists", wl_id, null) : null + var/has_wl = islist(data) && data["granted"] + dat += " - [wl_id]: [has_wl ? "Granted" : "Not Granted"]" + if(islist(data)) + if(has_wl) + dat += " (by [data["granted_by"]] on [time2text(data["granted_on"], "DD/MM/YYYY")])" + else if(data["granted_by"]) + dat += " (revoked by [data["granted_by"]] on [time2text(data["revoked_on"], "DD/MM/YYYY")])" + if(has_wl) + dat += " Remove" + else + dat += " Grant" + dat += "
" + + dat += "
Add Custom Whitelist ID" + + var/datum/browser/popup = new(user, "whitelist_panel", "Whitelist Panel", 400, 400) + popup.set_content(dat.Join()) + popup.open() + +/datum/whitelist_panel/proc/get_all_whitelist_ids() + return list( + WHITELIST_AUTOMATON, + //as we add more we can fill it out here + ) + +/datum/whitelist_panel/Topic(href, list/href_list) + . = ..() + if(!holder) + return + var/mob/user = usr + if(holder.owner != user.client) + return + if(!check_rights_for(user.client, R_ADMIN)) + to_chat(user, span_boldwarning("No admin permission")) + return + + switch(href_list["task"]) + if("ckey") + var/chosen_ckey = input(user, "Enter ckey", "CKEY", selected_ckey) as text|null + if(!chosen_ckey) + return + selected_ckey = ckey(chosen_ckey) + + if("add_whitelist") + if(!selected_ckey) + to_chat(user, span_boldwarning("No ckey selected.")) + return + var/wl_id = href_list["wl_id"] + grant_whitelist(user, selected_ckey, wl_id) + + if("remove_whitelist") + if(!selected_ckey) + to_chat(user, span_boldwarning("No ckey selected.")) + return + var/wl_id = href_list["wl_id"] + revoke_whitelist(user, selected_ckey, wl_id) + + if("add_custom") + if(!selected_ckey) + to_chat(user, span_boldwarning("No ckey selected.")) + return + var/wl_id = input(user, "Enter custom whitelist ID (must match vessel_id exactly)", "Custom Whitelist", "") as text|null + if(!wl_id) + return + grant_whitelist(user, selected_ckey, wl_id) + + show_ui(user) + +/datum/whitelist_panel/proc/grant_whitelist(mob/user, target_ckey, wl_id) + var/datum/save_manager/SM = get_save_manager(target_ckey) + if(!SM) + to_chat(user, span_boldwarning("Could not load save manager for [target_ckey].")) + return + SM.set_data("whitelists", wl_id, list( + "granted" = TRUE, + "granted_by" = ckey(user.ckey), + "granted_on" = world.realtime + )) + var/msg = "[key_name_admin(user)] granted whitelist '[wl_id]' to [target_ckey]" + message_admins(msg) + log_admin(msg) + +/datum/whitelist_panel/proc/revoke_whitelist(mob/user, target_ckey, wl_id) + var/datum/save_manager/SM = get_save_manager(target_ckey) + if(!SM) + to_chat(user, span_boldwarning("Could not load save manager for [target_ckey].")) + return + SM.set_data("whitelists", wl_id, list( + "granted" = FALSE, + "granted_by" = ckey(user.ckey), + "revoked_on" = world.realtime + )) + var/msg = "[key_name_admin(user)] revoked whitelist '[wl_id]' from [target_ckey]" + message_admins(msg) + log_admin(msg) + +/client/proc/is_whitelisted(whitelist_id) + var/datum/save_manager/SM = get_save_manager(ckey) + if(!SM) + return FALSE + var/data = SM.get_data("whitelists", whitelist_id, null) + if(!islist(data)) + return FALSE + return data["granted"] + +/proc/is_whitelisted_for(target_ckey, whitelist_id) + var/datum/save_manager/SM = get_save_manager(target_ckey) + if(!SM) + return FALSE + var/data = SM.get_data("whitelists", whitelist_id, null) + if(!islist(data)) + return FALSE + return data["granted"] diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index df331b46a89..11b5d0134ab 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -25,6 +25,7 @@ GLOBAL_PROTECT(href_token) var/deadmined var/datum/role_ban_panel/role_ban_panel + var/datum/whitelist_panel/WP var/datum/pathfind_debug/path_debug var/datum/create_wave/create_wave @@ -49,6 +50,7 @@ GLOBAL_PROTECT(href_token) admin_signature = "Nanotrasen Officer #[rand(0,9)][rand(0,9)][rand(0,9)]" href_token = GenerateToken() role_ban_panel = new /datum/role_ban_panel(src) + WP = new /datum/whitelist_panel(src) if(R.rights & R_DEBUG) //grant profile access world.SetConfig("APP/admin", ckey, "role=admin") //only admins with +ADMIN start admined diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index d099fb661a5..983862b307a 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1329,6 +1329,12 @@ mind_of_mob.set_assigned_role(new_job) + else if(href_list["open_whitelist_panel"]) + var/mob/M = locate(href_list["open_whitelist_panel"]) + if(!M?.ckey) + return + WP.show_ui(usr, ckey(M.ckey)) + else if(href_list["roleban"]) if(!check_rights(R_ADMIN)) return diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 2158714728b..5f3ea967431 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -182,6 +182,9 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt")) if(href_list["PossessVessel"]) var/id = href_list["PossessVessel"] + if(!client.is_whitelisted(id)) + to_chat(src, span_boldwarning("You are not whitelisted for [id].")) + return var/list/group = GLOB.active_ghost_vessels[id] if(!length(group)) to_chat(src, span_warning("No vessels of that type are available.")) @@ -584,15 +587,21 @@ GLOBAL_LIST_INIT(roleplay_readme, world.file2list("strings/rt/Lore_Primer.txt")) if(column_counter > 0 && (column_counter % 4 == 0)) dat += "" if(length(GLOB.active_ghost_vessels)) - dat += "
" - dat += "Vessels" + var/list/available_vessel_ids = list() for(var/id in GLOB.active_ghost_vessels) - var/count = length(GLOB.active_ghost_vessels[id]) - dat += "Join as [id] ([count] available)" - dat += "

" - column_counter++ - if(column_counter > 0 && (column_counter % 4 == 0)) - dat += "" + if(client.is_whitelisted(id)) + available_vessel_ids += id + + if(length(available_vessel_ids)) + dat += "
" + dat += "Vessels" + for(var/id in available_vessel_ids) + var/count = length(GLOB.active_ghost_vessels[id]) + dat += "Join as [id] ([count] available)" + dat += "

" + column_counter++ + if(column_counter > 0 && (column_counter % 4 == 0)) + dat += "" dat += "" dat += "" diff --git a/vanderlin.dme b/vanderlin.dme index 4da9f648af3..60bb29940be 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -195,6 +195,7 @@ #include "code\__DEFINES\weaponsounds.dm" #include "code\__DEFINES\weights.dm" #include "code\__DEFINES\werewolf.dm" +#include "code\__DEFINES\whitelists.dm" #include "code\__DEFINES\wires.dm" #include "code\__DEFINES\ai\_ai.dm" #include "code\__DEFINES\ai\hostile.dm" @@ -2028,6 +2029,7 @@ #include "code\modules\admin\create_turf.dm" #include "code\modules\admin\create_wave.dm" #include "code\modules\admin\fun_balloon.dm" +#include "code\modules\admin\granual_whitelist.dm" #include "code\modules\admin\greyscale_modifiy_menu.dm" #include "code\modules\admin\holder2.dm" #include "code\modules\admin\IsBanned.dm" From bba441d3118720ed5cf3ffc4675a859406d2ffbc Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:21:59 -0800 Subject: [PATCH 06/73] adds a prefilled variant of the vessel --- .../carbon/human/species_types/automatons/_automaton.dm | 4 ++++ 1 file changed, 4 insertions(+) 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 index 60e5e8be763..d32a1dec844 100644 --- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm +++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm @@ -6,6 +6,10 @@ . = ..() AddComponent(/datum/component/ghost_vessel, /obj/item/reagent_containers/lux) +/mob/living/carbon/human/species/automaton/prefilled_vessel/LateInitialize() + . = ..() + AddComponent(/datum/component/ghost_vessel) + /datum/species/automaton name = "Automaton" id = SPEC_ID_AUTOMATON From c56e2c09855b09f79a9f5346884bd5ec7b5ca122 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:23:25 -0800 Subject: [PATCH 07/73] Update ghost_vessel.dm --- code/datums/components/ghost_vessel.dm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index af1ff09f74b..c8107bb53f5 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -16,7 +16,11 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) ADD_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) ADD_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) if(!vessel_item_type) - INVOKE_ASYNC(src, PROC_REF(begin_ghost_offer)) + being_offered = TRUE + owner.balloon_alert_to_viewers("This vessel awaits a soul...") + if(!GLOB.active_ghost_vessels[vessel_id]) + GLOB.active_ghost_vessels[vessel_id] = list() + GLOB.active_ghost_vessels[vessel_id] += owner // store the mob, not the component return RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby)) From 9328b5e8e8ca9f7adfa7b714bcf7777f402a19fb Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:27:11 -0800 Subject: [PATCH 08/73] Update game.dm --- code/__HELPERS/game.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index b2b6642bc08..850855c0f1c 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -303,7 +303,7 @@ return L /proc/pollCandidatesForMobWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag = 0, poll_time = 300, mob/M, ignore_category = null, new_players = FALSE, whitelist_type) - var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, wnitelist_type = whitelist_type) + var/list/L = pollGhostCandidatesWhitelisted(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, new_players = new_players, whitelist_type = whitelist_type) if(!M || QDELETED(M) || !M.loc) return list() return L From 8eace221aa0d5b088a913fec1179d3f951837970 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:34:19 -0800 Subject: [PATCH 09/73] Update vanderlin.dmm --- _maps/map_files/vanderlin/vanderlin.dmm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm index 375113e0518..7cf87f294c0 100644 --- a/_maps/map_files/vanderlin/vanderlin.dmm +++ b/_maps/map_files/vanderlin/vanderlin.dmm @@ -9143,6 +9143,10 @@ }, /turf/open/floor/ruinedwood/spiral, /area/indoors/town/theatre) +"etk" = ( +/mob/living/carbon/human/species/automaton/prefilled_vessel, +/turf/open/floor/blocks/stonered/tiny, +/area/under/town/basement) "etw" = ( /obj/machinery/light/fueled/firebowl, /turf/open/floor/ruinedwood/darker, @@ -29490,7 +29494,6 @@ /obj/structure/door/violet{ name = "Royal Guard's Storage" }, -/obj/effect/mapping_helpers/access/locker, /obj/effect/mapping_helpers/access/keyset/manor/atarms, /turf/open/floor/cobble, /area/under/town/basement) @@ -76700,7 +76703,7 @@ qFx qFx dHC vpQ -dIz +etk jTO oqo jTO From cfc3f2a67d105bcb21386e4d2c829447c1613de6 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:48:46 -0800 Subject: [PATCH 10/73] adds job boost and roundstart support to vessel targets --- _maps/map_files/debug/roguetest.dmm | 6 +- code/controllers/subsystem/ticker.dm | 63 ++++++- code/modules/admin/granual_whitelist.dm | 4 +- .../client/preferences/_preferences.dm | 173 +++++++++--------- code/modules/client/save_system/job_boosts.dm | 7 + 5 files changed, 163 insertions(+), 90 deletions(-) diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm index d9504e83d6a..814d0b18a0c 100644 --- a/_maps/map_files/debug/roguetest.dmm +++ b/_maps/map_files/debug/roguetest.dmm @@ -1403,6 +1403,10 @@ /obj/structure/closet/crate/miningcar, /turf/open/floor/grass/yel, /area/outdoors) +"ke" = ( +/mob/living/carbon/human/species/automaton/prefilled_vessel, +/turf/open/floor/wood, +/area/indoors/town/keep) "ki" = ( /obj/structure/rotation_piece/cog{ dir = 4 @@ -16632,7 +16636,7 @@ QN Wr QN zg -GB +ke GB vD vD diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index eab98c59c2c..ce9dfa69d8d 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -318,16 +318,16 @@ SUBSYSTEM_DEF(ticker) var/init_start = world.timeofday CHECK_TICK - //Configure mode and assign player to special mode stuff - var/can_continue = 0 - CHECK_TICK + // Vessel assignment happens first, removes those players from the pool + assign_vessel_players() - can_continue = SSgamemode.pre_setup() + CHECK_TICK + var/can_continue = SSgamemode.pre_setup() CHECK_TICK - can_continue = can_continue && SSjob.DivideOccupations(list()) //Distribute jobs + can_continue = can_continue && SSjob.DivideOccupations(list()) //Distribute jobs CHECK_TICK log_game("GAME SETUP: Divide Occupations success") @@ -399,6 +399,59 @@ SUBSYSTEM_DEF(ticker) return TRUE +/datum/controller/subsystem/ticker/proc/assign_vessel_players() + var/list/vessel_candidates = list() + + for(var/mob/dead/new_player/player in GLOB.new_player_list) + if(!player?.client) + continue + if(player.ready != PLAYER_READY_TO_PLAY) + continue + for(var/id in GLOB.vessel_ids) + if(!(id in player.client.prefs.be_special)) + continue + if(!player.client.is_whitelisted(id)) + continue + if(!vessel_candidates[id]) + vessel_candidates[id] = list() + vessel_candidates[id] += player + break + + for(var/id in vessel_candidates) + var/list/vessel_mobs = GLOB.active_ghost_vessels[id] + if(!length(vessel_mobs)) + continue + + // Build weighted list using boost system, respecting vessel_id + var/list/weighted_players = list() + for(var/mob/dead/new_player/player in vessel_candidates[id]) + var/player_weight = 1 + for(var/datum/job_priority_boost/boost in SSjob.get_player_boosts(player)) + if(boost.can_boost_vessel(id)) + player_weight += boost.boost_amount + weighted_players[player] = player_weight + + while(length(weighted_players) && length(vessel_mobs)) + var/mob/dead/new_player/player = pickweight(weighted_players) + weighted_players -= player + + var/mob/living/carbon/human/vessel_mob = pick(vessel_mobs) + var/datum/component/ghost_vessel/gc = vessel_mob.GetComponent(/datum/component/ghost_vessel) + if(!gc) + vessel_mobs -= vessel_mob + continue + + // Consume the first applicable boost, same as DO does + for(var/datum/job_priority_boost/boost in SSjob.get_player_boosts(player)) + if(boost.can_boost_vessel(id)) + boost.use_boost() + break + + gc.possess_vessel(player) + vessel_mobs -= vessel_mob + GLOB.new_player_list -= player + log_game("Assigned [player.ckey] to vessel '[id]' ([vessel_mob.name])") + /datum/controller/subsystem/ticker/proc/PostSetup() set waitfor = FALSE diff --git a/code/modules/admin/granual_whitelist.dm b/code/modules/admin/granual_whitelist.dm index 80cae97d622..1e352d4650c 100644 --- a/code/modules/admin/granual_whitelist.dm +++ b/code/modules/admin/granual_whitelist.dm @@ -1,3 +1,5 @@ +GLOBAL_LIST_INIT(vessel_ids, list(WHITELIST_AUTOMATON)) + /datum/whitelist_panel var/datum/admins/holder var/selected_ckey = null @@ -16,7 +18,7 @@ if(selected_ckey) dat += "Current Whitelists for [selected_ckey]:
" - var/list/all_whitelists = get_all_whitelist_ids() + var/list/all_whitelists = GLOB.vessel_ids for(var/wl_id in all_whitelists) var/datum/save_manager/SM = get_save_manager(selected_ckey) var/data = SM ? SM.get_data("whitelists", wl_id, null) : null diff --git a/code/modules/client/preferences/_preferences.dm b/code/modules/client/preferences/_preferences.dm index cffa9b168ae..f15740bf2d4 100644 --- a/code/modules/client/preferences/_preferences.dm +++ b/code/modules/client/preferences/_preferences.dm @@ -439,8 +439,8 @@ GLOBAL_LIST_INIT(name_adjustments, list()) .v-color-box { top: 136px; left: 34px; width: 48px; height: 15px; background-image: url('voice_colour.png'); } .v-blob { top: 4px; left: 35px; width: 8px; height: 7px; - background-image: url('voice_colour_blob.png'); - background-blend-mode: multiply; } + background-image: url('voice_colour_blob.png'); + background-blend-mode: multiply; } .menu-keybinds { top: 280px; @@ -559,7 +559,7 @@ GLOBAL_LIST_INIT(name_adjustments, list()) var silhouette = document.getElementById('silhouette'); silhouette.style.backgroundImage = "url('features_bodytype_" + data.gender + ".png')"; if (data.gender === "F") silhouette.style.width = "15px"; - if (data.gender === "M") silhouette.style.width = "18px"; + if (data.gender === "M") silhouette.style.width = "18px"; } // Update voice color blob @@ -1108,49 +1108,49 @@ GLOBAL_LIST_INIT(name_adjustments, list()) /datum/preferences/proc/update_job_preference(mob/user, role, desiredLvl) - if(!SSjob || !length(SSjob.joinable_occupations)) - return - var/datum/job/job = SSjob.GetJob(role) - if(!job || !(job.job_flags & JOB_NEW_PLAYER_JOINABLE)) - user << browse(null, "window=mob_occupation") - update_menu_data(user, list("job")) - return - if(!isnum(desiredLvl)) - to_chat(user, "update_job_preference - desired level was not a number. Please notify coders!") - CRASH("update_job_preference called with desiredLvl value of [isnull(desiredLvl) ? "null" : desiredLvl]") - - var/jpval = null - // desiredLvl comes from the links: 1=High, 2=Medium, 3=Low, 4=NEVER - // JP constants: JP_LOW=1, JP_MEDIUM=2, JP_HIGH=3 - switch(desiredLvl) - if(1) - jpval = JP_HIGH // 3 - if(2) - jpval = JP_MEDIUM // 2 - if(3) - jpval = JP_LOW // 1 - if(4) - jpval = null // NEVER - - var/was_high = (jpval == JP_HIGH) - var/previous_high_job = null - - if(was_high) - for(var/job_title in job_preferences) - if(job_preferences[job_title] == JP_HIGH) - previous_high_job = job_title - break - - set_job_preference_level(job, jpval) - - // Send back the desiredLvl value directly since that's what JavaScript expects - update_job_display(user, role, desiredLvl) - - if(was_high && previous_high_job && previous_high_job != role) - update_job_display(user, previous_high_job, 2) // Medium - - update_menu_data(user, list("job")) - return 1 + if(!SSjob || !length(SSjob.joinable_occupations)) + return + var/datum/job/job = SSjob.GetJob(role) + if(!job || !(job.job_flags & JOB_NEW_PLAYER_JOINABLE)) + user << browse(null, "window=mob_occupation") + update_menu_data(user, list("job")) + return + if(!isnum(desiredLvl)) + to_chat(user, "update_job_preference - desired level was not a number. Please notify coders!") + CRASH("update_job_preference called with desiredLvl value of [isnull(desiredLvl) ? "null" : desiredLvl]") + + var/jpval = null + // desiredLvl comes from the links: 1=High, 2=Medium, 3=Low, 4=NEVER + // JP constants: JP_LOW=1, JP_MEDIUM=2, JP_HIGH=3 + switch(desiredLvl) + if(1) + jpval = JP_HIGH // 3 + if(2) + jpval = JP_MEDIUM // 2 + if(3) + jpval = JP_LOW // 1 + if(4) + jpval = null // NEVER + + var/was_high = (jpval == JP_HIGH) + var/previous_high_job = null + + if(was_high) + for(var/job_title in job_preferences) + if(job_preferences[job_title] == JP_HIGH) + previous_high_job = job_title + break + + set_job_preference_level(job, jpval) + + // Send back the desiredLvl value directly since that's what JavaScript expects + update_job_display(user, role, desiredLvl) + + if(was_high && previous_high_job && previous_high_job != role) + update_job_display(user, previous_high_job, 2) // Medium + + update_menu_data(user, list("job")) + return 1 /datum/preferences/proc/reset_jobs(mob/user, silent = FALSE) job_preferences = list() @@ -1270,31 +1270,38 @@ GLOBAL_LIST_INIT(name_adjustments, list()) /datum/preferences/proc/set_antag(mob/user) var/list/dat = list() - dat += "" dat += "
Done
" dat += "

Villains

" - if(is_total_antag_banned(user.ckey)) dat += "I am banned from antagonist roles.
" src.be_special = list() - for (var/i in GLOB.special_roles_rogue) if(is_antag_banned(user.ckey, i)) dat += "[capitalize(i)]: BANNED
" else var/days_remaining = null - if(ispath(GLOB.special_roles_rogue[i]) && CONFIG_GET(flag/use_age_restriction_for_jobs)) //If it's a game mode antag, check if the player meets the minimum age + if(ispath(GLOB.special_roles_rogue[i]) && CONFIG_GET(flag/use_age_restriction_for_jobs)) days_remaining = get_remaining_days(user.client) - if(days_remaining) - dat += "[capitalize(i)]: \[IN [days_remaining] DAYS\]
" + dat += "[capitalize(i)]: \[IN [days_remaining] DAYS__~~\]~~__
" else dat += "[capitalize(i)]: [(i in be_special) ? "Enabled" : "Disabled"]
" - dat += "" + var/list/vessel_ids = GLOB.vessel_ids + var/list/available_vessel_ids = list() + for(var/id in vessel_ids) + if(user.client.is_whitelisted(id)) + available_vessel_ids += id + + if(length(available_vessel_ids)) + dat += "

Vessels

" + for(var/id in available_vessel_ids) + var/enabled = (id in be_special) + dat += "[id]: [enabled ? "Enabled" : "Disabled"]
" - var/datum/browser/noclose/popup = new(user, "antag_setup", "
Special Roles
", 265, 340) //no reason not to reuse the occupation window, as it's cleaner that way + dat += "" + var/datum/browser/noclose/popup = new(user, "antag_setup", "
Special Roles
", 265, 340) popup.set_window_options(can_close = FALSE) popup.set_content(dat.Join()) popup.open(FALSE) @@ -2384,22 +2391,22 @@ GLOBAL_LIST_INIT(name_adjustments, list()) @@ -2411,19 +2418,19 @@ GLOBAL_LIST_INIT(name_adjustments, list()) diff --git a/code/modules/client/save_system/job_boosts.dm b/code/modules/client/save_system/job_boosts.dm index 53b5ae12acc..1201272756a 100644 --- a/code/modules/client/save_system/job_boosts.dm +++ b/code/modules/client/save_system/job_boosts.dm @@ -52,6 +52,13 @@ return FALSE return TRUE +/datum/job_priority_boost/proc/can_boost_vessel(vessel_id) + if(!is_valid()) + return FALSE + if(length(applicable_jobs) && !(vessel_id in applicable_jobs)) + return FALSE + return TRUE + /datum/job_priority_boost/proc/use_boost() if(uses_remaining > 0) uses_remaining-- From 1c0923cc79744fc36413bf4f2be6860495aef4d1 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:49:58 -0800 Subject: [PATCH 11/73] Update ticker.dm --- code/controllers/subsystem/ticker.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index ce9dfa69d8d..ff970f69aca 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -447,6 +447,7 @@ SUBSYSTEM_DEF(ticker) boost.use_boost() break + player.stop_sound_channel(CHANNEL_LOBBYMUSIC) gc.possess_vessel(player) vessel_mobs -= vessel_mob GLOB.new_player_list -= player From 194bc9a1b29f69d37940edf99609c6254ab6f1a7 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:52:18 -0800 Subject: [PATCH 12/73] Update ghost_vessel.dm --- code/datums/components/ghost_vessel.dm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index c8107bb53f5..3562f9b6505 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -84,4 +84,7 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) owner.key = ghost.client.key + var/new_name = browser_input_text(owner, "Choose a new Name", "New Name", owner.name) + if(new_name) + owner.real_name = new_name qdel(src) From 519059eb8886f7ee5fc0f3097bb2897696ec44de Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:57:01 -0800 Subject: [PATCH 13/73] Update job_boosts.dm --- code/modules/client/save_system/job_boosts.dm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/modules/client/save_system/job_boosts.dm b/code/modules/client/save_system/job_boosts.dm index 1201272756a..aa9cc66d1b5 100644 --- a/code/modules/client/save_system/job_boosts.dm +++ b/code/modules/client/save_system/job_boosts.dm @@ -17,6 +17,12 @@ ..() expiry_time = world.time + (duration_hours * 36000) // SS13 time conversion +/datum/job_priority_boost/automaton_15 + name = "Automaton Vessel Boost" + desc = "Increases your odds of rolling Automaton for a brief period" + boost_amount = 10 + uses_remaining = 15 + /datum/job_priority_boost/minor name = "Minor Job Boost" desc = "Slightly increases job priority" From 3e3025fbca8e6d906ed3110265abd895600b2b5c Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:57:15 -0800 Subject: [PATCH 14/73] Update job_boosts.dm --- code/modules/client/save_system/job_boosts.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/modules/client/save_system/job_boosts.dm b/code/modules/client/save_system/job_boosts.dm index aa9cc66d1b5..723cc030963 100644 --- a/code/modules/client/save_system/job_boosts.dm +++ b/code/modules/client/save_system/job_boosts.dm @@ -22,6 +22,7 @@ desc = "Increases your odds of rolling Automaton for a brief period" boost_amount = 10 uses_remaining = 15 + applicable_jobs = list(WHITELIST_AUTOMATON) /datum/job_priority_boost/minor name = "Minor Job Boost" From 36d06abb004b7685a074a94f512d3140487ab892 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:08:42 -0800 Subject: [PATCH 15/73] better --- code/controllers/subsystem/ticker.dm | 2 +- code/modules/admin/admin.dm | 1 + code/modules/admin/holder2.dm | 2 + code/modules/admin/topic.dm | 6 + code/modules/client/save_system/job_boosts.dm | 271 ++++++++++++++++++ 5 files changed, 281 insertions(+), 1 deletion(-) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index ff970f69aca..75afea68c11 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -448,7 +448,7 @@ SUBSYSTEM_DEF(ticker) break player.stop_sound_channel(CHANNEL_LOBBYMUSIC) - gc.possess_vessel(player) + INVOKE_ASYNC(gc, TYPE_PROC_REF(/datum/component/ghost_vessel, possess_vessel), player) vessel_mobs -= vessel_mob GLOB.new_player_list -= player log_game("Assigned [player.ckey] to vessel '[id]' ([vessel_mob.name])") diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 343b37aa246..aaca5bb20db 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -115,6 +115,7 @@ body += "
" body += "\[Role Ban Panel\] " body += "Whitelists - " + body += "JOB BOOST - " var/patron = "" if(isliving(M)) diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index 11b5d0134ab..30c9c8d3af8 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -26,6 +26,7 @@ GLOBAL_PROTECT(href_token) var/deadmined var/datum/role_ban_panel/role_ban_panel var/datum/whitelist_panel/WP + var/datum/job_boost_panel/BP var/datum/pathfind_debug/path_debug var/datum/create_wave/create_wave @@ -51,6 +52,7 @@ GLOBAL_PROTECT(href_token) href_token = GenerateToken() role_ban_panel = new /datum/role_ban_panel(src) WP = new /datum/whitelist_panel(src) + BP = new /datum/job_boost_panel(src) if(R.rights & R_DEBUG) //grant profile access world.SetConfig("APP/admin", ckey, "role=admin") //only admins with +ADMIN start admined diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 983862b307a..b8cc714510b 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1335,6 +1335,12 @@ return WP.show_ui(usr, ckey(M.ckey)) + if(href_list["open_boost_panel"]) + var/mob/M = locate(href_list["open_boost_panel"]) + if(!M?.ckey) + return + BP.show_ui(usr, ckey(M.ckey)) + else if(href_list["roleban"]) if(!check_rights(R_ADMIN)) return diff --git a/code/modules/client/save_system/job_boosts.dm b/code/modules/client/save_system/job_boosts.dm index 723cc030963..1dd2ecc5491 100644 --- a/code/modules/client/save_system/job_boosts.dm +++ b/code/modules/client/save_system/job_boosts.dm @@ -200,3 +200,274 @@ boost.applicable_jobs = applicable_jobs return SSjob.give_job_boost(target_client, boost) + +/datum/job_boost_panel + var/datum/admins/holder + var/selected_ckey = null + + var/selected_boost_type = "general" + var/selected_boost_amount = 1 + var/selected_uses = -1 + var/selected_expiry_hours = 0 + var/list/selected_applicable = list() + +/datum/job_boost_panel/New(datum/admins/passed_holder) + holder = passed_holder + return ..() + +/datum/job_boost_panel/proc/show_ui(mob/user, forced_key) + if(forced_key) + selected_ckey = forced_key + + var/list/dat = list() + dat += "
Job Boost Panel

" + dat += "CKEY: [selected_ckey ? selected_ckey : "None"] Change

" + + if(selected_ckey) + var/client/target_client = GLOB.directory[selected_ckey] + + dat += "Active Boosts for [selected_ckey]:" + if(!target_client) + dat += " (offline — boosts cannot be managed)" + dat += "
" + + if(target_client && islist(target_client.job_priority_boosts) && length(target_client.job_priority_boosts)) + var/i = 1 + for(var/datum/job_priority_boost/boost in target_client.job_priority_boosts) + var/validity = boost.is_valid() ? "Valid" : "Expired" + var/expiry_info = boost.expiry_time > 0 ? "expires [time2text(boost.expiry_time, "DD/MM/YYYY")]" : "no expiry" + var/uses_info = boost.uses_remaining >= 0 ? "[boost.uses_remaining] uses left" : "unlimited" + var/applicable_info = length(boost.applicable_jobs) ? " ([boost.applicable_jobs.Join(", ")])" : "" + dat += "  [boost.name][applicable_info] - x[boost.boost_amount], [expiry_info], [uses_info]: [validity]" + dat += " Remove
" + i++ + else + dat += "  No active boosts[!target_client ? " (player offline)" : ""].
" + + dat += "
" + + dat += "Build New Boost:
" + + var/list/boost_types = list("general", "minor", "major", "premium", "timed", "limited") + dat += "  Type: " + for(var/bt in boost_types) + if(bt == selected_boost_type) + dat += "[bt] " + else + dat += "[bt] " + dat += "
" + + dat += "  Amount: [selected_boost_amount] " + dat += "+ " + dat += "- " + dat += "Custom
" + + var/uses_display = selected_uses < 0 ? "Unlimited" : "[selected_uses]" + dat += "  Uses: [uses_display] " + dat += "+ " + dat += "- " + dat += "Custom " + dat += "Unlimited
" + + // Expiry — only shown for timed + if(selected_boost_type == "timed") + var/expiry_display = selected_expiry_hours > 0 ? "[selected_expiry_hours]h" : "Not set" + dat += "  Expiry: [expiry_display] " + dat += "+1h " + dat += "-1h " + dat += "Custom
" + + dat += "  Applicable (empty = all):
" + for(var/entry in selected_applicable) + dat += "    - [entry] Remove
" + dat += "    + Add Job " + dat += "+ Add Vessel " + if(length(selected_applicable)) + dat += "Clear All" + dat += "
" + + dat += "
" + if(target_client) + dat += "\[ APPLY BOOST \] " + dat += "Reset Builder
" + + var/datum/browser/popup = new(user, "job_boost_panel", "Job Boost Panel", 520, 520) + popup.set_content(dat.Join()) + popup.open() + +/datum/job_boost_panel/Topic(href, list/href_list) + . = ..() + if(!holder) + return + var/mob/user = usr + if(holder.owner != user.client) + return + if(!check_rights_for(user.client, R_ADMIN)) + to_chat(user, span_boldwarning("No admin permission.")) + return + + switch(href_list["task"]) + if("ckey") + var/chosen_ckey = input(user, "Enter ckey", "CKEY", selected_ckey) as text|null + if(!chosen_ckey) + return + selected_ckey = ckey(chosen_ckey) + reset_builder() + + if("set_type") + selected_boost_type = href_list["boost_type"] + if(selected_boost_type != "timed") + selected_expiry_hours = 0 + switch(selected_boost_type) + if("general") + selected_boost_amount = 1 + selected_uses = -1 + if("minor") + selected_boost_amount = 2 + selected_uses = -1 + if("major") + selected_boost_amount = 5 + selected_uses = -1 + if("premium") + selected_boost_amount = 10 + selected_uses = -1 + if("timed") + selected_boost_amount = 3 + selected_uses = -1 + selected_expiry_hours = 24 + if("limited") + selected_boost_amount = 5 + selected_uses = 3 + + if("set_amount") + if(href_list["dir"] == "up") + selected_boost_amount++ + else + selected_boost_amount = max(1, selected_boost_amount - 1) + + if("set_amount_custom") + var/val = input(user, "Enter boost amount", "Boost Amount", selected_boost_amount) as num|null + if(isnull(val)) + return + selected_boost_amount = max(1, round(val)) + + if("set_uses") + if(selected_uses < 0) + selected_uses = 1 + else if(href_list["dir"] == "up") + selected_uses++ + else + selected_uses = max(1, selected_uses - 1) + + if("set_uses_custom") + var/val = input(user, "Enter number of uses (-1 for unlimited)", "Uses", selected_uses) as num|null + if(isnull(val)) + return + selected_uses = round(val) + + if("set_uses_unlimited") + selected_uses = -1 + + if("set_expiry") + if(href_list["dir"] == "up") + selected_expiry_hours++ + else + selected_expiry_hours = max(1, selected_expiry_hours - 1) + + if("set_expiry_custom") + var/val = input(user, "Enter expiry in hours", "Expiry Hours", selected_expiry_hours) as num|null + if(isnull(val)) + return + selected_expiry_hours = max(1, round(val)) + + if("add_job") + var/chosen_job = input(user, "Select job", "Add Job", null) as null|anything in SSjob.name_occupations + if(!chosen_job) + return + selected_applicable |= chosen_job + + if("add_vessel") + var/chosen_vessel = input(user, "Select vessel", "Add Vessel", null) as null|anything in GLOB.vessel_ids + if(!chosen_vessel) + return + selected_applicable |= chosen_vessel + + if("remove_applicable") + selected_applicable -= href_list["entry"] + + if("clear_applicable") + selected_applicable.Cut() + + if("reset_builder") + reset_builder() + + if("remove_boost") + var/client/target_client = GLOB.directory[selected_ckey] + if(!target_client) + return + var/boost_index = text2num(href_list["boost_index"]) + if(!boost_index || boost_index > length(target_client.job_priority_boosts)) + return + var/datum/job_priority_boost/boost = target_client.job_priority_boosts[boost_index] + target_client.job_priority_boosts -= boost + var/msg = "[key_name_admin(user)] removed boost '[boost.name]' from [selected_ckey]" + message_admins(msg) + log_admin(msg) + qdel(boost) + + if("apply_boost") + if(!selected_ckey) + to_chat(user, span_boldwarning("No ckey selected.")) + return + var/client/target_client = GLOB.directory[selected_ckey] + if(!target_client) + to_chat(user, span_boldwarning("[selected_ckey] must be online.")) + return + + var/datum/job_priority_boost/boost + switch(selected_boost_type) + if("general") + boost = new /datum/job_priority_boost() + if("minor") + boost = new /datum/job_priority_boost/minor() + if("major") + boost = new /datum/job_priority_boost/major() + if("premium") + boost = new /datum/job_priority_boost/premium() + if("timed") + boost = new /datum/job_priority_boost/timed(selected_expiry_hours) + if("limited") + boost = new /datum/job_priority_boost/limited_use() + + if(!boost) + return + + boost.boost_amount = selected_boost_amount + boost.uses_remaining = selected_uses + if(selected_expiry_hours > 0 && selected_boost_type == "timed") + boost.expiry_time = world.time + (selected_expiry_hours * 36000) + if(length(selected_applicable)) + boost.applicable_jobs = selected_applicable.Copy() + + var/auto_name = "[capitalize(selected_boost_type)] Boost" + if(length(selected_applicable)) + auto_name = "[selected_applicable.Join("/") ] [capitalize(selected_boost_type)] Boost" + boost.name = auto_name + + if(SSjob.give_job_boost(target_client, boost)) + var/msg = "[key_name_admin(user)] gave boost '[boost.name]' (x[boost.boost_amount], uses: [selected_uses < 0 ? "unlimited" : "[selected_uses]"][length(boost.applicable_jobs) ? ", restricted to: [boost.applicable_jobs.Join(", ")]" : ""]) to [selected_ckey]" + message_admins(msg) + log_admin(msg) + reset_builder() + else + qdel(boost) + to_chat(user, span_boldwarning("Failed to apply boost.")) + + show_ui(user) + +/datum/job_boost_panel/proc/reset_builder() + selected_boost_type = "general" + selected_boost_amount = 1 + selected_uses = -1 + selected_expiry_hours = 0 + selected_applicable.Cut() From 32934e0593e219981499f1e27b114cb31cbca17d Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:50:04 -0800 Subject: [PATCH 16/73] the fuck you mean em dashes don't work we literally have them as a say mod --- code/modules/client/save_system/job_boosts.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/client/save_system/job_boosts.dm b/code/modules/client/save_system/job_boosts.dm index 1dd2ecc5491..eec90ad464c 100644 --- a/code/modules/client/save_system/job_boosts.dm +++ b/code/modules/client/save_system/job_boosts.dm @@ -228,7 +228,7 @@ dat += "Active Boosts for [selected_ckey]:" if(!target_client) - dat += " (offline — boosts cannot be managed)" + dat += " (offline -- boosts cannot be managed)" dat += "
" if(target_client && islist(target_client.job_priority_boosts) && length(target_client.job_priority_boosts)) @@ -269,7 +269,7 @@ dat += "Custom " dat += "Unlimited
" - // Expiry — only shown for timed + // Expiry -- only shown for timed if(selected_boost_type == "timed") var/expiry_display = selected_expiry_hours > 0 ? "[selected_expiry_hours]h" : "Not set" dat += "  Expiry: [expiry_display] " From 17635d35f3afc6c71e636697ae4903318b843adf Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:46:40 -0800 Subject: [PATCH 17/73] bracketed --- code/modules/admin/admin.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index aaca5bb20db..c73f7a482ea 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -114,8 +114,8 @@ body += "\[Check Triumphs\] " body += "
" body += "\[Role Ban Panel\] " - body += "Whitelists - " - body += "JOB BOOST - " + body += "\Whitelists\] " + body += "\JOB BOOST\] " var/patron = "" if(isliving(M)) From 58337ad37fa26e534a6964590e7f41e6ba6e4c75 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:55:02 -0800 Subject: [PATCH 18/73] Update admin.dm --- code/modules/admin/admin.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index c73f7a482ea..6e714ba8b19 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -114,8 +114,8 @@ body += "\[Check Triumphs\] " body += "
" body += "\[Role Ban Panel\] " - body += "\Whitelists\] " - body += "\JOB BOOST\] " + body += "\[Whitelists\] " + body += "\[JOB BOOST\] " var/patron = "" if(isliving(M)) From 97c71e863b4ab4a4fff9c22794d16a1f4bec03c6 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:34:28 -0800 Subject: [PATCH 19/73] 2 new components forced shutdown and repairable robotics, makes their limbs actually robotic with a bullshit fix to get them to render --- code/datums/components/bodypart_repair.dm | 57 +++++++++++++++++++ code/datums/components/damage_shutdown.dm | 49 ++++++++++++++++ code/game/objects/items/tools/hammer.dm | 1 + .../species_types/automatons/_automaton.dm | 5 ++ code/modules/mob/living/living_defines.dm | 2 +- code/modules/surgery/bodyparts/_bodyparts.dm | 2 +- vanderlin.dme | 2 + 7 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 code/datums/components/bodypart_repair.dm create mode 100644 code/datums/components/damage_shutdown.dm diff --git a/code/datums/components/bodypart_repair.dm b/code/datums/components/bodypart_repair.dm new file mode 100644 index 00000000000..841ccc9e606 --- /dev/null +++ b/code/datums/components/bodypart_repair.dm @@ -0,0 +1,57 @@ +/datum/component/easy_repair + dupe_mode = COMPONENT_DUPE_UNIQUE + +/datum/component/easy_repair/Initialize(mapload) + . = ..() + if(!iscarbon(parent)) + return COMPONENT_INCOMPATIBLE + +/datum/component/easy_repair/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attacked)) + +/datum/component/easy_repair/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_ATOM_ATTACKBY) + +/datum/component/easy_repair/proc/on_attacked(datum/source, obj/item/held, mob/living/carbon/attacker, modifiers) + SIGNAL_HANDLER + + var/mob/living/carbon/carbon_parent = parent + if(attacker == carbon_parent) + return + + if(!held || !(istype(held, /obj/item/weapon/hammer))) + return + + if(attacker.cmode) + return + + var/zone = attacker.zone_selected + var/obj/item/bodypart/targeted_part = carbon_parent.get_bodypart(zone) + + if(!targeted_part) + return + + if(targeted_part.status != BODYPART_ROBOTIC) + return NONE // not robotic, do nothing + + INVOKE_ASYNC(src, TYPE_PROC_REF(/datum/component/easy_repair, try_heal), source, held, attacker, modifiers) + + return COMPONENT_CANCEL_ATTACK_CHAIN + +/datum/component/easy_repair/proc/try_heal(datum/source, obj/item/held, mob/living/carbon/attacker, modifiers) + var/mob/living/carbon/carbon_parent = parent + var/zone = attacker.zone_selected + var/obj/item/bodypart/targeted_part = carbon_parent.get_bodypart(zone) + var/damaged = TRUE + while(damaged) + if(!do_after(attacker, 3 SECONDS, parent)) + return + held.play_tool_sound(carbon_parent) + var/heal_value = held.force * max(1, (0.5 * attacker.get_skill_level(/datum/skill/craft/engineering))) + targeted_part.heal_damage(heal_value,heal_value) // repairs brute and burn equal to tool force + attacker.visible_message( + span_notice("[attacker] taps [carbon_parent]'s [targeted_part.name] with [held], straightening out the damage."), + span_notice("You tap [carbon_parent]'s [targeted_part.name] with [held], repairing some damage.") + ) + damaged = carbon_parent.getBruteLoss() + carbon_parent.getFireLoss() + attacker.adjust_experience(/datum/skill/craft/engineering, 10) diff --git a/code/datums/components/damage_shutdown.dm b/code/datums/components/damage_shutdown.dm new file mode 100644 index 00000000000..bdeb569330b --- /dev/null +++ b/code/datums/components/damage_shutdown.dm @@ -0,0 +1,49 @@ +/datum/component/damage_shutdown + dupe_mode = COMPONENT_DUPE_UNIQUE + + /// Total combined damage (brute + burn) that triggers shutdown. Default: 60. + var/shutdown_threshold = 200 + /// Total combined damage at or below which the mob wakes back up. Default: 20. + var/recovery_threshold = 40 + /// Whether the mob is currently shut down by this component. + var/is_shut_down = FALSE + +/datum/component/damage_shutdown/Initialize(mapload, shutdown_threshold = 200, recovery_threshold = 20) + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.shutdown_threshold = shutdown_threshold + src.recovery_threshold = recovery_threshold + +/datum/component/damage_shutdown/RegisterWithParent() + RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_update)) + +/datum/component/damage_shutdown/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE) + +/datum/component/damage_shutdown/proc/on_health_update(datum/source) + SIGNAL_HANDLER + + var/mob/living/living_parent = parent + var/total_damage = living_parent.getBruteLoss() + living_parent.getFireLoss() + + if(!is_shut_down && total_damage >= shutdown_threshold) + failure() + else if(is_shut_down && total_damage <= recovery_threshold) + revive() + +/datum/component/damage_shutdown/proc/failure() + var/mob/living/living_parent = parent + is_shut_down = TRUE + living_parent.SetUnconscious(INFINITY) // holds them unconscious until we clear it through health updates + living_parent.visible_message( + span_warning("[living_parent] suddenly goes limp, systems shutting down!") + ) + +/datum/component/damage_shutdown/proc/revive() + var/mob/living/living_parent = parent + is_shut_down = FALSE + living_parent.SetUnconscious(0) + living_parent.visible_message( + span_notice("[living_parent] stirs, systems coming back online.") + ) diff --git a/code/game/objects/items/tools/hammer.dm b/code/game/objects/items/tools/hammer.dm index a93fb8882e7..32b4e1da9e0 100644 --- a/code/game/objects/items/tools/hammer.dm +++ b/code/game/objects/items/tools/hammer.dm @@ -5,6 +5,7 @@ icon = 'icons/roguetown/weapons/tools.dmi' mob_overlay_icon = 'icons/roguetown/onmob/onmob.dmi' force = DAMAGE_HAMMER + usesound = list('sound/items/bsmith1.ogg','sound/items/bsmith2.ogg','sound/items/bsmith3.ogg','sound/items/bsmith4.ogg') possible_item_intents = list(MACE_STRIKE, MACE_SMASH) max_integrity = INTEGRITY_STRONG sharpness = IS_BLUNT 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 index d32a1dec844..8c8f1a281bb 100644 --- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm +++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm @@ -121,6 +121,8 @@ C.AddComponent(/datum/component/steam_life) C.AddComponent(/datum/component/command_follower) C.AddComponent(/datum/component/augmentable) + C.AddComponent(/datum/component/easy_repair) + C.AddComponent(/datum/component/damage_shutdown) RegisterSignal(C, COMSIG_MOB_SAY, PROC_REF(handle_speech)) C.grant_language(/datum/language/common) @@ -130,6 +132,9 @@ C.add_movespeed_modifier("automaton", multiplicative_slowdown = 0.9) + for(var/obj/item/bodypart/part as anything in C.bodyparts) + part.status = BODYPART_ROBOTIC + /datum/species/automaton/on_species_loss(mob/living/carbon/C) . = ..() UnregisterSignal(C, list(COMSIG_MOB_SAY)) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 1e7e240b5c6..47a784c7918 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -79,7 +79,7 @@ var/has_limbs = 0 //does the mob have distinct limbs?(arms,legs, chest,head) ///How many legs does this mob have by default. This shouldn't change at runtime. - var/default_num_legs = 2 + var/default_num_legs = 4 ///How many legs does this mob currently have. Should only be changed through set_num_legs() var/num_legs = 2 ///How many usable legs this mob currently has. Should only be changed through set_usable_legs() diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 77b1920a2c5..789abce053c 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -666,7 +666,7 @@ var/skel = skeletonized ? "_s" : "" - if(is_organic_limb()) + if(is_organic_limb() || (is_species(owner, /datum/species/automaton) && species_icon))//fuck this stupid rendering system if(should_draw_greyscale) limb.icon = species_icon if(should_draw_gender) diff --git a/vanderlin.dme b/vanderlin.dme index 60bb29940be..ba359759e07 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -902,6 +902,7 @@ #include "code\datums\components\beetlejuice.dm" #include "code\datums\components\blacksmith.dm" #include "code\datums\components\blood_stability.dm" +#include "code\datums\components\bodypart_repair.dm" #include "code\datums\components\bounded.dm" #include "code\datums\components\breed.dm" #include "code\datums\components\butchering.dm" @@ -913,6 +914,7 @@ #include "code\datums\components\conjured_item.dm" #include "code\datums\components\connect_mob_behalf.dm" #include "code\datums\components\construction.dm" +#include "code\datums\components\damage_shutdown.dm" #include "code\datums\components\deadchat_control.dm" #include "code\datums\components\decal.dm" #include "code\datums\components\dejavu.dm" From 8e5eeebffc8a803c2c137f726581c077ca41e80c Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:40:13 -0800 Subject: [PATCH 20/73] Update _component.dm --- code/datums/components/command_listener/_component.dm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/datums/components/command_listener/_component.dm b/code/datums/components/command_listener/_component.dm index 813211cced9..daf61bfe08c 100644 --- a/code/datums/components/command_listener/_component.dm +++ b/code/datums/components/command_listener/_component.dm @@ -56,6 +56,7 @@ 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)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) /datum/component/command_follower/Destroy() clear_command() @@ -163,5 +164,8 @@ SEND_SIGNAL(owner, COMSIG_PARENT_COMMAND_RECEIVED, new_cmd, clicker) +/datum/component/command_follower/proc/on_examine(datum/source, mob/user, list/examine_list) + examine_list += span_blue("Alt-Click on this mob to give it a direct command.") + /atom/movable/screen/command_display name = "Command Display" From e4e4522ab67eb382ddb44f4aaa5562f0c2cc06d2 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:51:34 -0800 Subject: [PATCH 21/73] Update _component.dm --- code/datums/components/command_listener/_component.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/datums/components/command_listener/_component.dm b/code/datums/components/command_listener/_component.dm index daf61bfe08c..25261ac6b99 100644 --- a/code/datums/components/command_listener/_component.dm +++ b/code/datums/components/command_listener/_component.dm @@ -165,7 +165,7 @@ SEND_SIGNAL(owner, COMSIG_PARENT_COMMAND_RECEIVED, new_cmd, clicker) /datum/component/command_follower/proc/on_examine(datum/source, mob/user, list/examine_list) - examine_list += span_blue("Alt-Click on this mob to give it a direct command.") + examine_list += span_blue("Ctrl-Click on this mob to give it a direct command.") /atom/movable/screen/command_display name = "Command Display" From 86d1bb35bcadddd020af1e30528ee9b5476be70c Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:56:03 -0800 Subject: [PATCH 22/73] whoops --- .../rotational_objects/fluid_objects/steam_recharger.dm | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/code/game/rotational_objects/fluid_objects/steam_recharger.dm b/code/game/rotational_objects/fluid_objects/steam_recharger.dm index 455fadf8292..85beec3af64 100644 --- a/code/game/rotational_objects/fluid_objects/steam_recharger.dm +++ b/code/game/rotational_objects/fluid_objects/steam_recharger.dm @@ -138,14 +138,10 @@ to_chat(user, span_warning("[src] is already occupied!")) return FALSE - if(!ishuman(automaton)) + if(!automaton.GetComponent(/datum/component/steam_life)) 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 From 1219f63888a9aae96cf2397b633f4d21c70007d0 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:08:24 -0800 Subject: [PATCH 23/73] Update living_defines.dm --- code/modules/mob/living/living_defines.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 47a784c7918..1e7e240b5c6 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -79,7 +79,7 @@ var/has_limbs = 0 //does the mob have distinct limbs?(arms,legs, chest,head) ///How many legs does this mob have by default. This shouldn't change at runtime. - var/default_num_legs = 4 + var/default_num_legs = 2 ///How many legs does this mob currently have. Should only be changed through set_num_legs() var/num_legs = 2 ///How many usable legs this mob currently has. Should only be changed through set_usable_legs() From df5b92da38764d2de2f37368d739503da1882185 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:46:08 -0800 Subject: [PATCH 24/73] Update ghost_vessel.dm --- code/datums/components/ghost_vessel.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/datums/components/ghost_vessel.dm b/code/datums/components/ghost_vessel.dm index 3562f9b6505..7513486e007 100644 --- a/code/datums/components/ghost_vessel.dm +++ b/code/datums/components/ghost_vessel.dm @@ -83,6 +83,7 @@ GLOBAL_LIST_EMPTY(active_ghost_vessels) REMOVE_TRAIT(owner, TRAIT_STASIS, REF(src)) REMOVE_TRAIT(owner, TRAIT_IMMOBILIZED, SOULSTONE_TRAIT) REMOVE_TRAIT(owner, TRAIT_HANDS_BLOCKED, SOULSTONE_TRAIT) + owner.after_creation() owner.key = ghost.client.key var/new_name = browser_input_text(owner, "Choose a new Name", "New Name", owner.name) if(new_name) From 7542a66832c9a31ca3d6b257518783258744e640 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:46:54 -0800 Subject: [PATCH 25/73] radiant quests and reduxed regional threats, improved human npc code --- _maps/map_files/debug/roguetest.dmm | 20 +- code/__DEFINES/ai/_ai.dm | 21 + code/__DEFINES/ai/hostile.dm | 3 +- code/__DEFINES/components.dm | 3 + code/__DEFINES/danger.dm | 11 + code/__DEFINES/quests.dm | 59 +++ code/__DEFINES/traits/definitions.dm | 2 + code/__HELPERS/_logging.dm | 4 + code/__HELPERS/directions.dm | 59 +++ code/_globalvars/lists/mapping.dm | 1 + code/controllers/subsystem/regional_threat.dm | 60 +++ .../behaviours/hostile/find_highest_aggro.dm | 2 + code/datums/ai/controllers/human_npc.dm | 34 +- code/datums/ai/controllers/mirespider.dm | 287 +++++++++++++ code/datums/ai/subtrees/__bow_base.dm | 49 +++ code/datums/ai/subtrees/be_a_minion.dm | 41 ++ code/datums/ai/subtrees/bow_usage.dm | 143 +++++++ code/datums/ai/subtrees/climb_tree.dm | 52 +++ code/datums/ai/subtrees/flank.dm | 129 ++++++ code/datums/ai/subtrees/generic_wield.dm | 28 ++ code/datums/ai/subtrees/harass.dm | 161 +++++++ code/datums/ai/subtrees/human_basic_attack.dm | 315 ++++++++++++++ code/datums/ai/subtrees/retrieve_arrow.dm | 83 ++++ code/datums/threat_regions/_region.dm | 57 +++ code/datums/threat_regions/basin.dm | 8 + code/datums/threat_regions/coast.dm | 8 + code/datums/threat_regions/mount_decap.dm | 8 + code/datums/threat_regions/north_grove.dm | 8 + code/datums/threat_regions/outer_grove.dm | 8 + code/datums/threat_regions/terror_bog.dm | 8 + .../traders/trade_data_types/zhongese.dm | 2 +- code/game/area/areas.dm | 3 + code/game/area/roguetownareas.dm | 47 ++- code/game/area/wilderness.dm | 20 +- code/game/objects/effects/glowshroom.dm | 4 +- code/game/objects/items.dm | 2 + code/game/objects/items/signal_horn.dm | 55 ++- .../objects/items/weapons/melee/polearms.dm | 4 +- .../game/objects/items/weapons/ranged/bows.dm | 8 +- code/modules/ambush/_ambush_config.dm | 2 + code/modules/ambush/bog_ambush.dm | 34 ++ code/modules/ambush/coast.dm | 21 + code/modules/ambush/mount_decap.dm | 30 ++ code/modules/ambush/treasure_hunters.dm | 14 + code/modules/ambush/wilderness.dm | 56 +++ code/modules/clothing/belt/misc.dm | 12 + code/modules/food_and_drinks/rations.dm | 54 +++ code/modules/jobs/job_types/_job.dm | 4 + .../jobs/job_types/apprentices/shophand.dm | 1 + .../jobs/job_types/nobility/steward.dm | 1 + code/modules/mob/inventory.dm | 2 + code/modules/mob/living/ambush.dm | 215 +++++++--- .../human/npc/human_soldiers/bog_deserters.dm | 263 ++++++++++++ .../human/npc/human_soldiers/deranged.dm | 257 ++++++++++++ .../human/npc/human_soldiers/drow_raiders.dm | 117 ++++++ .../human/npc/human_soldiers/highwaymen.dm | 95 +++++ .../npc/human_soldiers/northern_milita.dm | 127 ++++++ .../human/npc/human_soldiers/searaider.dm | 127 ++++++ .../human/npc/human_soldiers/thieves.dm | 108 +++++ .../npc/human_soldiers/treasure_hunters.dm | 113 +++++ .../carbon/human/npc/{orc.dm => orc/_orc.dm} | 8 +- .../living/carbon/human/npc/orc/soldiers.dm | 179 ++++++++ .../{skeleton.dm => skeleton/_skeleton.dm} | 0 .../carbon/human/npc/skeleton/difficulties.dm | 233 +++++++++++ .../hostile/retaliate/mirespider.dm | 393 ++++++++++++++++++ code/modules/projectiles/projectile.dm | 2 +- code/modules/questing/contract_machine.dm | 376 +++++++++++++++++ code/modules/questing/items/parcel.dm | 120 ++++++ code/modules/questing/items/quest_scroll.dm | 336 +++++++++++++++ code/modules/questing/items/spawn_effect.dm | 55 +++ code/modules/questing/landmarks.dm | 64 +++ code/modules/questing/quest_object.dm | 215 ++++++++++ code/modules/questing/quests/__quest_list.dm | 5 + code/modules/questing/quests/_quest.dm | 182 ++++++++ code/modules/questing/quests/courier.dm | 123 ++++++ .../questing/quests/kill/_kill_base.dm | 34 ++ code/modules/questing/quests/kill/clearout.dm | 24 ++ .../modules/questing/quests/kill/easy_kill.dm | 22 + code/modules/questing/quests/kill/outlaw.dm | 37 ++ code/modules/questing/quests/kill/raid.dm | 24 ++ code/modules/questing/quests/retrieval.dm | 46 ++ .../chemistry/reagents/toxin_reagents.dm | 54 +++ .../bodyparts/bodypart_dismemberment.dm | 2 + icons/mob/mirespider_big.dmi | Bin 0 -> 9057 bytes icons/mob/mirespider_shroom.dmi | Bin 0 -> 2320 bytes icons/mob/mirespider_small.dmi | Bin 0 -> 5396 bytes icons/obj/questing.dmi | Bin 0 -> 7817 bytes icons/obj/ration.dmi | Bin 0 -> 1976 bytes icons/obj/webbing.dmi | Bin 0 -> 18718 bytes vanderlin.dme | 58 ++- 90 files changed, 5953 insertions(+), 109 deletions(-) create mode 100644 code/__DEFINES/danger.dm create mode 100644 code/__DEFINES/quests.dm create mode 100644 code/__HELPERS/directions.dm create mode 100644 code/controllers/subsystem/regional_threat.dm create mode 100644 code/datums/ai/controllers/mirespider.dm create mode 100644 code/datums/ai/subtrees/__bow_base.dm create mode 100644 code/datums/ai/subtrees/be_a_minion.dm create mode 100644 code/datums/ai/subtrees/bow_usage.dm create mode 100644 code/datums/ai/subtrees/climb_tree.dm create mode 100644 code/datums/ai/subtrees/flank.dm create mode 100644 code/datums/ai/subtrees/generic_wield.dm create mode 100644 code/datums/ai/subtrees/harass.dm create mode 100644 code/datums/ai/subtrees/human_basic_attack.dm create mode 100644 code/datums/ai/subtrees/retrieve_arrow.dm create mode 100644 code/datums/threat_regions/_region.dm create mode 100644 code/datums/threat_regions/basin.dm create mode 100644 code/datums/threat_regions/coast.dm create mode 100644 code/datums/threat_regions/mount_decap.dm create mode 100644 code/datums/threat_regions/north_grove.dm create mode 100644 code/datums/threat_regions/outer_grove.dm create mode 100644 code/datums/threat_regions/terror_bog.dm create mode 100644 code/modules/ambush/_ambush_config.dm create mode 100644 code/modules/ambush/bog_ambush.dm create mode 100644 code/modules/ambush/coast.dm create mode 100644 code/modules/ambush/mount_decap.dm create mode 100644 code/modules/ambush/treasure_hunters.dm create mode 100644 code/modules/ambush/wilderness.dm create mode 100644 code/modules/food_and_drinks/rations.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm create mode 100644 code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm rename code/modules/mob/living/carbon/human/npc/{orc.dm => orc/_orc.dm} (98%) create mode 100644 code/modules/mob/living/carbon/human/npc/orc/soldiers.dm rename code/modules/mob/living/carbon/human/npc/{skeleton.dm => skeleton/_skeleton.dm} (100%) create mode 100644 code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm create mode 100644 code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm create mode 100644 code/modules/questing/contract_machine.dm create mode 100644 code/modules/questing/items/parcel.dm create mode 100644 code/modules/questing/items/quest_scroll.dm create mode 100644 code/modules/questing/items/spawn_effect.dm create mode 100644 code/modules/questing/landmarks.dm create mode 100644 code/modules/questing/quest_object.dm create mode 100644 code/modules/questing/quests/__quest_list.dm create mode 100644 code/modules/questing/quests/_quest.dm create mode 100644 code/modules/questing/quests/courier.dm create mode 100644 code/modules/questing/quests/kill/_kill_base.dm create mode 100644 code/modules/questing/quests/kill/clearout.dm create mode 100644 code/modules/questing/quests/kill/easy_kill.dm create mode 100644 code/modules/questing/quests/kill/outlaw.dm create mode 100644 code/modules/questing/quests/kill/raid.dm create mode 100644 code/modules/questing/quests/retrieval.dm create mode 100644 icons/mob/mirespider_big.dmi create mode 100644 icons/mob/mirespider_shroom.dmi create mode 100644 icons/mob/mirespider_small.dmi create mode 100644 icons/obj/questing.dmi create mode 100644 icons/obj/ration.dmi create mode 100644 icons/obj/webbing.dmi diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm index 814d0b18a0c..809a499a949 100644 --- a/_maps/map_files/debug/roguetest.dmm +++ b/_maps/map_files/debug/roguetest.dmm @@ -1116,6 +1116,10 @@ }, /turf/open/floor/cobblerock, /area/outdoors/mountains/decap) +"eg" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/grass/yel, +/area/outdoors) "ek" = ( /obj/machinery/light/fueled/torchholder/hotspring/standing{ pixel_x = 15 @@ -2405,6 +2409,7 @@ dir = 1; pixel_y = -5 }, +/obj/structure/fake_machine/contractledger, /turf/open/floor/wood, /area/indoors/town/keep) "Be" = ( @@ -3042,6 +3047,11 @@ /obj/effect/landmark/start/consort, /turf/open/floor/grass/yel, /area/outdoors) +"Pn" = ( +/obj/structure/flora/grass, +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/grass/yel, +/area/outdoors) "Pq" = ( /obj/machinery/light/fueled/smelter/great, /turf/open/floor/cobble, @@ -3527,6 +3537,10 @@ /obj/structure/flora/grass, /turf/open/floor/grass/yel, /area/outdoors) +"YO" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/grass/yel, +/area/outdoors) "YR" = ( /obj/structure/hotspring/border/twelve, /turf/open/floor/cobblerock, @@ -16142,7 +16156,7 @@ QN Wr QN aq -QN +eg QN ky ew @@ -22302,7 +22316,7 @@ tA tA tA ai -QN +YO ai tA tA @@ -76017,7 +76031,7 @@ QN qc QN ky -qc +Pn QN ky jC diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 6df8f23a3ed..efe8b01cba8 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -223,7 +223,28 @@ #define BB_CAT_HOME "cat_home" /// key that holds the human we will beg #define BB_HUMAN_BEG_TARGET "human_beg_target" +#define BB_HUMAN_NPC_ATTACK_ZONE_COUNTER "human_npc_attack_zone_counter" +#define BB_HUMAN_NPC_LAST_ATTACK_ZONE "human_npc_last_attack_zone" +#define BB_HUMAN_NPC_WEAKPOINT "human_npc_weakpoint" +#define BB_HUMAN_NPC_JUMP_COOLDOWN "human_npc_jump_cooldown" +#define BB_HUMAN_NPC_FLANK_ANGLE "human_npc_flank_angle" +#define BB_HUMAN_NPC_FLANK_TARGET "human_npc_flank_target" +#define BB_HUMAN_NPC_HARASS_MODE "human_npc_harass_mode" +#define BB_HUMAN_NPC_HARASS_RETREATING "human_npc_harass_retreating" +#define BB_HUMAN_NPC_HARASS_COOLDOWN "human_npc_harass_cooldown" #define BB_BEGGING_FOOD_ITEM "item_beg_target" +#define BB_ARCHER_NPC_TARGET_ARROW "archer_target_arrow" +#define BB_ARCHER_NPC_STASHED_WEAPON "archer_stashed_weapon" +#define BB_ARCHER_NPC_CHARGE_TIMER "archer_charge_timer" +#define BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY "archer_npc_equipment_cache_expiry" +#define BB_ARCHER_NPC_BOW "archer_npc_bow" +#define BB_ARCHER_NPC_QUIVER "archer_npc_quiver" + +#define ARCHER_NPC_EQUIPMENT_CACHE_TIME (40 SECONDS) +#define ARCHER_NPC_MIN_RANGE 3 // tiles - closer than this, prefer melee +#define ARCHER_NPC_ARROW_SEARCH_RANGE 9 +#define ARCHER_NPC_SIMULATED_CHARGETIME 15 // fallback charge wait in deciseconds + #define BB_CAT_KITTEN_TARGET "BB_cat_kitten_target" #define BB_CAT_HOLDING_FOOD "BB_cat_holding_food" diff --git a/code/__DEFINES/ai/hostile.dm b/code/__DEFINES/ai/hostile.dm index a8b4a4df270..d867e9bff62 100644 --- a/code/__DEFINES/ai/hostile.dm +++ b/code/__DEFINES/ai/hostile.dm @@ -60,7 +60,8 @@ ///list of foods this mob likes #define BB_BASIC_FOODS "BB_basic_foods" - +///What creature we want to cocoon +#define BB_BASIC_MOB_COCOON_TARGET "BB_basic_mob_cocoon_target" /// Flag to set on or off if you want your mob to prioritise running away #define BB_BASIC_MOB_FLEEING "BB_basic_fleeing" diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 5bf3d604d6a..fbe584351c8 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -125,6 +125,9 @@ #define SPEECH_IGNORE_SPAM 6 #define SPEECH_FORCED 7 */ +/// Called from the base of '/obj/item/bodypart/proc/drop_limb(special)' () +#define COMSIG_MOB_DISMEMBER "mob_drop_limb" + #define COMPONENT_CANCEL_DISMEMBER (1<<0) //cancel the drop limb #define COMSIG_MOB_DEADSAY "mob_deadsay" // from /mob/say_dead(): (mob/speaker, message) #define MOB_DEADSAY_SIGNAL_INTERCEPT 1 ///from base of /mob/verb/pointed: (atom/A) diff --git a/code/__DEFINES/danger.dm b/code/__DEFINES/danger.dm new file mode 100644 index 00000000000..8791a1db713 --- /dev/null +++ b/code/__DEFINES/danger.dm @@ -0,0 +1,11 @@ + +#define DANGER_SAFE_FLOOR 0 +#define DANGER_SAFE_LIMIT 10 +#define DANGER_LOW_FLOOR 11 +#define DANGER_LOW_LIMIT 20 +#define DANGER_MODERATE_FLOOR 21 +#define DANGER_MODERATE_LIMIT 30 +#define DANGER_DANGEROUS_FLOOR 31 +#define DANGER_DANGEROUS_LIMIT 40 +#define DANGER_DIRE_FLOOR 41 +#define DANGER_DIRE_LIMIT 60 diff --git a/code/__DEFINES/quests.dm b/code/__DEFINES/quests.dm new file mode 100644 index 00000000000..561ca21d3e1 --- /dev/null +++ b/code/__DEFINES/quests.dm @@ -0,0 +1,59 @@ +#define QUEST_DIFFICULTY_EASY "Easy" +#define QUEST_DIFFICULTY_MEDIUM "Medium" +#define QUEST_DIFFICULTY_HARD "Hard" + +#define QUEST_RETRIEVAL "Retrieval" +#define QUEST_COURIER "Courier" +#define QUEST_KILL_EASY "Kill" +#define QUEST_CLEAR_OUT "Clear Out" +#define QUEST_RAID "Raid" +#define QUEST_OUTLAW "Outlaw" +#define QUEST_BEACON "Beacon" + +#define QUEST_REWARD_EASY_LOW 30 +#define QUEST_REWARD_EASY_HIGH 35 +#define QUEST_REWARD_MEDIUM_LOW 45 +#define QUEST_REWARD_MEDIUM_HIGH 65 +#define QUEST_REWARD_HARD_LOW 120 +#define QUEST_REWARD_HARD_HIGH 180 + +#define QUEST_DEPOSIT_EASY 5 +#define QUEST_DEPOSIT_MEDIUM 10 +#define QUEST_DEPOSIT_HARD 20 + +#define QUEST_HANDLER_REWARD_MULTIPLIER 2 + +// Delivery quest additional reward scaling +#define QUEST_DELIVERY_DISTANCE_DIVISOR 8 // Divides the distance for reward calculation +#define QUEST_DELIVERY_DISTANCE_BONUS 1 // Adds a bonus for longer distances +#define QUEST_COURIER_BONUS_FLAT 10 // Flat bonus for courier quests, since you gotta wait for a person to open a package +#define QUEST_DELIVERY_PER_ITEM_BONUS 2 // Bonus per item delivered + +// All eligible quest kill mobs +// The extra per number reward are based on toughness + whether their head is worth anything +#define QUEST_KILL_MOBS_LIST list(\ + /mob/living/carbon/human/species/goblin/npc/ambush/sea = 3,\ + /mob/living/carbon/human/species/skeleton/npc/supereasy = 4,\ + /mob/living/carbon/human/species/skeleton/npc/easy = 5,\ + /mob/living/carbon/human/species/skeleton/npc/pirate = 5,\ + /mob/living/carbon/human/species/human/northern/militia/deserter = 4,\ + /mob/living/carbon/human/species/orc/npc/footsoldier = 6,\ +) + +// Medium difficulty quest kill mobs, this is where I can put some slightly spicier mobs +#define QUEST_KILL_MEDIUM_LIST list(\ + /mob/living/carbon/human/species/human/northern/searaider/ambush = 6,\ + /mob/living/carbon/human/species/human/northern/highwayman = 6,\ + /mob/living/carbon/human/species/orc/npc/footsoldier = 6,\ + /mob/living/carbon/human/species/orc/npc/marauder = 8,\ + /mob/living/carbon/human/species/skeleton/npc/mediumspread = 6,\ + /mob/living/carbon/human/species/skeleton/npc/mediumspread = 6,\ + /mob/living/carbon/human/species/human/northern/thief = 8,\ + ) + +// Raid difficulty kill mobs - Only three mobs for now. Per person reward is low because base / head reward is high +#define QUEST_RAID_LIST list(\ + /mob/living/carbon/human/species/orc/npc/berserker = 10,\ + /mob/living/carbon/human/species/elf/dark/drowraider = 5, \ + /mob/living/carbon/human/species/human/northern/bog_deserters = 5,\ +) diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index fd25a2dcb03..ebd08c13be4 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -70,6 +70,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_DEATHCOMA "deathcoma" /// ??? should be a signal? #define TRAIT_SANGUINE "sanguine" +#define TRAIT_FRESHSPAWN "freshspawn" /// The mob has the stasis effect. /// Does nothing on its own, applied via status effect. #define TRAIT_STASIS "in_stasis" @@ -430,6 +431,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_SATE "SATE" #define TRAIT_NODE_EXTRACTED "Humors Extracted" #define TRAIT_NO_EXPERIENCE "unlearning" +#define TRAIT_STUCKITEMS "stuck_items" // Prevents removing items except for hand slots /// This mob should never close UI even if it doesn't have a client #define TRAIT_PRESERVE_UI_WITHOUT_CLIENT "preserve_ui_without_client" diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index bfb95e89d9d..192c52af196 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -104,6 +104,10 @@ if(CONFIG_GET(flag/log_manifest)) WRITE_LOG(GLOB.world_manifest_log, "\[[TIMETOTEXT4LOGS]\] [ckey] \\ [body.real_name] \\ [mind.assigned_role.title] \\ [mind.special_role ? mind.special_role : "NONE"] \\ [latejoin ? "LATEJOIN":"ROUNDSTART"]") +/proc/log_quest(ckey, datum/mind/mind, mob/body, text) + if (CONFIG_GET(flag/log_law)) + WRITE_LOG(GLOB.world_game_log, "\[[TIMETOTEXT4LOGS]] QUEST: [ckey] \\ [body.real_name] \\ [text]") + /proc/log_bomber(atom/user, details, atom/bomb, additional_details, message_admins = TRUE) var/bomb_message = "\[[TIMETOTEXT4LOGS]\] [details][bomb ? " [bomb.name] at [AREACOORD(bomb)]": ""][additional_details ? " [additional_details]" : ""]." diff --git a/code/__HELPERS/directions.dm b/code/__HELPERS/directions.dm new file mode 100644 index 00000000000..56a5ed738c0 --- /dev/null +++ b/code/__HELPERS/directions.dm @@ -0,0 +1,59 @@ +/// Get the direction between one atom to another in precise compass terms (North-northwest etc.) +/proc/get_precise_direction_between(atom/from_atom, atom/to_atom) + var/turf/from_turf = get_turf(from_atom) + var/turf/to_turf = get_turf(to_atom) + if(!from_turf || !to_turf) + return null + + var/dx = to_turf.x - from_turf.x + var/dy = to_turf.y - from_turf.y + if(!dx && !dy) + return null + + var/angle = ATAN2(dx, dy) + if(angle < 0) + angle += 360 + return get_precise_direction_from_angle(angle) + +/proc/get_precise_direction_from_angle(angle) + // Normalize the incoming angle first. + angle = (angle + 360) % 360 + + // Convert to compass bearing (0° = north, 90° = east). + var/compass_angle = (450 - angle) % 360 // 450 = 360 + 90 + + switch(compass_angle) + if(348.75 to 360, 0 to 11.25) + return "north" + if(11.25 to 33.75) + return "north-northeast" + if(33.75 to 56.25) + return "northeast" + if(56.25 to 78.75) + return "east-northeast" + if(78.75 to 101.25) + return "east" + if(101.25 to 123.75) + return "east-southeast" + if(123.75 to 146.25) + return "southeast" + if(146.25 to 168.75) + return "south-southeast" + if(168.75 to 191.25) + return "south" + if(191.25 to 213.75) + return "south-southwest" + if(213.75 to 236.25) + return "southwest" + if(236.25 to 258.75) + return "west-southwest" + if(258.75 to 281.25) + return "west" + if(281.25 to 303.75) + return "west-northwest" + if(303.75 to 326.25) + return "northwest" + if(326.25 to 348.75) + return "north-northwest" + + return null diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm index eabcbf3ed5f..09f33c86b30 100644 --- a/code/_globalvars/lists/mapping.dm +++ b/code/_globalvars/lists/mapping.dm @@ -17,6 +17,7 @@ GLOBAL_LIST_EMPTY(start_landmarks_list) //list of all spawn points created GLOBAL_LIST_EMPTY(department_security_spawns) //list of all department security spawns GLOBAL_LIST_EMPTY(generic_event_spawns) //handles clockwork portal+eminence teleport destinations GLOBAL_LIST_EMPTY(jobspawn_overrides) //These will take precedence over normal spawnpoints if created. +GLOBAL_LIST_EMPTY(quest_landmarks_list) GLOBAL_LIST_EMPTY(lich_starts) GLOBAL_LIST_EMPTY(bandit_starts) diff --git a/code/controllers/subsystem/regional_threat.dm b/code/controllers/subsystem/regional_threat.dm new file mode 100644 index 00000000000..6b779d1b2cd --- /dev/null +++ b/code/controllers/subsystem/regional_threat.dm @@ -0,0 +1,60 @@ +// Danger levels. Each danger level is defined as an ambush that can happen. Every time this fire, this number iterates. +#define DANGER_LEVEL_SAFE "Safe" +#define DANGER_LEVEL_LOW "Low" +#define DANGER_LEVEL_MODERATE "Moderate" +#define DANGER_LEVEL_DANGEROUS "Dangerous" +#define DANGER_LEVEL_BLEAK "Bleak" + +#define THREAT_REGION_BASIN "Basin" +#define THREAT_REGION_NORTHERN_GROVE "Northern Grove" +#define THREAT_REGION_OUTER_GROVE "Outer Grove" // Grove west of the road +#define THREAT_REGION_MOUNT_DECAP "Mount Decapitation" +#define THREAT_REGION_TERRORBOG "Terrorbog" +#define THREAT_REGION_COAST "Coast" + +SUBSYSTEM_DEF(regionthreat) + name = "Regional Threat" + wait = 15 MINUTES + flags = SS_KEEP_TIMING | SS_BACKGROUND + runlevels = RUNLEVEL_GAME + var/list/threat_regions = list() + +/datum/controller/subsystem/regionthreat/Initialize(start_timeofday) + for(var/datum/threat_region/region as anything in subtypesof(/datum/threat_region)) + if(IS_ABSTRACT(region)) + continue + threat_regions += new region() + return ..() + +/datum/controller/subsystem/regionthreat/fire(resumed) + var/player_count = GLOB.player_list.len + var/ishighpop = player_count >= LOWPOP_THRESHOLD + for(var/T in threat_regions) + var/datum/threat_region/TR = T + if(ishighpop) + TR.increase_latent_ambush(TR.highpop_tick) + else + TR.increase_latent_ambush(TR.lowpop_tick) + +/datum/controller/subsystem/regionthreat/proc/get_region(region_name) + for(var/T in threat_regions) + var/datum/threat_region/TR = T + if(TR.region_name == region_name) + return TR + return null + +/datum/threat_region_display + var/region_name + var/danger_level + var/danger_color + +/datum/controller/subsystem/regionthreat/proc/get_threat_regions_for_display() + var/list/threat_region_displays = list() + for(var/T in threat_regions) + var/datum/threat_region/TR = T + var/datum/threat_region_display/TRS = new /datum/threat_region_display + TRS.region_name = TR.region_name + TRS.danger_level = TR.get_danger_level() + TRS.danger_color = TR.get_danger_color() + threat_region_displays += TRS + return threat_region_displays diff --git a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm index 1bcc42cee57..2de56e94a2a 100644 --- a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm +++ b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm @@ -124,6 +124,8 @@ finish_action(controller, succeeded = FALSE) /datum/ai_behavior/find_aggro_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + if(HAS_TRAIT(controller.pawn, TRAIT_FRESHSPAWN)) + return var/aggro_range = controller.blackboard[BB_AGGRO_RANGE] || 9 // takes the larger between our range() input and our implicit hearers() input (world.view) aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2)) diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 942db798a49..5eaba11af79 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -1,31 +1,40 @@ + /datum/ai_controller/human_npc movement_delay = 0.5 SECONDS - ai_movement = /datum/ai_movement/hybrid_pathing - blackboard = list( BB_WEAPON_TYPE = /obj/item/weapon, BB_ARMOR_CLASS = 2, - BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), BB_PET_TARGETING_DATUM = new /datum/targetting_datum/basic/not_friends(), + BB_HUMAN_NPC_ATTACK_ZONE_COUNTER = 0, // how many times we've hit the same zone + BB_HUMAN_NPC_LAST_ATTACK_ZONE = null, // last zone we attacked + BB_HUMAN_NPC_WEAKPOINT = null, // cached weakpoint zone if we found one + BB_HUMAN_NPC_JUMP_COOLDOWN = 0, // world.time when we can next jump + BB_HUMAN_NPC_FLANK_ANGLE = null, // our claimed flank direction (degrees, 0-359) + BB_HUMAN_NPC_FLANK_TARGET = null, // the turf we're moving toward for flanking + BB_HUMAN_NPC_HARASS_MODE = FALSE, // TRUE when in hit-and-run mode + BB_HUMAN_NPC_HARASS_RETREATING = FALSE,// TRUE when in the back-off phase of harass + BB_HUMAN_NPC_HARASS_COOLDOWN = 0, // world.time before we can dart in again ) - planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/generic_wield, /datum/ai_planning_subtree/generic_resist, /datum/ai_planning_subtree/generic_stand, /datum/ai_planning_subtree/flee_target, - + /datum/ai_planning_subtree/tree_climb, + /datum/ai_planning_subtree/archer_base, + /datum/ai_planning_subtree/ranged_attack_subtree, /datum/ai_planning_subtree/aggro_find_target, - /datum/ai_planning_subtree/basic_melee_attack_subtree, - + /datum/ai_planning_subtree/wounded_harass, + /datum/ai_planning_subtree/squad_flank, + /datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc, /datum/ai_planning_subtree/find_weapon, /datum/ai_planning_subtree/equip_item, - + /datum/ai_planning_subtree/retrieve_arrows, ) - idle_behavior = /datum/idle_behavior/idle_random_walk /datum/ai_controller/human_npc/TryPossessPawn(atom/new_pawn) @@ -34,14 +43,11 @@ RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed)) movement_delay = living_pawn.cached_multiplicative_slowdown - /datum/ai_controller/human_npc/UnpossessPawn(destroy) - UnregisterSignal(pawn, list( COMSIG_MOB_MOVESPEED_UPDATED, )) - - return ..() //Run parent at end + return ..() /datum/ai_controller/human_npc/proc/update_movespeed(mob/living/pawn) SIGNAL_HANDLER @@ -52,5 +58,5 @@ if(!.) return FALSE var/mob/living/living_pawn = pawn - if(living_pawn.pulledby) // to mimick normal human behavior + if(living_pawn.pulledby) return FALSE diff --git a/code/datums/ai/controllers/mirespider.dm b/code/datums/ai/controllers/mirespider.dm new file mode 100644 index 00000000000..a74593bec28 --- /dev/null +++ b/code/datums/ai/controllers/mirespider.dm @@ -0,0 +1,287 @@ +/datum/ai_controller/mirespider + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + + /datum/ai_planning_subtree/simple_self_recovery, + /datum/ai_planning_subtree/find_food, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/being_a_minion/mirespider + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_controller/mirespider_lurker + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker, + /datum/ai_planning_subtree/find_cocoon_target, + /datum/ai_planning_subtree/cocoon_target + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_controller/mirespider_paralytic + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker, + /datum/ai_planning_subtree/find_cocoon_target, + /datum/ai_planning_subtree/cocoon_target + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_planning_subtree/being_a_minion/mirespider + /// Blackboard key where we travel a place + location_key = BB_TRAVEL_DESTINATION + /// Who we're following + follow_target = BB_FOLLOW_TARGET + /// What do we do in order to travel + travel_behavior = /datum/ai_behavior/travel_towards/stop_on_arrival + +/datum/ai_planning_subtree/being_a_minion/mirespider/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/turf/travel = controller.blackboard[BB_TRAVEL_DESTINATION] + var/mob/living/simple_animal/hostile/mirespider_lurker/following = controller.blackboard[BB_FOLLOW_TARGET] + var/mob/living/pawn = controller.pawn + + if (travel) // Check if travel is defined + controller.queue_behavior(travel_behavior, BB_TRAVEL_DESTINATION) + return SUBTREE_RETURN_FINISH_PLANNING // end here + + else if (following) // If we're following someone + var/mob/target = following.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + following.add_follower(pawn) + + // If the follow target has a target, stop following + if (target) + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + + // If too far from the following target, stop following + else if (get_dist(pawn, following) > 12) + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + + // Otherwise, continue following + else + controller.queue_behavior(/datum/ai_behavior/follow_friend/mirespider, BB_FOLLOW_TARGET) + + return SUBTREE_RETURN_FINISH_PLANNING // end here + return // No travel target and no one to follow, being a minion in other ways + +/datum/ai_behavior/follow_friend/mirespider + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/follow_friend/mirespider/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/simple_animal/hostile/mirespider_lurker/target = controller.blackboard[target_key] + var/mob/living/simple_animal/hostile/mirespider_lurker/target_target = target.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + + if (target_target) + return FALSE + + if (QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/follow_friend/mirespider/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/living/simple_animal/hostile/mirespider_lurker/target = controller.blackboard[target_key] + var/mob/living/simple_animal/hostile/mirespider_lurker/target_target = target.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + + if (target_target) + return // Stop following if the target has a target + + if (QDELETED(target)) + return + + return + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker + ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + + if(isliving(target)) + var/mob/living/carbon/L = target + if(L) + if(L.stat || L.getBruteLoss() > 500) + controller.set_blackboard_key(BB_BASIC_MOB_COCOON_TARGET, L) + controller.queue_behavior(/datum/ai_behavior/cocoon_target, BB_BASIC_MOB_COCOON_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + var/mob/living/simple_animal/hostile/mirespider_lurker/lurker = controller.pawn + if (lurker) + lurker.clear_followers_if_any() + + controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/cocoon_target + behavior_flags = AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 1 SECONDS + +/datum/ai_behavior/cocoon_target/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/target = controller.blackboard[target_key] + if (!target || QDELETED(target) || !isliving(target)) + return FALSE + + set_movement_target(controller, (target)) + +/datum/ai_behavior/cocoon_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/simple_animal/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if (!target || QDELETED(target) || !isliving(target)) + finish_action(controller, FALSE, target_key) + return + + if (istype(target.loc, /obj/structure/spider/cocoon)) + finish_action(controller, TRUE, target_key) + return + + if (target.stat) + if (do_after(pawn, 5 SECONDS, FALSE, target)) + if (istype(target.loc, /obj/structure/spider/cocoon)) + finish_action(controller, TRUE, target_key) + return + var/turf/T = get_turf(target) + var/cocoon = new /obj/structure/spider/cocoon(T) + target.forceMove(cocoon) + // Very gentle healing effect that restores a lot of bloodloss. Allows the target to break out later. + target.apply_status_effect(/datum/status_effect/buff/healing/spider_cocoon, 0.25) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/cocoon_target/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + if(!succeeded) + controller.clear_blackboard_key(target_key) + controller.clear_blackboard_key(target_key) + +/datum/ai_planning_subtree/cocoon_target + var/datum/ai_behavior/cocoon_target/behavior = /datum/ai_behavior/cocoon_target + +/datum/ai_planning_subtree/cocoon_target/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/obj/item/target = controller.blackboard[BB_BASIC_MOB_COCOON_TARGET] + if(QDELETED(target)) + controller.clear_blackboard_key(BB_BASIC_MOB_COCOON_TARGET) + return + var/mob/living/pawn = controller.pawn + if(pawn.doing()) + return + if(!istype(target, /mob/living/carbon)) + behavior = /datum/ai_behavior/cocoon_target + + controller.queue_behavior(behavior, BB_BASIC_MOB_COCOON_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/find_cocoon_target + var/vision_range = 6 + var/datum/ai_behavior/find_and_set/cocoon_target/behavior = /datum/ai_behavior/find_and_set/cocoon_target + var/cocoon_target_key = BB_BASIC_MOB_COCOON_TARGET + +/datum/ai_planning_subtree/find_cocoon_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + + var/atom/target = controller.blackboard[cocoon_target_key] + if(!QDELETED(target)) + // Busy with something + return + + controller.queue_behavior(behavior, cocoon_target_key, controller.blackboard[BB_BASIC_FOODS], vision_range) + +/datum/ai_behavior/find_and_set/cocoon_target + action_cooldown = 20 SECONDS + +/datum/ai_behavior/find_and_set/cocoon_target/search_tactic(datum/ai_controller/controller, locate_paths, search_range) + var/list/found = list() + for(var/mob/living/carbon/mob in oview(search_range, controller.pawn)) + var/obj/structure/spider/cocoon/cocoon = mob.loc + if(istype(cocoon, /obj/structure/spider/cocoon)) + continue + if(mob.stat == DEAD || mob.stat == CONSCIOUS) + continue + found |= mob + if(!length(found)) + return null + return pick(found) + +/atom/movable/screen/alert/status_effect/buff/healing/spider_cocoon + name = "Spider loogies" + desc = "Arachnid weave is stitching some of my wounds up slowly." + icon_state = "buff" + +#define COCOON_FILTER "cocoon_glow" + +/datum/status_effect/buff/healing/spider_cocoon + id = "healing_spider" + alert_type = /atom/movable/screen/alert/status_effect/buff/healing/spider_cocoon + duration = 1800 SECONDS + examine_text = "SUBJECTPRONOUN is covered in spider silk... eww!" + healing_on_tick = 1 + outline_colour = "#4e4c4c00" + var/blood_healing_on_tick = 20 + +/datum/status_effect/buff/healing/spider_cocoon/on_apply() + . = ..() + //The hardier you are, the more likely you are to recover from grievous wounds. + var/stat_bonus = 0 + stat_bonus += ((owner.STACON - 10 ) * 0.05) + stat_bonus += ((owner.STASTR - 10 ) * 0.05) + stat_bonus += ((owner.STAEND - 10 ) * 0.05) + if(stat_bonus > 0) + healing_on_tick += stat_bonus + blood_healing_on_tick += (stat_bonus * 10) + var/filter = owner.get_filter(COCOON_FILTER) + if (!filter) + owner.add_filter(COCOON_FILTER, 2, list("type" = "outline", "color" = outline_colour, "alpha" = 60, "size" = 1)) + return TRUE + +/datum/status_effect/buff/healing/spider_cocoon/tick() + var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal_rogue(get_turf(owner)) + H.color = "#4e4c4c00" + var/list/wCount = owner.get_wounds() + if(owner.blood_volume < BLOOD_VOLUME_NORMAL) + //Keeps the user alive + owner.blood_volume = min(owner.blood_volume+blood_healing_on_tick, BLOOD_VOLUME_NORMAL) + if(wCount.len > 0) + owner.heal_wounds(healing_on_tick) + owner.update_damage_overlays() + owner.adjustBruteLoss(-healing_on_tick, 0) + owner.adjustFireLoss(-healing_on_tick, 0) + owner.adjustOxyLoss(-healing_on_tick * 5, 0) + owner.adjustToxLoss(-healing_on_tick, 0) + owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, -healing_on_tick) + owner.adjustCloneLoss(-healing_on_tick, 0) + +#undef COCOON_FILTER diff --git a/code/datums/ai/subtrees/__bow_base.dm b/code/datums/ai/subtrees/__bow_base.dm new file mode 100644 index 00000000000..a940b1881db --- /dev/null +++ b/code/datums/ai/subtrees/__bow_base.dm @@ -0,0 +1,49 @@ +/datum/ai_planning_subtree/archer_base/proc/validate_archer_equipment(datum/ai_controller/controller) + var/mob/living/carbon/human/pawn = controller.pawn + + + if(world.time < controller.blackboard[BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY]) + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/cached_bow = controller.blackboard[BB_ARCHER_NPC_BOW] + var/obj/item/ammo_holder/quiver/cached_quiver = controller.blackboard[BB_ARCHER_NPC_QUIVER] + if(QDELETED(cached_bow) || QDELETED(cached_quiver)) + _clear_equipment_cache(controller) + return FALSE + return TRUE + + _clear_equipment_cache(controller) + + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/found_bow = null + for(var/obj/item/I in pawn.get_active_held_items()) + if(istype(I, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + found_bow = I + break + if(!found_bow) + for(var/obj/item/I in pawn.get_equipped_items()) + if(istype(I, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + found_bow = I + break + if(!found_bow) + return FALSE + + // Find a quiver compatible with this bow's ammo + var/ammo_check = found_bow?.magazine.ammo_type + var/obj/item/ammo_holder/quiver/found_quiver = null + for(var/obj/item/ammo_holder/quiver/Q in pawn.get_equipped_items()) + for(var/accepted in Q.ammo_type) + if(ispath(ammo_check, accepted)) + found_quiver = Q + break + if(found_quiver) + break + if(!found_quiver) + return FALSE + + controller.set_blackboard_key(BB_ARCHER_NPC_BOW, found_bow) + controller.set_blackboard_key(BB_ARCHER_NPC_QUIVER, found_quiver) + controller.set_blackboard_key(BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY, world.time + ARCHER_NPC_EQUIPMENT_CACHE_TIME) + return TRUE + +/datum/ai_planning_subtree/archer_base/proc/_clear_equipment_cache(datum/ai_controller/controller) + controller.clear_blackboard_key(BB_ARCHER_NPC_BOW) + controller.clear_blackboard_key(BB_ARCHER_NPC_QUIVER) + controller.clear_blackboard_key(BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY) diff --git a/code/datums/ai/subtrees/be_a_minion.dm b/code/datums/ai/subtrees/be_a_minion.dm new file mode 100644 index 00000000000..9596bc4df67 --- /dev/null +++ b/code/datums/ai/subtrees/be_a_minion.dm @@ -0,0 +1,41 @@ +/// Obey your summoner (or equivalent) +/datum/ai_planning_subtree/being_a_minion + /// Blackboard key where we travel a place + var/location_key = BB_TRAVEL_DESTINATION + /// Who we're following + var/follow_target = BB_FOLLOW_TARGET + /// What do we do in order to travel + var/travel_behavior = /datum/ai_behavior/travel_towards/stop_on_arrival +/datum/ai_planning_subtree/being_a_minion/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/turf/travel = controller.blackboard[location_key] + var/mob/following = controller.blackboard[follow_target] + var/mob/living/pawn = controller.pawn + + if(travel) + controller.queue_behavior(travel_behavior, location_key) + return SUBTREE_RETURN_FINISH_PLANNING //end here + else if(following) + if(get_dist(pawn, following) > 12) //If further than 12 then you've lost that friendly target + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + else + controller.queue_behavior(/datum/ai_behavior/follow_friend, follow_target) + return SUBTREE_RETURN_FINISH_PLANNING //end here + return //no travel target and no one to follow. being a minion in other ways +/// Follow the target +/datum/ai_behavior/follow_friend + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM +/datum/ai_behavior/follow_friend/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + + if (QDELETED(target)) + return FALSE + set_movement_target(controller, target) +/datum/ai_behavior/follow_friend/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/target = controller.blackboard[target_key] + + if (QDELETED(target)) + return + + return diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm new file mode 100644 index 00000000000..3447fe9802e --- /dev/null +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -0,0 +1,143 @@ +/datum/ai_planning_subtree/ranged_attack_subtree + parent_type = /datum/ai_planning_subtree/archer_base + +/datum/ai_planning_subtree/ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!validate_archer_equipment(controller)) + return + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || !isliving(target)) + return + if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) + return + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] + if(!Q.ammo_list.len) + return + + controller.queue_behavior(/datum/ai_behavior/ranged_attack_bow, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/ranged_attack_bow + // No REQUIRE_REACH — we want to stay at range + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 0.2 SECONDS + +/datum/ai_behavior/ranged_attack_bow/setup(datum/ai_controller/controller, target_key) + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + if(!target) + return FALSE + + // Stash held melee weapon if needed so both hands are free for the bow + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + continue + if(!held) + continue + var/stashed = FALSE + for(var/slot in list(ITEM_SLOT_BACK, ITEM_SLOT_HIP, ITEM_SLOT_BELT_L, ITEM_SLOT_BACK_L, ITEM_SLOT_BACK_R, ITEM_SLOT_BELT_R)) + if(!pawn.get_item_by_slot(slot)) + if(pawn.equip_to_slot_if_possible(held, slot, disable_warning = TRUE)) + controller.set_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON, held) + stashed = TRUE + break + if(!stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_QUIVER) //this is weird you might say? but it saves a memory slot since it cannot execute a bow shot without a quiver causing it to go on cooldown for 40 seconds. + return FALSE + + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + bow = held + break + if(!bow) + for(var/obj/item/worn in pawn.get_equipped_items()) + if(istype(worn, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + pawn.put_in_active_hand(worn) + bow = worn + break + if(!bow) + return FALSE + + if(!bow.chambered) + var/ammo_check = bow.magazine.ammo_type + for(var/obj/item/ammo_holder/quiver/Q in pawn.get_equipped_items()) + if(!Q.ammo_list.len) + continue + for(var/obj/item/ammo_casing/arrow in Q.ammo_list) + if(ispath(arrow.type, ammo_check)) + Q.ammo_list -= arrow + arrow.forceMove(bow) + // Mirror what attackby does for loading + bow.attackby(arrow, pawn, null) + break + break + + if(!bow.chambered) + return FALSE + + // For crossbows, ensure cocked + if(istype(bow, /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow)) + var/obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/xbow = bow + if(!xbow.cocked) + xbow.cocked = TRUE + xbow.update_appearance(UPDATE_ICON_STATE) + + var/chargetime = ARCHER_NPC_SIMULATED_CHARGETIME + if(pawn.used_intent && pawn.used_intent.chargetime) + chargetime = pawn.used_intent.get_chargetime() + controller.set_blackboard_key(BB_ARCHER_NPC_CHARGE_TIMER, world.time + (chargetime)) + + return TRUE + +/datum/ai_behavior/ranged_attack_bow/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + + if(!target || (ismob(target) && target:stat == DEAD)) + finish_action(controller, FALSE, target_key) + return + + // Break off if target closed to melee range — let melee tree handle it + if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) + finish_action(controller, FALSE, target_key) + return + + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + bow = held + break + if(!bow || !bow.chambered) + finish_action(controller, FALSE, target_key) + return + + // Wait for simulated charge since we have no client + if(world.time < controller.blackboard[BB_ARCHER_NPC_CHARGE_TIMER]) + pawn.face_atom(target) + return + + pawn.face_atom(target) + controller.ai_interact(target, TRUE, TRUE) + + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/ranged_attack_bow/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + + controller.clear_blackboard_key(BB_ARCHER_NPC_CHARGE_TIMER) + + // Re-equip stashed melee weapon + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) diff --git a/code/datums/ai/subtrees/climb_tree.dm b/code/datums/ai/subtrees/climb_tree.dm new file mode 100644 index 00000000000..95370200e12 --- /dev/null +++ b/code/datums/ai/subtrees/climb_tree.dm @@ -0,0 +1,52 @@ +/datum/ai_planning_subtree/tree_climb + +/datum/ai_planning_subtree/tree_climb/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + var/turf/my_turf = get_turf(pawn) + var/turf/their_turf = get_turf(target) + // Target must be directly above us (z+1) on a transparent turf, cardinal distance 1 + if(!their_turf || my_turf.z >= their_turf.z) + return + if(my_turf.Distance3D(their_turf, pawn) != 1) + return + if(!istransparentturf(their_turf)) + return + // There must be a branch on their turf to climb + var/obj/structure/flora/newbranch/the_branch = locate() in their_turf + if(!the_branch) + return + controller.queue_behavior(/datum/ai_behavior/human_npc_climb_tree, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_climb_tree + action_cooldown = 1 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/human_npc_climb_tree/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + if(!target) + finish_action(controller, FALSE, target_key) + return + var/turf/their_turf = get_turf(target) + var/turf/my_turf = get_turf(pawn) + // Revalidate - target may have moved + if(!their_turf || my_turf.z >= their_turf.z || !istransparentturf(their_turf)) + finish_action(controller, FALSE, target_key) + return + var/obj/structure/flora/newbranch/the_branch = locate() in their_turf + if(!the_branch) + finish_action(controller, FALSE, target_key) + return + // Find the tree from the branch (same logic as process_ai) + var/obj/structure/flora/newtree/the_tree = locate() in get_step_multiz(the_branch, REVERSE_DIR(the_branch.dir)|DOWN) + if(!the_tree) + finish_action(controller, FALSE, target_key) + return + the_tree.attack_hand(pawn) + finish_action(controller, TRUE, target_key) + diff --git a/code/datums/ai/subtrees/flank.dm b/code/datums/ai/subtrees/flank.dm new file mode 100644 index 00000000000..75d0b1ef4d2 --- /dev/null +++ b/code/datums/ai/subtrees/flank.dm @@ -0,0 +1,129 @@ +#define FLANK_RADIUS 3 // tiles away from target to orbit +#define FLANK_MIN_SEPARATION 60 // degrees between us and nearest ally +#define FLANK_ENGAGE_DIST 2 // tiles - "close enough" to our flank spot +#define FLANK_ATTACK_CHANCE 25 // % chance to commit a real attack while flanking +#define FLANK_RECHECK_INTERVAL (3 SECONDS) + +/datum/ai_planning_subtree/squad_flank + +/datum/ai_planning_subtree/squad_flank/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || QDELETED(target)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + var/list/ally_angles = list() + var/turf/target_turf = get_turf(target) + for(var/mob/living/carbon/human/ally in view(10, pawn)) + if(ally == pawn) + continue + if(!faction_check(pawn.faction, ally.faction)) + continue + var/datum/ai_controller/human_npc/ally_ctrl = ally.ai_controller + if(!ally_ctrl) + continue + var/mob/living/ally_target = ally_ctrl.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(ally_target != target) + continue + // What angle is this ally at from the target? + var/turf/ally_turf = get_turf(ally) + if(!ally_turf || !target_turf) + continue + var/dx = ally_turf.x - target_turf.x + var/dy = ally_turf.y - target_turf.y + var/angle = round(arctan(dy, dx) + 360) % 360 + ally_angles += angle + + // If no allies are fighting this target, no need to flank + if(!length(ally_angles)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + // Sort angles and find the largest gap + ally_angles = sortList(ally_angles) + var/largest_gap = 0 + var/gap_start_angle = 0 + for(var/i in 1 to length(ally_angles)) + var/next_i = (i % length(ally_angles)) + 1 + var/gap = (ally_angles[next_i] - ally_angles[i] + 360) % 360 + if(gap > largest_gap) + largest_gap = gap + gap_start_angle = ally_angles[i] + + // Not enough angular separation to bother flanking + if(largest_gap < FLANK_MIN_SEPARATION) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + // Our ideal angle is the midpoint of the largest gap + var/my_angle = (gap_start_angle + round(largest_gap / 2)) % 360 + + var/cached_angle = controller.blackboard[BB_HUMAN_NPC_FLANK_ANGLE] + var/turf/flank_turf = controller.blackboard[BB_HUMAN_NPC_FLANK_TARGET] + + // Recalculate if our angle has shifted more than 30 degrees or we have no turf + if(isnull(cached_angle) || abs(cached_angle - my_angle) > 30 || QDELETED(flank_turf)) + var/fx = round(target_turf.x + FLANK_RADIUS * cos(my_angle)) + var/fy = round(target_turf.y + FLANK_RADIUS * sin(my_angle)) + flank_turf = locate(clamp(fx, 1, world.maxx), clamp(fy, 1, world.maxy), target_turf.z) + var/search_attempts = 0 + while(flank_turf && (!flank_turf.can_traverse_safely(pawn) || flank_turf.density) && search_attempts < FLANK_RADIUS) + flank_turf = get_step_towards(flank_turf, target_turf) + search_attempts++ + if(!flank_turf || !flank_turf.can_traverse_safely(pawn)) + return + controller.set_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE, my_angle) + controller.set_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET, flank_turf) + + if(get_dist(pawn, flank_turf) <= FLANK_ENGAGE_DIST) + // We're in position. Occasionally fire an attack, otherwise just hold. + if(prob(FLANK_ATTACK_CHANCE)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + // Hold position, face target, look threatening + pawn.face_atom(target) + return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/human_npc_move_to_flank, BB_HUMAN_NPC_FLANK_TARGET, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_move_to_flank + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_move_to_flank/setup(datum/ai_controller/controller, flank_turf_key, target_key) + . = ..() + var/turf/flank_turf = controller.blackboard[flank_turf_key] + if(!flank_turf || QDELETED(flank_turf)) + return FALSE + set_movement_target(controller, flank_turf) + +/datum/ai_behavior/human_npc_move_to_flank/perform(delta_time, datum/ai_controller/controller, flank_turf_key, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/turf/flank_turf = controller.blackboard[flank_turf_key] + var/mob/living/target = controller.blackboard[target_key] + + if(!flank_turf || QDELETED(flank_turf)) + finish_action(controller, FALSE) + return + + // If the target moved a lot, the flank turf may no longer make sense - let subtree recalculate + if(target && get_dist(get_turf(target), flank_turf) > FLANK_RADIUS + 3) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(flank_turf_key) + finish_action(controller, FALSE) + return + + // Face the target while moving so we look alert + if(target) + pawn.face_atom(target) + + if(get_dist(pawn, flank_turf) <= FLANK_ENGAGE_DIST) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) diff --git a/code/datums/ai/subtrees/generic_wield.dm b/code/datums/ai/subtrees/generic_wield.dm new file mode 100644 index 00000000000..068b372c9f4 --- /dev/null +++ b/code/datums/ai/subtrees/generic_wield.dm @@ -0,0 +1,28 @@ +/datum/ai_planning_subtree/generic_wield/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + var/obj/item/active = living_pawn.get_active_held_item() + var/obj/item/inactive = living_pawn.get_inactive_held_item() + if(active && inactive) + return + var/obj/item/unwielded_twohander = null + if(active?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(active, TRAIT_WIELDED)) + unwielded_twohander = active + else if(inactive?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(inactive, TRAIT_WIELDED)) + unwielded_twohander = inactive + if(unwielded_twohander) + controller.queue_behavior(/datum/ai_behavior/wield_weapon) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/wield_weapon/perform(seconds_per_tick, datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + var/obj/item/active = living_pawn.get_active_held_item() + var/obj/item/inactive = living_pawn.get_inactive_held_item() + var/obj/item/to_wield = null + if(active?.GetComponent(/datum/component/two_handed)&& !HAS_TRAIT(active, TRAIT_WIELDED)) + to_wield = active + else if(inactive?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(inactive, TRAIT_WIELDED)) + to_wield = inactive + if(to_wield) + var/datum/component/two_handed/twohanded = to_wield.GetComponent(/datum/component/two_handed) + twohanded.wield(living_pawn) + finish_action(controller, TRUE) diff --git a/code/datums/ai/subtrees/harass.dm b/code/datums/ai/subtrees/harass.dm new file mode 100644 index 00000000000..a840f4bb5f2 --- /dev/null +++ b/code/datums/ai/subtrees/harass.dm @@ -0,0 +1,161 @@ +#define HARASS_HEALTH_THRESHOLD 0.65 // below 65% max health = consider harassing +#define HARASS_STAMINA_THRESHOLD 55 // above this stamina damage = consider harassing +#define HARASS_RETREAT_DIST 4 // tiles to back off after an attack +#define HARASS_BASE_COOLDOWN (4 SECONDS) // base pause between dart-ins +#define HARASS_STAMINA_COOLDOWN_SCALE 0.05 // extra seconds per point of stamina over threshold + +/datum/ai_planning_subtree/wounded_harass + +/datum/ai_planning_subtree/wounded_harass/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || QDELETED(target)) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, FALSE) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, FALSE) + return + + var/health_frac = pawn.health / pawn.maxHealth //idk if this does anything for humans + // Stamina threshold scales down as health drops - the more hurt we are the easier tiredness triggers harass + var/effective_stamina_thresh = HARASS_STAMINA_THRESHOLD * health_frac + var/should_harass = (health_frac < HARASS_HEALTH_THRESHOLD) || (pawn.stamina > effective_stamina_thresh) + + var/currently_harassing = controller.blackboard[BB_HUMAN_NPC_HARASS_MODE] + + if(!should_harass && !currently_harassing) + return // Healthy and not tired, normal combat + + if(!should_harass && currently_harassing) + // ewxit harass mode once we finish a retreat + if(!controller.blackboard[BB_HUMAN_NPC_HARASS_RETREATING]) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, FALSE) + return + // Still retreating from last dart, finish it out + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_retreat, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, TRUE) + + if(controller.blackboard[BB_HUMAN_NPC_HARASS_RETREATING]) + if(world.time < controller.blackboard[BB_HUMAN_NPC_HARASS_COOLDOWN]) + //back off and wait + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_retreat, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, FALSE) + return SUBTREE_RETURN_FINISH_PLANNING // re-plan next tick into DART IN + + // If we're adjacent, fire the attack then immediately start retreating + if(pawn.Adjacent(target)) + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_strike, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM) + return SUBTREE_RETURN_FINISH_PLANNING + + // Not adjacent - move in + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_dart_in, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_harass_dart_in + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_dart_in/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/human_npc_harass_dart_in/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE) + return + pawn.face_atom(target) + if(pawn.Adjacent(target)) + finish_action(controller, TRUE) // adjacent, subtree will now queue the strike + return + + +/datum/ai_behavior/human_npc_harass_dart_in/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + +/datum/ai_behavior/human_npc_harass_strike + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_strike/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + var/datum/targetting_datum/td = controller.blackboard[targetting_datum_key] + + if(!target || QDELETED(target) || !td.can_attack(pawn, target)) + finish_action(controller, FALSE) + return + + if(pawn.next_move > world.time) // not ready to swing yet + finish_action(controller, FALSE) + return + + pawn.face_atom(target) + + // Pick an intent - same logic as smart melee + var/list/possible_intents = list() + for(var/datum/intent/intent as anything in pawn.possible_a_intents) + if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) + continue + possible_intents |= intent + if(length(possible_intents)) + pawn.a_intent = pick(possible_intents) + pawn.used_intent = pawn.a_intent + + if(!pawn.CanReach(target)) + finish_action(controller, FALSE) + return + + controller.ai_interact(target, TRUE, TRUE) + if(pawn.next_click < world.time) + pawn.next_click = world.time + pawn.attack_speed + SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) + + // Cooldown scales up with stamina damage and scales down with health - the more gassed/hurt, the longer we hide + var/health_frac = pawn.health / pawn.maxHealth + var/stamina_over = max(0, pawn.stamina - HARASS_STAMINA_THRESHOLD) + var/cooldown = HARASS_BASE_COOLDOWN + cooldown += stamina_over * HARASS_STAMINA_COOLDOWN_SCALE SECONDS + cooldown *= lerp(1.5, 1.0, health_frac) // up to 50% longer cooldown at 0 health + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, TRUE) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_COOLDOWN, world.time + cooldown) + finish_action(controller, TRUE) + +/datum/ai_behavior/human_npc_harass_strike/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key) + . = ..() + +/datum/ai_behavior/human_npc_harass_retreat + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_retreat/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + return FALSE + var/turf/retreat_turf = get_turf(pawn) + var/retreat_dir = REVERSE_DIR(get_dir(pawn, target)) + for(var/i in 1 to HARASS_RETREAT_DIST) + var/turf/next = get_step(retreat_turf, retreat_dir) + if(!next || !next.can_traverse_safely(pawn) || next.density) + break + retreat_turf = next + set_movement_target(controller, retreat_turf) + +/datum/ai_behavior/human_npc_harass_retreat/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + // Keep facing target while backing off so we don't turn our back + if(target && !QDELETED(target)) + pawn.face_atom(target) + var/turf/move_target = controller.current_movement_target + if(!move_target || get_dist(pawn, move_target) <= 1) + finish_action(controller, TRUE) + return. diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm new file mode 100644 index 00000000000..7aa59822a9d --- /dev/null +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -0,0 +1,315 @@ +#define HUMAN_NPC_BASE_JUKE_CHANCE 15 +#define HUMAN_NPC_JUKE_MIN_SPD 10 +#define HUMAN_NPC_JUKE_PER_OVERSPD 5 +#define HUMAN_NPC_MAX_ATTACK_STAMINA 85 +#define HUMAN_NPC_WEAKPOINT_SCAN_CHANCE 20 +#define HUMAN_NPC_WEAKPOINT_CACHE_DURATION (6 SECONDS) + +//Note alot of this is just adapted from old code so its probably not the best + +/datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc + melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/human_npc + end_planning = TRUE + +/datum/ai_behavior/basic_melee_attack/human_npc + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/basic_melee_attack/human_npc/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + + var/obj/item/held_item = pawn.get_active_held_item() + if((!isweapon(held_item))) + pawn.swap_hand() + for(var/slot in list(ITEM_SLOT_BACK, ITEM_SLOT_HIP, ITEM_SLOT_BELT_L, ITEM_SLOT_BACK_L, ITEM_SLOT_BACK_R, ITEM_SLOT_BELT_R)) + if(!pawn.get_item_by_slot(slot)) + if(pawn.equip_to_slot_if_possible(held_item, slot, disable_warning = TRUE)) + break + + var/list/possible_intents = list() + for(var/datum/intent/intent as anything in pawn.possible_a_intents) + if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) + continue + possible_intents |= intent + if(length(possible_intents)) + pawn.a_intent = pick(possible_intents) + pawn.used_intent = pawn.a_intent + + if(prob(HUMAN_NPC_WEAKPOINT_SCAN_CHANCE) && isliving(target)) + _scan_for_weakpoint(controller, pawn, target) + +/datum/ai_behavior/basic_melee_attack/human_npc/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + var/datum/targetting_datum/td = controller.blackboard[targetting_datum_key] + + if(!td.can_attack(pawn, target)) + finish_action(controller, FALSE, target_key) + return + if(ismob(target) && target:stat == DEAD) + finish_action(controller, FALSE, target_key) + return + + // Stamina gate + if(pawn.stamina > HUMAN_NPC_MAX_ATTACK_STAMINA) + finish_action(controller, FALSE, target_key) + return + + var/hiding_target = td.find_hidden_mobs(pawn, target) + controller.set_blackboard_key(hiding_location_key, hiding_target) + + pawn.face_atom(target) + _choose_attack_zone(controller, pawn, target) + + if(!pawn.CanReach(target)) + finish_action(controller, FALSE, target_key) + return + + if(hiding_target) + controller.ai_interact(hiding_target, TRUE, TRUE) + else + controller.ai_interact(target, TRUE, TRUE) + + if(pawn.next_click < world.time) + pawn.next_click = world.time + (pawn.used_intent?.clickcd * 1.2) + SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) + + if(prob(HUMAN_NPC_WEAKPOINT_SCAN_CHANCE) && isliving(target)) + _scan_for_weakpoint(controller, pawn, target) + + _try_backstep(pawn, target) + +/datum/ai_behavior/basic_melee_attack/human_npc/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + pawn.cmode = FALSE + SEND_SIGNAL(pawn, COMSIG_COMBAT_TARGET_SET, FALSE) + +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_choose_attack_zone(datum/ai_controller/controller, mob/living/carbon/human/pawn, mob/living/target) + var/list/wp = controller.blackboard[BB_HUMAN_NPC_WEAKPOINT] + if(wp && world.time < wp[2] && wp[3] == target) + var/aimheight = _zone_to_aimheight(wp[1]) + if(aimheight) + pawn.aimheight_change(aimheight) + return + + var/counter = controller.blackboard[BB_HUMAN_NPC_ATTACK_ZONE_COUNTER] + if(counter < 4) + controller.set_blackboard_key(BB_HUMAN_NPC_ATTACK_ZONE_COUNTER, counter + 1) + return + + controller.set_blackboard_key(BB_HUMAN_NPC_ATTACK_ZONE_COUNTER, 0) + controller.clear_blackboard_key(BB_HUMAN_NPC_WEAKPOINT) + + // Parity with npc_choose_attack_zone aimheight picks + if(pawn.mind?.has_antag_datum(/datum/antagonist/zombie)) + pawn.aimheight_change(pawn.deadite_get_aimheight(target)) + return + if(!(pawn.mobility_flags & MOBILITY_STAND)) + pawn.aimheight_change(rand(1, 4)) + return + if(HAS_TRAIT(target, TRAIT_BLOODLOSS_IMMUNE)) + pawn.aimheight_change(rand(12, 19)) + return + pawn.aimheight_change(pick(rand(5, 8), rand(9, 11), rand(12, 19))) + +/// Scan target bodyparts for wounded (brute/burn > 20) or unarmored zones. +/// Caches as list(zone, expiry_time, target_ref). +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_scan_for_weakpoint(datum/ai_controller/controller, mob/living/carbon/human/pawn, mob/living/target) + if(!istype(target, /mob/living/carbon/human)) + return + var/mob/living/carbon/human/htarget = target + + // Resolve weapon skill and blade class from active intent + var/skill_type = null + var/bclass = null + var/intent_reach = 1 + if(pawn.used_intent) + bclass = pawn.used_intent.blade_class + intent_reach = pawn.used_intent.reach || 1 + // Walk held items to find associated_skill + for(var/obj/item/held in pawn.get_active_held_items()) + if(held?.associated_skill) + skill_type = held.associated_skill + break + + var/skill_level = skill_type ? pawn.get_skill_level(skill_type) : SKILL_LEVEL_NONE + var/armor_rating = bclass ? bclass_to_armor_rating(bclass) : "blunt" + + var/list/wounded = list() + var/list/exposed = list() + var/list/soft = list() // armored but below meaningful resistance for our damage type + + for(var/obj/item/bodypart/part in htarget.bodyparts) + if(!part) + continue + + // Wound exploitation — requires trained eye AND good perception + if(skill_level >= SKILL_LEVEL_JOURNEYMAN && pawn.STAPER >= 10) + if(part.brute_dam > 20 || part.burn_dam > 20) + wounded += part.body_zone + + var/obj/item/worn = htarget.get_item_by_slot(part.body_zone) + if(!worn?.armor) + exposed += part.body_zone + continue + + // Basic+ fighters read armor and seek soft coverage for their damage type + if(skill_level >= SKILL_LEVEL_NOVICE) + var/rating = worn.armor.getRating(armor_rating) + if(rating < 25) + soft += part.body_zone + // Unskilled fighters just notice bare skin + else if(!worn) + exposed += part.body_zone + + // Priority: wounded > bare exposed > soft armor coverage > armored fallback (experts only) + var/chosen = null + if(length(wounded)) + chosen = pick(wounded) + else if(length(exposed)) + chosen = pick(exposed) + else if(length(soft)) + chosen = pick(soft) + else if(skill_level >= SKILL_LEVEL_EXPERT) + // Expert fallback: just pick whatever zone has the lowest resistance for our damage type + var/lowest_rating = INFINITY + var/lowest_zone = null + for(var/obj/item/bodypart/part in htarget.bodyparts) + if(!part) + continue + var/obj/item/worn = htarget.get_item_by_slot(part.body_zone) + if(!worn?.armor) + continue + var/rating = worn.armor.getRating(armor_rating) + if(rating < lowest_rating) + lowest_rating = rating + lowest_zone = part.body_zone + chosen = lowest_zone + + if(!chosen) + return + + // Skill scales how long the targeting solution stays valid + // Reach also matters — longer weapons can maintain solutions longer + // since the fighter isn't scrambling to stay close + var/cache_duration = HUMAN_NPC_WEAKPOINT_CACHE_DURATION + switch(skill_level) + if(SKILL_LEVEL_NONE) + cache_duration *= 0.1 + if(SKILL_LEVEL_NOVICE) + cache_duration *= 0.5 + if(SKILL_LEVEL_APPRENTICE) + cache_duration *= 0.75 + if(SKILL_LEVEL_JOURNEYMAN) + cache_duration *= 1.0 + if(SKILL_LEVEL_EXPERT) + cache_duration *= 1.5 + if(SKILL_LEVEL_MASTER) + cache_duration *= 2.0 + if(SKILL_LEVEL_LEGENDARY) + cache_duration *= 3.0 + + // Reach bonus: each point of reach beyond 1 adds 10% duration + // rationale: you're not fighting in a scramble, you have space to think + cache_duration *= (1 + ((intent_reach - 1) * 0.1)) + + controller.set_blackboard_key(BB_HUMAN_NPC_WEAKPOINT, list( + chosen, + world.time + cache_duration, + target, + )) + +/// Zone string -> aimheight int. +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_zone_to_aimheight(zone) + switch(zone) + if(BODY_ZONE_HEAD) + return rand(12, 19) + if(BODY_ZONE_CHEST) + return rand(9, 11) + if(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM) + return rand(5, 8) + if(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) + return rand(1, 4) + return null + +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_try_backstep(mob/living/carbon/human/pawn, atom/target) + if(pawn.mind?.has_antag_datum(/datum/antagonist/zombie)) + return FALSE + if(pawn.body_position == LYING_DOWN) + return FALSE + if(!target || !isturf(pawn.loc) || !isturf(target.loc)) + return FALSE + + var/juke_chance = HUMAN_NPC_BASE_JUKE_CHANCE + if(pawn.STASPD > HUMAN_NPC_JUKE_MIN_SPD) + juke_chance += (pawn.STASPD - HUMAN_NPC_JUKE_MIN_SPD) * HUMAN_NPC_JUKE_PER_OVERSPD + + if(!prob(juke_chance)) + return FALSE + + pawn.tempfixeye = TRUE + var/was_fixedeye = pawn.fixedeye + if(!was_fixedeye) + pawn.fixedeye = TRUE + + var/list/candidates = pawn.get_dodge_destinations(target, null) + if(!length(candidates)) + pawn.tempfixeye = FALSE + if(!was_fixedeye) + pawn.fixedeye = FALSE + return FALSE + + var/turf/juke_turf = pick(candidates) + pawn.Move(juke_turf, get_dir(pawn, juke_turf), pawn.cached_multiplicative_slowdown) + + pawn.tempfixeye = FALSE + if(!was_fixedeye) + pawn.fixedeye = FALSE + return TRUE + +/mob/living/proc/get_dodge_destinations(mob/living/attacker, atom/origin = src) + var/dodge_dir = get_dir(attacker, origin) + if(!dodge_dir) + return null + var/list/dirry = list(turn(dodge_dir, -90), dodge_dir, turn(dodge_dir, 90)) + var/list/turf/dodge_candidates = list() + for(var/dir_to_check in dirry) + var/turf/dodge_candidate = get_step(origin, dir_to_check) + if(!dodge_candidate) + continue + if(dodge_candidate.density) + continue + var/has_impassable_atom = FALSE + for(var/atom/movable/AM in dodge_candidate) + if(!AM.CanPass(src, dodge_candidate)) + has_impassable_atom = TRUE + break + if(has_impassable_atom) + continue + dodge_candidates += dodge_candidate + return dodge_candidates + +/mob/living/carbon/human/proc/deadite_get_aimheight(victim) + if(!(mobility_flags & MOBILITY_STAND)) + return rand(1, 2) // Bite their ankles! + return pick(rand(11, 13), rand(14, 17), rand(5, 8)) // Chest, neck, and mouth; face and ears; arms and hands. + +///I couldn't find anything that does this +/proc/bclass_to_armor_rating(bclass) + switch(bclass) + if(BCLASS_BLUNT, BCLASS_SMASH, BCLASS_PUNCH, BCLASS_LASHING) + return "blunt" + if(BCLASS_CUT, BCLASS_CHOP) + return "slash" + if(BCLASS_STAB, BCLASS_DRILL, BCLASS_PICK, BCLASS_TWIST, BCLASS_BITE) + return "stab" + if(BCLASS_PIERCE, BCLASS_SHOT) + return "piercing" + if(BCLASS_BURN) + return "fire" + return "blunt" // safest fallback - everything has some blunt resistance defined diff --git a/code/datums/ai/subtrees/retrieve_arrow.dm b/code/datums/ai/subtrees/retrieve_arrow.dm new file mode 100644 index 00000000000..0dd973057b6 --- /dev/null +++ b/code/datums/ai/subtrees/retrieve_arrow.dm @@ -0,0 +1,83 @@ +/datum/ai_planning_subtree/retrieve_arrows + parent_type = /datum/ai_planning_subtree/archer_base + +/datum/ai_planning_subtree/retrieve_arrows/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!validate_archer_equipment(controller)) + return + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = controller.blackboard[BB_ARCHER_NPC_BOW] + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] + + if(bow.chambered) + return + if(Q.ammo_list.len >= Q.max_storage) + return + if(!controller.blackboard[BB_ARCHER_NPC_TARGET_ARROW]) + var/obj/item/arrow = _find_nearby_arrow(get_turf(controller.pawn), Q) + if(!arrow) + return + controller.set_blackboard_key(BB_ARCHER_NPC_TARGET_ARROW, arrow) + + controller.queue_behavior(/datum/ai_behavior/retrieve_arrow, BB_ARCHER_NPC_TARGET_ARROW) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/retrieve_arrows/proc/_find_nearby_arrow(mob/living/carbon/human/pawn, obj/item/ammo_holder/quiver/Q) + var/turf/pawn_turf = get_turf(pawn) + for(var/obj/item/ammo_casing/arrow in range(ARCHER_NPC_ARROW_SEARCH_RANGE, pawn_turf)) + for(var/accepted in Q.ammo_type) + if(istype(arrow, accepted)) + return arrow + return null + + +/datum/ai_behavior/retrieve_arrow + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 0.5 SECONDS + +/datum/ai_behavior/retrieve_arrow/setup(datum/ai_controller/controller, arrow_key) + . = ..() + if(!.) + return FALSE + var/obj/item/arrow = controller.blackboard[arrow_key] + if(!arrow || QDELETED(arrow)) + controller.clear_blackboard_key(arrow_key) + return FALSE + controller.current_movement_target = arrow + return TRUE + +/datum/ai_behavior/retrieve_arrow/perform(delta_time, datum/ai_controller/controller, arrow_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/obj/item/ammo_casing/arrow = controller.blackboard[arrow_key] + + if(!arrow || QDELETED(arrow)) + finish_action(controller, FALSE, arrow_key) + return + + if(!pawn.CanReach(arrow)) + finish_action(controller, FALSE, arrow_key) + return + + // Find the quiver again at perform time in case equipment changed + var/obj/item/ammo_holder/quiver/Q = null + for(var/obj/item/ammo_holder/quiver/worn in pawn.get_equipped_items()) + for(var/accepted in worn.ammo_type) + if(istype(arrow, accepted)) + Q = worn + break + if(Q) + break + + if(!Q || Q.ammo_list.len >= Q.max_storage) + finish_action(controller, FALSE, arrow_key) + return + + // Pick up the arrow and store directly into quiver + // Mirrors ammo_holder/attackby logic but without needing a mob intermediary since we want this to just work + arrow.forceMove(Q) + Q.ammo_list += arrow + Q.update_appearance(UPDATE_ICON_STATE) + + finish_action(controller, TRUE, arrow_key) + +/datum/ai_behavior/retrieve_arrow/finish_action(datum/ai_controller/controller, succeeded, arrow_key) + . = ..() + controller.clear_blackboard_key(arrow_key) diff --git a/code/datums/threat_regions/_region.dm b/code/datums/threat_regions/_region.dm new file mode 100644 index 00000000000..da05295ca1c --- /dev/null +++ b/code/datums/threat_regions/_region.dm @@ -0,0 +1,57 @@ +/datum/threat_region + abstract_type = /datum/threat_region + var/region_name = "Generic Region Scream At Coder" + var/latent_ambush = DANGER_SAFE_FLOOR + var/min_ambush = DANGER_SAFE_FLOOR + var/max_ambush = DANGER_DIRE_LIMIT + var/fixed_ambush = FALSE // Some region like Underdark cannot be reduced in danger + var/lowpop_tick = 1 // How much ambush to tick up every iteration <= 30 pop + var/highpop_tick = 2 // How much ambush to tick up every iteration > 30 pop + var/last_natural_ambush_time = 0 + var/last_induced_ambush_time = 0 // Time between now and the previous ambush triggered by horn + +/datum/threat_region/proc/reduce_latent_ambush(amount) + if(fixed_ambush) + return + if(latent_ambush - amount < min_ambush) + latent_ambush = min_ambush + else + latent_ambush -= amount + +/datum/threat_region/proc/increase_latent_ambush(amount) + if(fixed_ambush) + return + if(latent_ambush + amount > max_ambush) + latent_ambush = max_ambush + else + latent_ambush += amount + +// Special proc because danger level is dependent on the number of latent ambush +/datum/threat_region/proc/get_danger_level() + if(latent_ambush <= DANGER_SAFE_LIMIT) + return DANGER_LEVEL_SAFE + else if(latent_ambush <= DANGER_LOW_LIMIT) + return DANGER_LEVEL_LOW + else if(latent_ambush <= DANGER_MODERATE_LIMIT) + return DANGER_LEVEL_MODERATE + else if(latent_ambush <= DANGER_DANGEROUS_LIMIT) + return DANGER_LEVEL_DANGEROUS + else if(latent_ambush <= DANGER_DIRE_LIMIT) + return DANGER_LEVEL_BLEAK + else + return DANGER_LEVEL_SAFE + +/datum/threat_region/proc/get_danger_color(level) + switch(get_danger_level()) + if(DANGER_LEVEL_SAFE) + return "#00FF00" + if(DANGER_LEVEL_LOW) + return "#FFFF00" + if(DANGER_LEVEL_MODERATE) + return "#FFA500" + if(DANGER_LEVEL_DANGEROUS) + return "#FF0000" + if(DANGER_LEVEL_BLEAK) + return "#800080" + else + return "#FFFFFF" diff --git a/code/datums/threat_regions/basin.dm b/code/datums/threat_regions/basin.dm new file mode 100644 index 00000000000..b1c37ec1153 --- /dev/null +++ b/code/datums/threat_regions/basin.dm @@ -0,0 +1,8 @@ +/datum/threat_region/basin + region_name= THREAT_REGION_BASIN + latent_ambush = DANGER_LOW_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/coast.dm b/code/datums/threat_regions/coast.dm new file mode 100644 index 00000000000..5a47358823c --- /dev/null +++ b/code/datums/threat_regions/coast.dm @@ -0,0 +1,8 @@ +/datum/threat_region/coast + region_name = THREAT_REGION_COAST + latent_ambush = DANGER_MODERATE_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/mount_decap.dm b/code/datums/threat_regions/mount_decap.dm new file mode 100644 index 00000000000..9f3d8c82224 --- /dev/null +++ b/code/datums/threat_regions/mount_decap.dm @@ -0,0 +1,8 @@ +/datum/threat_region/mount_decap + region_name = THREAT_REGION_MOUNT_DECAP + latent_ambush = DANGER_DANGEROUS_FLOOR + min_ambush = DANGER_MODERATE_FLOOR + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/threat_regions/north_grove.dm b/code/datums/threat_regions/north_grove.dm new file mode 100644 index 00000000000..6acf7e763e7 --- /dev/null +++ b/code/datums/threat_regions/north_grove.dm @@ -0,0 +1,8 @@ +/datum/threat_region/northern_grove + region_name = THREAT_REGION_NORTHERN_GROVE + latent_ambush = DANGER_MODERATE_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/outer_grove.dm b/code/datums/threat_regions/outer_grove.dm new file mode 100644 index 00000000000..8847cdd81ee --- /dev/null +++ b/code/datums/threat_regions/outer_grove.dm @@ -0,0 +1,8 @@ +/datum/threat_region/outer_grove + region_name = THREAT_REGION_OUTER_GROVE + latent_ambush = DANGER_MODERATE_LIMIT + min_ambush = DANGER_MODERATE_FLOOR + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/threat_regions/terror_bog.dm b/code/datums/threat_regions/terror_bog.dm new file mode 100644 index 00000000000..2decb7f1f58 --- /dev/null +++ b/code/datums/threat_regions/terror_bog.dm @@ -0,0 +1,8 @@ +/datum/threat_region/terrorbog + region_name = THREAT_REGION_TERRORBOG + latent_ambush = DANGER_DIRE_LIMIT + min_ambush = DANGER_SAFE_FLOOR // This is intended. A warden can engage in a long war to tame the terrorbog. + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/world_factions/traders/trade_data_types/zhongese.dm b/code/datums/world_factions/traders/trade_data_types/zhongese.dm index 43dd0b46ade..570e31a8ef3 100644 --- a/code/datums/world_factions/traders/trade_data_types/zhongese.dm +++ b/code/datums/world_factions/traders/trade_data_types/zhongese.dm @@ -95,7 +95,7 @@ /obj/item/weapon/sword/katana/mulyeog/rumahench = list(4, 65, 2), /obj/item/weapon/sword/katana/mulyeog/rumacaptain = list(6, 120, 1), /obj/item/weapon/sword/sabre/hook = list(5, 85, 2), - /obj/item/weapon/spear/naginata = list(6, 90, 2), + /obj/item/weapon/polearm/spear/naginata = list(6, 90, 2), /obj/item/weapon/knife/dagger/navaja = list(6, 75, 1), /obj/item/weapon/whip/nagaika = list(5, 60, 2), ) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 5196fd6f6d5..e3cdb20f4b9 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -87,6 +87,9 @@ var/list/ambush_times var/converted_type + + var/threat_region = "" // Key used to look up threat region this area belongs to + var/delver_restrictions = FALSE var/coven_protected = FALSE diff --git a/code/game/area/roguetownareas.dm b/code/game/area/roguetownareas.dm index 51e67cf667c..ee8d66eef25 100644 --- a/code/game/area/roguetownareas.dm +++ b/code/game/area/roguetownareas.dm @@ -67,9 +67,16 @@ ambush_types = list( /turf/open/floor/dirt) ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/troll = 30, - /mob/living/carbon/human/species/skeleton/npc/ambush = 30, - /mob/living/carbon/human/species/goblin/npc/ambush/cave = 60) + new /datum/ambush_config/pair_of_direbear = 10, + new /datum/ambush_config/trio_of_highwaymen = 10, + new /datum/ambush_config/singular_minotaur = 10, + new /datum/ambush_config/duo_minotaur = 5, + new /datum/ambush_config/solo_treasure_hunter = 15, + new /datum/ambush_config/duo_treasure_hunter = 2, + new /datum/ambush_config/medium_skeleton_party = 10, + new /datum/ambush_config/heavy_skeleton_party = 5, + ) + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/mountains/decap name = "mt decapitation" @@ -77,9 +84,15 @@ ambush_types = list( /turf/open/floor/dirt) ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/troll = 30, - /mob/living/carbon/human/species/skeleton/npc/ambush = 90, - /mob/living/carbon/human/species/goblin/npc/ambush/cave = 20) + new /datum/ambush_config/pair_of_direbear = 10, + new /datum/ambush_config/trio_of_highwaymen = 10, + new /datum/ambush_config/singular_minotaur = 10, + new /datum/ambush_config/duo_minotaur = 5, + new /datum/ambush_config/solo_treasure_hunter = 15, + new /datum/ambush_config/duo_treasure_hunter = 2, + new /datum/ambush_config/medium_skeleton_party = 10, + new /datum/ambush_config/heavy_skeleton_party = 5, + ) background_track = 'sound/music/area/decap.ogg' background_track_dusk = null background_track_night = null @@ -88,11 +101,14 @@ ambush_times = list("night","dawn","dusk","day") converted_type = /area/indoors/shelter/mountains/decap + threat_region = THREAT_REGION_MOUNT_DECAP + /area/indoors/shelter/mountains/decap icon_state = "decap" background_track = 'sound/music/area/decap.ogg' background_track_dusk = null background_track_night = null + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/basin name = "town basin" @@ -110,6 +126,7 @@ background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' converted_type = /area/indoors/shelter/basin + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/basin/Initialize() . = ..() @@ -124,6 +141,7 @@ background_track = 'sound/music/area/field.ogg' background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' + threat_region = THREAT_REGION_MOUNT_DECAP /area/indoors/shelter/woods icon_state = "woods" @@ -174,11 +192,18 @@ /mob/living/simple_animal/hostile/retaliate/bigrat = 20, /mob/living/simple_animal/hostile/retaliate/spider = 80, /mob/living/carbon/human/species/goblin/npc/ambush/sea = 50, - /mob/living/simple_animal/hostile/retaliate/troll/bog = 35) + /mob/living/simple_animal/hostile/retaliate/troll/bog = 35, + new /datum/ambush_config/bog_guard_deserters = 50, + new /datum/ambush_config/bog_guard_deserters/hard = 25, + new /datum/ambush_config/mirespiders_ambush = 110, + new /datum/ambush_config/mirespiders_crawlers = 25, + new /datum/ambush_config/mirespiders_aragn = 10, + new /datum/ambush_config/mirespiders_unfair = 5) first_time_text = "THE TERRORBOG" custom_area_sound = 'sound/misc/stings/BogSting.ogg' converted_type = /area/indoors/shelter/bog + threat_region = THREAT_REGION_TERRORBOG /area/indoors/shelter/bog icon_state = "bog" @@ -194,6 +219,14 @@ background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' + ambush_mobs = list( + /mob/living/carbon/human/species/goblin/npc/ambush/sea = 20, + new /datum/ambush_config/triple_deepone = 30, + new /datum/ambush_config/deepone_party = 20, + ) + + threat_region = THREAT_REGION_COAST + /area/outdoors/eora name = "eoran grove" icon_state = "eora" diff --git a/code/game/area/wilderness.dm b/code/game/area/wilderness.dm index cc4e042238f..fdaefbcc2c3 100644 --- a/code/game/area/wilderness.dm +++ b/code/game/area/wilderness.dm @@ -12,21 +12,33 @@ ambush_times = list("night","dawn","dusk","day") ambush_types = list( /turf/open/floor/grass) + ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/wolf = 60, - /mob/living/simple_animal/hostile/retaliate/troll/axe = 10, - /mob/living/carbon/human/species/goblin/npc/ambush = 45, - /mob/living/simple_animal/hostile/retaliate/mole = 25) + new /datum/ambush_config/wolf_pack = 15, + new /datum/ambush_config/lone_troll = 10, + new /datum/ambush_config/troll_and_wolves = 8, + new /datum/ambush_config/goblin_ambush_party = 15, + new /datum/ambush_config/goblin_raid_party = 8, + new /datum/ambush_config/raccoon_swarm = 20, + new /datum/ambush_config/mole_pack = 15, + new /datum/ambush_config/deserter_patrol = 12, + new /datum/ambush_config/highwayman_duo = 10, + new /datum/ambush_config/highwayman_gang = 6, + new /datum/ambush_config/mixed_wildlife = 15, + ) first_time_text = "THE MURDERWOOD" custom_area_sound = 'sound/misc/stings/ForestSting.ogg' converted_type = /area/indoors/shelter/woods + threat_region = THREAT_REGION_OUTER_GROVE /area/outdoors/wilderness/outpost icon_state = "outpost" + threat_region = THREAT_REGION_NORTHERN_GROVE /area/outdoors/wilderness/outpost/vanderlin name = "abandoned outpost" first_time_text = "Thatchwood Outpost" + threat_region = THREAT_REGION_NORTHERN_GROVE /area/outdoors/wilderness/outpost/salem name = "salem outpost" diff --git a/code/game/objects/effects/glowshroom.dm b/code/game/objects/effects/glowshroom.dm index d2a64288e04..13d916b7bae 100644 --- a/code/game/objects/effects/glowshroom.dm +++ b/code/game/objects/effects/glowshroom.dm @@ -37,7 +37,7 @@ if(L.electrocute_act(30, src)) L.emote("painscream") L.update_sneak_invis(TRUE) - L.consider_ambush() + L.consider_ambush(always = TRUE) if(L.throwing) L.throwing.finalize(FALSE) . = ..() @@ -48,7 +48,7 @@ var/mob/living/L = user if(L.electrocute_act(30, src)) // The kneestingers will let you pass if you worship dendor, but they won't take your stupid ass hitting them. L.emote("painscream") - L.consider_ambush() + L.consider_ambush(always = TRUE) if(L.throwing) L.throwing.finalize(FALSE) return FALSE diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index b652a48abfd..a82c3f013ba 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1340,6 +1340,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e return TRUE /obj/item/proc/canStrip(mob/stripper, mob/owner) + if(HAS_TRAIT(loc, TRAIT_STUCKITEMS)) + return FALSE return !HAS_TRAIT(src, TRAIT_NODROP) /obj/item/proc/doStrip(mob/stripper, mob/owner) diff --git a/code/game/objects/items/signal_horn.dm b/code/game/objects/items/signal_horn.dm index 071b99bb17b..76ea558d18e 100644 --- a/code/game/objects/items/signal_horn.dm +++ b/code/game/objects/items/signal_horn.dm @@ -1,3 +1,21 @@ +#define WARDEN_AMBUSH_MIN 2 +#define WARDEN_AMBUSH_MAX 9 + +/datum/status_effect/debuff/clickcd + id = "clickcd" + alert_type = /atom/movable/screen/alert/status_effect/debuff/clickcd + duration = 3 SECONDS + +/datum/status_effect/debuff/clickcd/on_creation(mob/living/new_owner, new_dur) + if(new_dur) + duration = new_dur + new_owner.changeNext_move(duration) + return ..() + +/atom/movable/screen/alert/status_effect/debuff/clickcd + name = "Action Delayed" + desc = "I cannot take another action." + /obj/item/signal_horn name = "signal horn" desc = "Used to sound the alarm." @@ -9,18 +27,36 @@ grid_width = 64 COOLDOWN_DECLARE(sound_horn) +/obj/item/signal_horn/examine() + . = ..() + . += span_notice("Using the horn will make you stand still and induce several ambushes to happen at once, enabling you to clear out an area. It cannot be used in rapid succession.") + . += span_notice("Using it will leave you exhausted for a moment. Bring friends!") + /obj/item/signal_horn/attack_self(mob/living/user, list/modifiers) . = ..() - if(!COOLDOWN_FINISHED(src, sound_horn)) - to_chat(user, span_warning("[src] is not ready to be used yet!")) + var/area/AR = get_area(user) + var/datum/threat_region/TR = SSregionthreat.get_region(AR.threat_region) + if(!TR || !TR.latent_ambush || TR.fixed_ambush) + to_chat(user, span_warning("There's no point in sounding the horn here.")) + return + if(user.get_will_block_ambush()) + to_chat(user, span_warning("This place is too well-lit for enemies to come.")) + return + if(!user.get_possible_ambush_spawn(min_dist = WARDEN_AMBUSH_MIN, max_dist = WARDEN_AMBUSH_MAX)) + to_chat(user, span_warning("This place is too lightly vegetated for enemies to hide.")) return - user.visible_message(span_warning("[user] is about to sound [src]!")) - if(do_after(user, 1.5 SECONDS)) + if(TR && TR.last_induced_ambush_time && (world.time < TR.last_induced_ambush_time + 5 MINUTES)) + to_chat(user, span_warning("Foes have been cleared out here recently, perhaps you should wait a moment before sounding the horn again.")) + return + user.visible_message(span_userdanger("[user] is about to sound [src]!")) + user.apply_status_effect(/datum/status_effect/debuff/clickcd, 5 SECONDS) // We don't want them to spam the message. + if(do_after(user, 30 SECONDS)) // Enough time for any antag to kick or interrupt third party, me think + TR.last_induced_ambush_time = world.time + user.Immobilize(30) // A very crude solution to kill any solo gamer sound_horn(user) - COOLDOWN_START(src, sound_horn, 1 MINUTES) /obj/item/signal_horn/proc/sound_horn(mob/living/user) - user.visible_message(span_warning("[user] sounds the alarm!")) + user.visible_message(span_danger("[user] sounds the horn!")) // New sound made by fem_tanyl playsound(src, 'sound/items/signalhorn.ogg', 100, TRUE) var/turf/origin_turf = get_turf(src) @@ -34,7 +70,7 @@ continue var/distance = get_dist(player, origin_turf) - if(distance <= 7) + if(distance <= 7 || distance > 21) // two screens away player.apply_status_effect(/datum/status_effect/signal_horn, null, user) continue var/dirtext = " to the " @@ -75,6 +111,11 @@ player.playsound_local(get_turf(player), 'sound/items/signalhorn.ogg', 35, FALSE, pressure_affected = FALSE) to_chat(player, span_warning("I hear the horn alarm somewhere[disttext][dirtext]!")) + var/random_ambushes = 4 + rand(0,2) // 4 - 6 ambushes + for(var/i = 0, i < random_ambushes, i++) + user.consider_ambush(TRUE, TRUE, min_dist = WARDEN_AMBUSH_MIN, max_dist = WARDEN_AMBUSH_MAX) + + /datum/status_effect/signal_horn id = "signal horn indicator" duration = 2 SECONDS diff --git a/code/game/objects/items/weapons/melee/polearms.dm b/code/game/objects/items/weapons/melee/polearms.dm index b13eb28f701..19a379eaf9b 100644 --- a/code/game/objects/items/weapons/melee/polearms.dm +++ b/code/game/objects/items/weapons/melee/polearms.dm @@ -600,7 +600,7 @@ w_class = WEIGHT_CLASS_BULKY melting_material = null -/obj/item/weapon/spear/naginata +/obj/item/weapon/polearm/spear/naginata name = "Naginata" desc = "A traditional eastern polearm, combining the reach of a spear with the cutting power of a curved blade. Due to the brittle quality of certain eastern bladesmithing, weaponsmiths have adapted its blade to be easily replaceable when broken by a peg upon the end of the shaft." icon = 'icons/roguetown/weapons/64/polearms.dmi' @@ -612,7 +612,7 @@ max_blade_int = 100 //Nippon suteeru (dogshit) minstr = 7 -/obj/item/weapon/spear/naginata/getonmobprop(tag) +/obj/item/weapon/polearm/spear/naginata/getonmobprop(tag) . = ..() if(tag) switch(tag) diff --git a/code/game/objects/items/weapons/ranged/bows.dm b/code/game/objects/items/weapons/ranged/bows.dm index b69dd9bf6c5..eb874a342bb 100644 --- a/code/game/objects/items/weapons/ranged/bows.dm +++ b/code/game/objects/items/weapons/ranged/bows.dm @@ -79,8 +79,12 @@ spread = 0 for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE)) var/obj/projectile/BB = CB.BB - if(user.client.chargedprog < 100) - BB.damage = BB.damage - (BB.damage * (user.client.chargedprog / 100)) + if(!user.client || user.client?.chargedprog < 100) + var/charge_prob = 1 + if(user.client) + charge_prob = (user.client.chargedprog / 100) + + BB.damage = BB.damage - (BB.damage * charge_prob) BB.embedchance = 5 else BB.damage = BB.damage diff --git a/code/modules/ambush/_ambush_config.dm b/code/modules/ambush/_ambush_config.dm new file mode 100644 index 00000000000..4a81a831946 --- /dev/null +++ b/code/modules/ambush/_ambush_config.dm @@ -0,0 +1,2 @@ +/datum/ambush_config + var/list/mob_types = list() diff --git a/code/modules/ambush/bog_ambush.dm b/code/modules/ambush/bog_ambush.dm new file mode 100644 index 00000000000..5bbcbcc72f2 --- /dev/null +++ b/code/modules/ambush/bog_ambush.dm @@ -0,0 +1,34 @@ +/datum/ambush_config/bog_guard_deserters + mob_types = list( + /mob/living/carbon/human/species/human/northern/bog_deserters/ambush = 2, + /mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush = 1 + ) + +/datum/ambush_config/bog_guard_deserters/hard + mob_types = list( + /mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush = 2, + /mob/living/carbon/human/species/human/northern/bog_deserters/ambush = 1, + ) + +/datum/ambush_config/mirespiders_ambush + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 2, + /mob/living/simple_animal/hostile/mirespider_lurker = 1 + ) + +/datum/ambush_config/mirespiders_crawlers + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 4, + ) + +/datum/ambush_config/mirespiders_aragn + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 2, + /mob/living/simple_animal/hostile/mirespider_paralytic = 1 + ) + +/datum/ambush_config/mirespiders_unfair + mob_types = list( + /mob/living/simple_animal/hostile/mirespider_paralytic = 2, + /mob/living/simple_animal/hostile/mirespider_lurker = 1 + ) diff --git a/code/modules/ambush/coast.dm b/code/modules/ambush/coast.dm new file mode 100644 index 00000000000..37627e4245d --- /dev/null +++ b/code/modules/ambush/coast.dm @@ -0,0 +1,21 @@ +/datum/ambush_config/triple_deepone + mob_types = list( + /mob/living/simple_animal/hostile/deepone = 3 + ) + +/datum/ambush_config/deepone_party + mob_types = list( + /mob/living/simple_animal/hostile/deepone = 1, + /mob/living/simple_animal/hostile/deepone/spit = 1, + /mob/living/simple_animal/hostile/deepone/wiz = 1 + ) + +/datum/ambush_config/singular_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 1 + ) + +/datum/ambush_config/duo_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 2 + ) diff --git a/code/modules/ambush/mount_decap.dm b/code/modules/ambush/mount_decap.dm new file mode 100644 index 00000000000..c4a02ad2ab5 --- /dev/null +++ b/code/modules/ambush/mount_decap.dm @@ -0,0 +1,30 @@ +/datum/ambush_config/pair_of_direbear + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/direbear = 2 + ) + +/datum/ambush_config/trio_of_highwaymen + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 3 + ) + +/datum/ambush_config/singular_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 1 + ) + +/datum/ambush_config/duo_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 2 + ) + +/datum/ambush_config/medium_skeleton_party + mob_types = list( + /mob/living/carbon/human/species/skeleton/npc/medium = 3 + ) + +/datum/ambush_config/heavy_skeleton_party + mob_types = list( + /mob/living/carbon/human/species/skeleton/npc/medium = 1, + /mob/living/carbon/human/species/skeleton/npc/hard = 2 + ) diff --git a/code/modules/ambush/treasure_hunters.dm b/code/modules/ambush/treasure_hunters.dm new file mode 100644 index 00000000000..f0c7a825098 --- /dev/null +++ b/code/modules/ambush/treasure_hunters.dm @@ -0,0 +1,14 @@ +/datum/ambush_config/solo_treasure_hunter + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 1, + ) + +/datum/ambush_config/duo_treasure_hunter + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 2, + ) + +/datum/ambush_config/treasure_hunter_posse + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 3, + ) diff --git a/code/modules/ambush/wilderness.dm b/code/modules/ambush/wilderness.dm new file mode 100644 index 00000000000..52491fb2a4b --- /dev/null +++ b/code/modules/ambush/wilderness.dm @@ -0,0 +1,56 @@ +/datum/ambush_config/wolf_pack + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/wolf = 3 + ) + +/datum/ambush_config/lone_troll + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/troll/axe = 1 + ) + +/datum/ambush_config/troll_and_wolves + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/troll/axe = 1, + /mob/living/simple_animal/hostile/retaliate/wolf = 2 + ) + +/datum/ambush_config/goblin_ambush_party + mob_types = list( + /mob/living/carbon/human/species/goblin/npc/ambush = 3 + ) + +/datum/ambush_config/goblin_raid_party + mob_types = list( + /mob/living/carbon/human/species/goblin/npc/ambush = 5 + ) + +/datum/ambush_config/raccoon_swarm + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/raccoon = 4 + ) + +/datum/ambush_config/mole_pack + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mole = 3 + ) + +/datum/ambush_config/deserter_patrol + mob_types = list( + /mob/living/carbon/human/species/human/northern/militia/deserter = 3 + ) + +/datum/ambush_config/highwayman_duo + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 2 + ) + +/datum/ambush_config/highwayman_gang + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 4 + ) + +/datum/ambush_config/mixed_wildlife + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/wolf = 2, + /mob/living/simple_animal/hostile/retaliate/raccoon = 2 + ) diff --git a/code/modules/clothing/belt/misc.dm b/code/modules/clothing/belt/misc.dm index 8b05a2282b3..55c84f4d255 100644 --- a/code/modules/clothing/belt/misc.dm +++ b/code/modules/clothing/belt/misc.dm @@ -176,6 +176,18 @@ grid_height = 64 grid_width = 32 +/obj/item/storage/belt/pouch/medicine + populate_contents = list( + /obj/item/needle, + /obj/item/natural/bundle/cloth/bandage/full, + /obj/item/reagent_containers/glass/bottle/healthpot + ) + +/obj/item/storage/belt/pouch/food + populate_contents = list( + /obj/item/reagent_containers/food/snacks/hardtack, + ) + /obj/item/storage/belt/pouch/coins/mid/Initialize() . = ..() var/obj/item/coin/silver/pile/H = new(loc) diff --git a/code/modules/food_and_drinks/rations.dm b/code/modules/food_and_drinks/rations.dm new file mode 100644 index 00000000000..e489c2d63c3 --- /dev/null +++ b/code/modules/food_and_drinks/rations.dm @@ -0,0 +1,54 @@ +// Design wise, I want to gate this behind high cooking skills role who can wrap their food and provide a convenient source of +// Stats-boosting food for adventurers / other alike. Currently there's very few good preserved food items that also give foodbuffs. +// This is a flavorful way to provide more reasons for adventurers to buy from innkeep instead of doing their own raisins. +// FOR GODS SAKE DO NOT GIVE THIS ROUNDSTART TO ANYONE BUT INNKEEP ON MAP. +/obj/item/ration // Ration. Sprites and Concept by Pintlewaiver. + name = "ration wrapping paper" + desc = "A piece of paper greased with a thin layer of oil, used to wrap food and preserve it for a long journey. \ + The final size of the ration depends on the size of the original food item. The food will last as long as the ration is wrapped." + icon = 'icons/obj/ration.dmi' + icon_state = "ration_wrapper" + w_class = WEIGHT_CLASS_TINY + grid_height = 32 + grid_width = 32 + dropshrink = 0.6 + var/obj/item/reagent_containers/food/snacks/food = null // The food item wrapped in the ration + +/obj/item/ration/attackby(obj/item/I, mob/user) + . = ..() + if(istype(I, /obj/item/reagent_containers/food/snacks)) + var/obj/item/reagent_containers/food/snacks/F = I + if(food) + to_chat(user, span_warning("There is already something wrapped in [src].")) + return + if(do_after(user, 2 SECONDS, target = src)) + user.transferItemToLoc(F, src) + food = I + to_chat(user, span_notice("You wrap [F] in the ration wrapper.")) + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + F.rotprocess = null + if(I.w_class >= WEIGHT_CLASS_NORMAL) + name = "large ration pack ([food.name])" + desc = "A large ration pack containing a [food.name]." + icon_state = "ration_large" + dropshrink = 1 + // No need to change grid size cuz cakes are 1x1 huh?? + else + name = "small ration pack ([food.name])" + desc = "A small ration pack containing a [food.name]." + icon_state = "ration_small" + dropshrink = 1 + update_icon() + +/obj/item/ration/attack_self(mob/user) + . = ..() + if(food) + if(do_after(user, 2 SECONDS, target = src)) + to_chat(user, span_notice("You unwrap [food] from the ration wrapper.")) + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + var/obj/item/reagent_containers/food/snacks/F = food + user.put_in_hands(F) + F.update_icon() + F.rotprocess = initial(F.rotprocess) + food = null + qdel(src) // No reusing wrapper diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index b010b6e05d9..549125d8a9d 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -11,6 +11,10 @@ var/datum/job/parent_job /// When joining the round, this text will be shown to the player. var/tutorial = null + /// Whether this job is intended to give quests + var/is_quest_giver = FALSE + /// How many quests this job can take at once + var/max_active_quests = 3 /// Id for the Job. var/id //Bitflags for the job diff --git a/code/modules/jobs/job_types/apprentices/shophand.dm b/code/modules/jobs/job_types/apprentices/shophand.dm index a697ddacbbd..b159a2b8cb5 100644 --- a/code/modules/jobs/job_types/apprentices/shophand.dm +++ b/code/modules/jobs/job_types/apprentices/shophand.dm @@ -10,6 +10,7 @@ total_positions = 1 spawn_positions = 1 display_order = JDO_SHOPHAND + is_quest_giver = TRUE give_bank_account = 10 bypass_lastclass = TRUE can_have_apprentices = FALSE diff --git a/code/modules/jobs/job_types/nobility/steward.dm b/code/modules/jobs/job_types/nobility/steward.dm index 7652226dc44..bcad824bd9f 100644 --- a/code/modules/jobs/job_types/nobility/steward.dm +++ b/code/modules/jobs/job_types/nobility/steward.dm @@ -11,6 +11,7 @@ total_positions = 1 spawn_positions = 1 bypass_lastclass = TRUE + is_quest_giver = TRUE allowed_races = RACES_PLAYER_NONDISCRIMINATED blacklisted_species = list(SPEC_ID_HALFLING) outfit = /datum/outfit/steward diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index b121c80a8cb..b01a9dd8699 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -5,6 +5,8 @@ /mob/proc/get_active_held_item() return get_item_for_held_index(active_hand_index) +/mob/proc/get_active_held_items() + return list(get_item_for_held_index(active_hand_index), get_item_for_held_index(get_inactive_hand_index())) //Finds the opposite limb for the active one (eg: upper left arm will find the item in upper right arm) //So we're treating each "pair" of limbs as a team, so "both" refers to them diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index d3fb0cacdc5..fa78fc9fc0d 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -1,7 +1,8 @@ // Mobs shouldn't consider their own ambush. // This should be it's own system. Please. -#define AMBUSH_CHANCE 5 +GLOBAL_VAR_INIT(ambush_chance_pct, 20) // Please don't raise this over 100 admins :') +GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each individual mob being considered for an ambush /mob/living/proc/ambushable() if(!mind) @@ -14,78 +15,168 @@ return FALSE return ambushable && !HAS_TRAIT(src, TRAIT_NOAMBUSH) -/mob/living/proc/consider_ambush() - if(!prob(AMBUSH_CHANCE)) - return - if(!MOBTIMER_FINISHED(src, MT_AMBUSHCHECK, 15 SECONDS)) - return - MOBTIMER_SET(src, MT_AMBUSHCHECK) - - if(!ambushable()) - return +/mob/living/proc/consider_ambush(always = FALSE, ignore_cooldown = FALSE, min_dist = 1, max_dist = 7) var/area/AR = get_area(src) - if(!length(AR.ambush_mobs)) + var/datum/threat_region/TR = SSregionthreat.get_region(AR.threat_region) + var/danger_level = DANGER_LEVEL_MODERATE // Fallback if there's no region + if(TR) + danger_level = TR.get_danger_level() + if(danger_level == DANGER_LEVEL_SAFE) + if(TR.latent_ambush == 0) + return + if(TR.latent_ambush <= DANGER_SAFE_LIMIT && !always) // Signal horn can dip below 10 + return + if(TR && ((world.time - TR.last_natural_ambush_time + 1 MINUTES) < 1 MINUTES)) return - var/turf/T = get_turf(src) - if(!T) + var/true_ambush_chance = GLOB.ambush_chance_pct + if(TR) + if(danger_level == DANGER_LEVEL_LOW) + true_ambush_chance *= 0.5 + else if(danger_level == DANGER_LEVEL_DANGEROUS) + true_ambush_chance *= 1.5 + else if(danger_level == DANGER_LEVEL_BLEAK) + true_ambush_chance *= 2 + if(!always && prob(100 - true_ambush_chance)) return - if(!(T.type in AR.ambush_types)) + if(get_will_block_ambush(src)) return - for(var/obj/machinery/light/fueled/RF in view(5, src)) - if(RF.on) + if(mob_timers["ambush_check"] && !ignore_cooldown) + if(world.time < mob_timers["ambush_check"] + GLOB.ambush_mobconsider_cooldown) return + mob_timers["ambush_check"] = world.time var/victims = 1 - var/list/victims_list + var/list/victimsa = list() for(var/mob/living/V in view(5, src)) if(V != src) if(V.ambushable()) victims++ - LAZYADD(victims_list, V) + victimsa += V if(victims > 3) return - var/static/list/valid_targets = list( - /obj/structure/flora/tree, \ - /obj/structure/flora/shroom_tree, \ - /obj/structure/flora/newtree - ) - var/list/possible_targets - for(var/obj/structure/object in oview(5, src)) //do not count the player - if(is_type_in_list(object, valid_targets)) - LAZYADD(possible_targets, get_turf(object)) - if(!LAZYLEN(possible_targets)) - return - MOBTIMER_SET(src, MT_AMBUSHLAST) - for(var/mob/living/V as anything in victims_list) - MOBTIMER_SET(V, MT_AMBUSHLAST) - var/spawnedtype = pickweight(AR.ambush_mobs) - var/mustype = 1 - for(var/i in 1 to clamp(victims, 2, 3)) - var/spawnloc = safepick(possible_targets) - if(!spawnloc) - return - var/mob/spawnedmob = new spawnedtype(spawnloc) - if(istype(spawnedmob, /mob/living/simple_animal/hostile)) - var/mob/living/simple_animal/hostile/M = spawnedmob - M.del_on_deaggro = 44 SECONDS - M.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) - if(istype(spawnedmob, /mob/living/carbon/human)) - var/mob/living/carbon/human/H = spawnedmob - H.del_on_deaggro = 44 SECONDS - H.last_aggro_loss = world.time - H.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) - mustype = 2 + var/list/possible_targets = get_possible_ambush_spawn(min_dist, max_dist) + if(possible_targets.len) + mob_timers["ambushlast"] = world.time + for(var/mob/living/V in victimsa) + V.mob_timers["ambushlast"] = world.time + if(TR) + TR.reduce_latent_ambush(1) // Remove one ambush from the ambient pool + TR.last_natural_ambush_time = world.time + var/list/mobs_to_spawn = list() + var/mobs_to_spawn_single = FALSE + var/max_spawns = 3 + var/mustype = 1 + var/spawnedtype = pickweight(AR.ambush_mobs) + + // This is the part where we scale ambush difficulty based on threat. Due to how we have a mix of + // Ambush Config and Single Mob Ambush, I use a weird scaling system: + // Single Mob + // Low - 1 Mob only + // Moderate - 1 to 2 (This is REALLY moderate) + // Dangerous - 2 to 3 + // Dire - 3 to 4 + // Ambush Difficulty Scaling: + // Low = -1 Mob + // Dangerous = +1 Mob + // Dire = + 2 Mobs + // Previous ambush system is 2 mobs, unless there's 3 victims, in which 3 mobs + // And Ambush Config number is fixed + + if(ispath(spawnedtype, /mob/living)) + switch(danger_level) + if(DANGER_LEVEL_SAFE) // Induced Ambush + max_spawns = 1 + if(DANGER_LEVEL_LOW) + max_spawns = 1 + if(DANGER_LEVEL_MODERATE) + max_spawns = rand(1, 2) // This is lower than before, to make moderate easier to deal with + if(DANGER_LEVEL_DANGEROUS) + max_spawns = rand(2, 3) + if(DANGER_LEVEL_BLEAK) + max_spawns = rand(3, 4) + mobs_to_spawn_single = TRUE + else if(istype(spawnedtype, /datum/ambush_config)) + var/datum/ambush_config/A = spawnedtype + for(var/type_path in A.mob_types) + var/amt = A.mob_types[type_path] + for(var/i in 1 to amt) + mobs_to_spawn += type_path + if(mobs_to_spawn.len > 1) + switch(danger_level) + if(DANGER_LEVEL_SAFE) + var/ri = rand(1, mobs_to_spawn.len) + mobs_to_spawn.Cut(ri, ri + 1) // Randomly remove one mob + if(DANGER_LEVEL_LOW) + var/ri = rand(1, mobs_to_spawn.len) + mobs_to_spawn.Cut(ri, ri + 1) // Randomly remove one mob + if(DANGER_LEVEL_DANGEROUS) + mobs_to_spawn += pick(mobs_to_spawn) // Randomly add 1 + if(DANGER_LEVEL_BLEAK) + mobs_to_spawn += pick(mobs_to_spawn) // Randomly add 2 + mobs_to_spawn += pick(mobs_to_spawn) + max_spawns = mobs_to_spawn.len + + for(var/i in 1 to max_spawns) + var/spawnloc = pick(possible_targets) + if(spawnloc) + var/mob_type + if(mobs_to_spawn_single) + mob_type = spawnedtype + else + if(!mobs_to_spawn.len) + continue + mob_type = mobs_to_spawn[1] + var/mob/spawnedmob = new mob_type(spawnloc) + if(mobs_to_spawn.len && !mobs_to_spawn_single) + mobs_to_spawn.Cut(1, 2) + if(istype(spawnedmob, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/M = spawnedmob + M.del_on_deaggro = 44 SECONDS + M.faction += "ambush" + if(istype(spawnedmob, /mob/living/carbon/human)) + var/mob/living/carbon/human/H = spawnedmob + H.del_on_deaggro = 44 SECONDS + H.last_aggro_loss = world.time + H.faction += "ambush" + mustype = 2 + if(mustype == 1) + playsound_local(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) + else + playsound_local(src, pick('sound/misc/jumphumans (1).ogg','sound/misc/jumphumans (2).ogg','sound/misc/jumphumans (3).ogg'), 100) + shake_camera(src, 2, 2) + +// Return whether a mob is blocked from being ambushed +/mob/living/proc/get_will_block_ambush() + if(!ambushable()) + return TRUE + var/campfires = 0 + for(var/obj/machinery/light/RF in view(5, src)) + if(RF.on) + campfires++ + if(campfires > 0) + return TRUE + +/mob/living/proc/get_possible_ambush_spawn(min_dist = 2, max_dist = 7) + var/list/possible_targets = list() + for(var/obj/structure/flora/tree/RT in orange(max_dist, src)) + if(istype(RT,/obj/structure/flora/tree/stump)) + continue + if(isturf(RT.loc) && !get_dist(RT.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RT.loc) + for(var/obj/structure/flora/grass/bush/RB in orange(max_dist, src)) + if(isturf(RB.loc) && !get_dist(RB.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RB.loc) + for(var/obj/structure/flora/newtree/RS in orange(max_dist, src)) + if(!RS.density) + continue + if(isturf(RS.loc) && !get_dist(RS.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RS.loc) - if(iscarbon(src)) - var/mob/living/carbon/C = src - var/heart_value = 30 - if(HAS_TRAIT(C, TRAIT_WEAK_HEART)) - heart_value *= 0.5 - if(C.stress >= heart_value && (prob(50))) - C.heart_attack() - if(mustype == 1) - playsound(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) - else - playsound(src, pick('sound/misc/jumphumans (1).ogg','sound/misc/jumphumans (2).ogg','sound/misc/jumphumans (3).ogg'), 100) - shake_camera(src, 2, 2) + return possible_targets -#undef AMBUSH_CHANCE +/proc/get_adjacent_ambush_turfs(turf/T) + var/list/adjacent = list() + for(var/turf/AT in get_adjacent_ambush_turfs(T)) + if(AT.density || T.LinkBlockedWithAccess(AT, null)) + continue + adjacent += AT + return adjacent diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm new file mode 100644 index 00000000000..a5fd2c01f7d --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm @@ -0,0 +1,263 @@ + +//After the bogfort fell to undead, the remaining guard who didn't flea turned to bandirty. Wellarmed and trained. +//These guys use alot of iron stuff with small amounts of steel mixed in, not really one for finetuned balance might be too hard or easy idk. Going off vibes atm +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_cloak(mob/living/carbon/human/H) + var/random_deserter_cloak = rand(1,4) + switch(random_deserter_cloak) + if(1) + cloak = /obj/item/clothing/cloak/stabard/mercenary + if(2) + cloak = /obj/item/clothing/cloak/stabard/colored/dungeon + if(3) + cloak = /obj/item/clothing/armor/brigandine/coatplates + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_weapon(mob/living/carbon/human/H) + var/random_deserter_weapon = rand(1,3) + switch(random_deserter_weapon) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/heater + if(2) + r_hand = /obj/item/weapon/polearm/spear + if(3) + r_hand = /obj/item/weapon/axe + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_weapon_hard(mob/living/carbon/human/H) + var/add_random_deserter_weapon_hard = rand(1,4) + switch(add_random_deserter_weapon_hard) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/heater + if(2) + r_hand = /obj/item/weapon/mace/warhammer + l_hand = /obj/item/weapon/shield/heater + if(3) + r_hand = /obj/item/weapon/axe + if(4) + r_hand = /obj/item/weapon/flail + l_hand = /obj/item/weapon/shield/heater + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_beltl_stuff(mob/living/carbon/human/H) + var/add_random_deserter_beltl_stuff = rand(1,7) + switch(add_random_deserter_beltl_stuff) + if(1) + beltl = /obj/item/storage/belt/pouch/food + if(2) + beltl = /obj/item/storage/belt/pouch/medicine + if(3) + beltl = /obj/item/storage/belt/pouch/coins/poor + if(4) + beltl = /obj/item/storage/belt/pouch/coins/mid + if(5) + beltl = /obj/item/reagent_containers/glass/bottle/waterskin + if(6) + beltl = /obj/item/reagent_containers/glass/bottle/healthpot + if(7) + beltl = /obj/item/weapon/scabbard/sword + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_beltr_stuff(mob/living/carbon/human/H) + var/add_random_deserter_beltr_stuff = rand(1,7) + switch(add_random_deserter_beltr_stuff) + if(1) + beltr = /obj/item/storage/belt/pouch/food + if(2) + beltr = /obj/item/storage/belt/pouch/medicine + if(3) + beltr = /obj/item/storage/belt/pouch/coins/poor + if(4) + beltr = /obj/item/storage/belt/pouch/coins/mid + if(5) + beltr = /obj/item/reagent_containers/glass/bottle/waterskin + if(6) + beltr = /obj/item/reagent_containers/glass/bottle/healthpot + if(7) + beltr = /obj/item/weapon/scabbard/sword + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_armor_hard(mob/living/carbon/human/H) + var/random_deserter_armor_hard = rand(1,3) + switch(random_deserter_armor_hard) + if(1) + armor = /obj/item/clothing/armor/brigandine/light + if(2) + armor = /obj/item/clothing/armor/cuirass/iron + if(3) + armor = /obj/item/clothing/armor/plate/fluted + +/mob/living/carbon/human/species/human/northern/bog_deserters + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + cmode = 1 + setparrytime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + d_intent = INTENT_PARRY + possible_mmb_intents = list(INTENT_BITE, INTENT_JUMP, INTENT_KICK) + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/bog_deserters/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/bog_deserters/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/bog_deserters/after_creation() + ..() + job = "Garrison Deserter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, TRAIT_GENERIC) //For when they're just kinda patrolling around/ambushes + equipOutfit(new /datum/outfit/job/human/northern/bog_deserters) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 50 // Big sellprice for these guys since they're deserters + +/datum/outfit/job/human/northern/bog_deserters/pre_equip(mob/living/carbon/human/H) + ..() + //Body Stuff + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + //skill Stuff + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) //NPCs do not get these skills unless a mind takes them over, hopefully in the future someone can fix + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 3, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_STEELHEARTED, TRAIT_GENERIC) + H.base_strength = rand(12,14) + H.base_speed = 11 + H.base_constitution = rand(11,13) + H.base_endurance = 13 + H.base_perception = 11 + H.base_intelligence = 10 + //Chest Gear + add_random_deserter_cloak(H) + shirt = /obj/item/clothing/armor/gambeson + armor = /obj/item/clothing/armor/chainmail/hauberk/iron + //Head Gear + neck = /obj/item/clothing/neck/coif + head = /obj/item/clothing/head/helmet/kettle/iron + //wrist Gear + gloves = /obj/item/clothing/gloves/chain/iron + wrists = /obj/item/clothing/wrists/bracers/iron + //Lower Gear + belt = /obj/item/storage/belt/leather + pants = /obj/item/clothing/pants/chainlegs/iron + shoes = /obj/item/clothing/shoes/boots/armor + //Weapons + add_random_deserter_weapon(H) + add_random_deserter_beltl_stuff(H) + add_random_deserter_beltr_stuff(H) + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear + faction = list("viking", "station") + ambushable = FALSE + cmode = 1 + setparrytime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + d_intent = INTENT_PARRY + possible_mmb_intents = list(INTENT_BITE, INTENT_JUMP, INTENT_KICK) + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/after_creation() + job = "Garrison Deserter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, TRAIT_GENERIC) //For when they're just kinda patrolling around/ambushes + equipOutfit(new /datum/outfit/job/human/northern/bog_deserters/better_gear) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 50 // Big sellprice for these guys since they're deserters + +/datum/outfit/job/human/northern/bog_deserters/better_gear/pre_equip(mob/living/carbon/human/H) + //Body Stuff + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + //skill Stuff + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) //NPCs do not get these skills unless a mind takes them over, hopefully in the future someone can fix + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 3, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_STEELHEARTED, TRAIT_GENERIC) + H.base_strength = rand(12,14) + H.base_speed = 11 + H.base_constitution = rand(11,13) + H.base_endurance = 13 + H.base_perception = 11 + H.base_intelligence = 10 + //Chest Gear + shirt = /obj/item/clothing/armor/chainmail/hauberk/iron + add_random_deserter_armor_hard(H) + add_random_deserter_cloak(H) + //Head Gear + neck = /obj/item/clothing/neck/chaincoif/iron + head = /obj/item/clothing/head/helmet/heavy/frog + //wrist Gear + gloves = /obj/item/clothing/gloves/plate/iron + wrists = /obj/item/clothing/wrists/bracers/iron + //Lower Gear + belt = /obj/item/storage/belt/leather + pants = /obj/item/clothing/pants/chainlegs/iron + shoes = /obj/item/clothing/shoes/boots/armor + //Weapons + add_random_deserter_weapon_hard(H) + add_random_deserter_beltl_stuff(H) + add_random_deserter_beltr_stuff(H) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm new file mode 100644 index 00000000000..00a2e99a3bb --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm @@ -0,0 +1,257 @@ +/* * + * Deranged Knight + * A miniboss for quest system, designed to be a high-level challenge for multiple players. + * Uses fuckoff gear that should not be looted - hence snowflake dismemberment code. + */ + +GLOBAL_LIST_INIT(matthios_aggro, world.file2list("strings/rt/matthiosaggrolines.txt")) +GLOBAL_LIST_INIT(zizo_aggro, world.file2list("strings/rt/zizoaggrolines.txt")) +GLOBAL_LIST_INIT(graggar_aggro, world.file2list("strings/rt/graggaraggrolines.txt")) +GLOBAL_LIST_INIT(hedgeknight_aggro, world.file2list("strings/rt/hedgeknightaggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/deranged_knight + ai_controller = /datum/ai_controller/human_npc + faction = list("dundead") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + var/preset = "matthios" + var/forced_preset = "" // If set, force a specific preset instead of randomizing. + +/mob/living/carbon/human/species/human/northern/deranged_knight/Initialize() + . = ..() + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + AddComponent(/datum/component/ai_aggro_system) + is_silent = TRUE + var/head = get_bodypart(BODY_ZONE_HEAD) + RegisterSignal(head, COMSIG_MOB_DISMEMBER, PROC_REF(handle_drop_limb)) + +/mob/living/carbon/human/species/human/northern/deranged_knight/Destroy() + var/head = get_bodypart(BODY_ZONE_HEAD) + if(head) + UnregisterSignal(head, COMSIG_MOB_DISMEMBER) + return ..() + +/// Snowflake DK behavior for decaps. Yes, they turn to dust prior to decaps. +/mob/living/carbon/human/species/human/northern/deranged_knight/proc/handle_drop_limb(obj/item/bodypart/bodypart, special) + if(!istype(bodypart, /obj/item/bodypart/head)) + return + + death(FALSE, TRUE) // No, you won't loot that tasty helmet. + return COMPONENT_CANCEL_DISMEMBER + +/mob/living/carbon/human/species/human/northern/deranged_knight/after_creation() + ..() + job = "Ascendant Knight" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_STUCKITEMS, TRAIT_GENERIC) + if(forced_preset) + preset = forced_preset + else + switch(rand(1, 4)) + if(1) + preset = "graggar" + if(2) + preset = "matthios" + if(3) + preset = "zizo" + if(4) + preset = "hedgeknight" + switch(preset) + if("graggar") + equipOutfit(new /datum/outfit/job/quest_miniboss/graggar) + if ("matthios") + equipOutfit(new /datum/outfit/job/quest_miniboss/matthios) + if ("zizo") + ADD_TRAIT(src, TRAIT_CABAL, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/quest_miniboss/zizo) + if ("hedgeknight") + if(prob(50)) + equipOutfit(new /datum/outfit/job/quest_miniboss/hedge_knight) + else + equipOutfit(new /datum/outfit/job/quest_miniboss/blacksteel) + // No special trait for hedgeknight, he's just a generic tough guy. + + gender = pick(MALE,FEMALE) + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/organ/ears/organ_ears = getorgan(/obj/item/organ/ears) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/countryponytailalt, + /datum/sprite_accessory/hair/head/stacy, + /datum/sprite_accessory/hair/head/kusanagi_alt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/dave, + /datum/sprite_accessory/hair/head/sabitsuki)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + + new_hair.accessory_colors = "#DDDDDD" + new_hair.hair_color = "#DDDDDD" + set_hair_color("#DDDDDD") + + head.add_bodypart_feature(new_hair) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#FFBF00" + organ_eyes.accessory_colors = "#FFBF00#FFBF00" + + if(organ_ears) + organ_ears.accessory_colors = "#5f5f70" + + skin_tone = "5f5f70" + + if(prob(1)) + real_name = "Taras Mura" + update_body() + + def_intent_change(INTENT_PARRY) + +/mob/living/carbon/human/species/human/northern/deranged_knight/death(gibbed, nocutscene) + if(preset == "matthios") + if(prob(95)) + say("Matthios, I have failed you...", forced = TRUE) + else + say("Matthios, is this true?!", forced = TRUE) + else if(preset == "zizo") + say("Zizo, forgive me!", forced = TRUE) + else if(preset == "graggar") + say("No more... Blood!") + emote("painscream") + . = ..() + if(!gibbed) + dust(FALSE, FALSE, TRUE) + +/datum/outfit/job/quest_miniboss/pre_equip(mob/living/carbon/human/H, visualsOnly) + . = ..() + H.base_strength = 15 + H.base_speed = 14 + H.base_constitution = 15 + H.base_endurance = 14 + H.base_perception = 12 + H.base_intelligence = 12 + H.base_fortune = 10 + + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +/datum/outfit/job/quest_miniboss/matthios/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/full/matthios + pants = /obj/item/clothing/pants/platelegs/matthios + shoes = /obj/item/clothing/shoes/boots/armor/matthios + wrists = /obj/item/clothing/wrists/bracers + gloves = /obj/item/clothing/gloves/plate/matthios + head = /obj/item/clothing/head/helmet/heavy/matthios + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/flail/peasantwarflail/matthios + mask = /obj/item/clothing/face/facemask/steel + +/datum/outfit/job/quest_miniboss/zizo/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/full/zizo + pants = /obj/item/clothing/pants/platelegs/zizo + shoes = /obj/item/clothing/shoes/boots/armor/zizo + wrists = /obj/item/clothing/wrists/bracers + gloves = /obj/item/clothing/gloves/plate/zizo + head = /obj/item/clothing/head/helmet/heavy/zizo + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long + mask = /obj/item/clothing/face/facemask/steel + +/datum/outfit/job/quest_miniboss/graggar/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/fluted/ornate + pants = /obj/item/clothing/pants/platelegs/graggar + shoes = /obj/item/clothing/shoes/boots/armor/graggar + gloves = /obj/item/clothing/gloves/plate/graggar + wrists = /obj/item/clothing/wrists/bracers + head = /obj/item/clothing/head/helmet/heavy/graggar + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/greataxe/steel/doublehead/graggar + mask = /obj/item/clothing/face/facemask/steel + wrists = /obj/item/clothing/wrists/bracers + cloak = /obj/item/clothing/cloak/graggar + +/datum/outfit/job/quest_miniboss/blacksteel/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/blkknight + pants = /obj/item/clothing/pants/platelegs/blk + shoes = /obj/item/clothing/shoes/boots/armor/blkknight + gloves = /obj/item/clothing/gloves/plate/blk + wrists = /obj/item/clothing/wrists/bracers + head = /obj/item/clothing/head/helmet/blacksteel + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long/greatsword + mask = /obj/item/clothing/face/facemask/steel + wrists = /obj/item/clothing/wrists/bracers + +/datum/outfit/job/quest_miniboss/hedge_knight/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/fluted + pants = /obj/item/clothing/pants/platelegs + shoes = /obj/item/clothing/shoes/boots/armor + gloves = /obj/item/clothing/gloves/plate + head = /obj/item/clothing/head/helmet/heavy/frog + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long/greatsword/gutsclaymore + mask = /obj/item/clothing/face/facemask/steel + belt = /obj/item/storage/belt/leather/steel + beltl = /obj/item/flashlight/flare/torch/lantern + beltr = /obj/item/weapon/sword/long + wrists = /obj/item/clothing/wrists/bracers + cloak = /obj/item/clothing/cloak/stabard/colored/dungeon + +/* + * Goon preset + * Intended to support knight, but should not have any special/overly expensive gear. +*/ + +/mob/living/carbon/human/species/human/northern/highwayman/dk_goon + faction = list("dundead") + +/mob/living/carbon/human/species/human/northern/deranged_knight/matthios + forced_preset = "matthios" + +/mob/living/carbon/human/species/human/northern/deranged_knight/zizo + forced_preset = "zizo" + +/mob/living/carbon/human/species/human/northern/deranged_knight/graggar + forced_preset = "graggar" + +/mob/living/carbon/human/species/human/northern/deranged_knight/hedgeknight + forced_preset = "hedgeknight" diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm new file mode 100644 index 00000000000..acfaa070633 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm @@ -0,0 +1,117 @@ +GLOBAL_LIST_INIT(drowraider_aggro, world.file2list("strings/rt/drowaggrolines.txt")) + +/mob/living/carbon/human/species/elf/dark/drowraider + ai_controller = /datum/ai_controller/human_npc + faction = list("drow") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + d_intent = INTENT_DODGE + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + +/mob/living/carbon/human/species/elf/dark/drowraider/ambush + wander = TRUE + +/mob/living/carbon/human/species/elf/dark/drowraider/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/elf/dark) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/elf/dark/drowraider/after_creation() + ..() + job = "Drow Raider" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DODGEEXPERT, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DUALWIELDER, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/elf/dark/drowraider) + if(prob(40)) + gender = MALE + else + gender = FEMALE + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/organ/ears/organ_ears = getorgan(/obj/item/organ/ears) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/countryponytailalt, + /datum/sprite_accessory/hair/head/stacy, + /datum/sprite_accessory/hair/head/kusanagi_alt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/dave, + /datum/sprite_accessory/hair/head/sabitsuki)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + + new_hair.accessory_colors = "#DDDDDD" + new_hair.hair_color = "#DDDDDD" + set_hair_color("#DDDDDD") + + head.add_bodypart_feature(new_hair) + head.sellprice = 40 + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#FFBF00" + organ_eyes.accessory_colors = "#FFBF00#FFBF00" + + if(organ_ears) + organ_ears.accessory_colors = "#5f5f70" + + skin_tone = "5f5f70" + + if(gender == FEMALE) + real_name = pick(world.file2list("strings/rt/names/elf/elfdf.txt")) + else + real_name = pick(world.file2list("strings/rt/names/elf/elfdm.txt")) + + faction += "spider_lowers" + + update_body() + +/datum/outfit/job/human/species/elf/dark/drowraider/pre_equip(mob/living/carbon/human/H) + shoes = /obj/item/clothing/shoes/boots/leather/advanced + pants = /obj/item/clothing/pants/trou/shadowpants + armor = /obj/item/clothing/armor/leather/jacket/silk_coat + shirt = /obj/item/clothing/shirt/shadowshirt + gloves = /obj/item/clothing/gloves/fingerless/shadowgloves + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + mask = /obj/item/clothing/face/facemask + neck = /obj/item/clothing/neck/coif + r_hand = /obj/item/weapon/whip + if(prob(45)) + r_hand = /obj/item/weapon/sword/sabre/stalker + l_hand = /obj/item/weapon/sword/sabre/stalker + else if(prob(15)) + r_hand = /obj/item/weapon/knife/dagger/steel/dirk + l_hand = /obj/item/weapon/knife/dagger/steel/dirk + + H.base_strength = 12 // 6 Points + H.base_speed = 13 // 3 points + H.base_constitution = 14 // 4 points + H.base_endurance = 12 // 2 points - 14 points spread. Equal to 1 more than a KC accounting for Statpack. + H.base_perception = 10 + H.base_intelligence = 10 + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm new file mode 100644 index 00000000000..c2a86ffa5c9 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm @@ -0,0 +1,95 @@ +GLOBAL_LIST_INIT(highwayman_aggro, world.file2list("strings/rt/highwaymanaggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/highwayman + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + d_intent = INTENT_PARRY + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/highwayman/ambush + wander = TRUE + + +/mob/living/carbon/human/species/human/northern/highwayman/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/highwayman/after_creation() + ..() + job = "Highwayman" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/highwayman) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 30 // 50% More than goblin + + +/datum/outfit/job/human/species/human/northern/highwayman/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + mask = /obj/item/clothing/face/shepherd/rag + armor = /obj/item/clothing/armor/leather + shirt = /obj/item/clothing/shirt/undershirt/colored/vagrant + if(prob(50)) + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + if(prob(50)) + head = /obj/item/clothing/head/helmet/leather + if(prob(30)) + head = /obj/item/clothing/head/helmet/leather/volfhelm + if(prob(50)) + neck = /obj/item/clothing/neck/coif + gloves = /obj/item/clothing/gloves/leather + H.base_strength = rand(12,14) //GENDER EQUALITY!! + H.base_speed = 11 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 13 + H.base_perception = 10 + H.base_intelligence = 10 + if(prob(50)) + r_hand = /obj/item/weapon/sword/short/iron + else + r_hand = /obj/item/weapon/mace/cudgel + if(prob(20)) + r_hand = /obj/item/weapon/sword/scimitar/falchion + if(prob(20)) + r_hand = /obj/item/weapon/pick + if(prob(25)) + l_hand = /obj/item/weapon/shield/wood + if(prob(10)) + l_hand = /obj/item/weapon/shield/tower/buckleriron + shoes = /obj/item/clothing/shoes/boots/leather + if(prob(30)) + neck = /obj/item/clothing/neck/leathercollar + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) // Trash mobs, untrained. + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm new file mode 100644 index 00000000000..768423fd9e3 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm @@ -0,0 +1,127 @@ +/mob/living/carbon/human/species/human/northern/militia //weak peasant infantry. Neutral but can be given factions for events. doesn't attack players. + ai_controller = /datum/ai_controller/human_npc + faction = list("neutral") + ambushable = FALSE + wander = TRUE + dodgetime = 30 + possible_rmb_intents = list() + var/is_silent = TRUE /// Determines whether or not we will scream our funny lines at people. + +/mob/living/carbon/human/species/human/northern/militia/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/militia/after_creation() + ..() + job = "Militia" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/militia) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + +/datum/outfit/job/human/species/human/northern/militia/pre_equip(mob/living/carbon/human/H) + if(H.faction && ("viking" in H.faction)) + cloak = /obj/item/clothing/cloak/stabard/mercenary + else + cloak = /obj/item/clothing/cloak/stabard/guard + wrists = /obj/item/clothing/wrists/bracers/leather + shirt = /obj/item/clothing/armor/gambeson/light + if(prob(50)) + shirt = /obj/item/clothing/armor/gambeson + if(prob(25)) + armor = /obj/item/clothing/armor/leather + pants = /obj/item/clothing/pants/trou/leather + if(prob(50)) + pants = /obj/item/clothing/pants/trou + // Helmet, or lackthereof + switch(rand(1, 7)) + if(1) + head = /obj/item/clothing/head/helmet/kettle/iron + if(2) + head = /obj/item/clothing/head/helmet/sallet/iron + if(3) + head = /obj/item/clothing/head/helmet/skullcap + if(4 to 6) + head = /obj/item/clothing/neck/coif + if(7) + head = null + // Neck protection, if there's no coif on head + if(prob(50)) + neck = /obj/item/clothing/neck/coif + gloves = /obj/item/clothing/gloves/fingerless + if(prob(25)) + gloves = /obj/item/clothing/gloves/angle + H.base_strength = rand(10,11) //GENDER EQUALITY!! + H.base_speed = 10 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 10 + H.base_perception = 10 + H.base_intelligence = 10 + switch(rand(1, 11)) + // Militia Weapon. Of course they spawn with it + if(1) + r_hand = /obj/item/weapon/polearm/woodstaff + if(2) + r_hand = /obj/item/weapon/greataxe + if(3) + r_hand = /obj/item/weapon/polearm/spear + if(4) + r_hand = /obj/item/weapon/polearm/spear + l_hand = /obj/item/weapon/shield/wood + if(5) + r_hand = /obj/item/weapon/sickle/scythe + if(6) + r_hand = /obj/item/weapon/pick + if(7) + r_hand = /obj/item/weapon/sword/scimitar/falchion + if(8) + r_hand = /obj/item/weapon/mace/cudgel + if(9) + r_hand = /obj/item/weapon/mace/goden + if(10) + r_hand = /obj/item/weapon/axe/iron + l_hand = /obj/item/weapon/shield/wood + if(11) + r_hand = /obj/item/weapon/flail/peasantwarflail + shoes = /obj/item/clothing/shoes/boots/leather + var/eye_color = pick("27becc", "35cc27", "000000") + H.set_eye_color(eye_color,eye_color) + H.set_hair_color(pick("4f4f4f", "61310f", "faf6b9")) + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(pick(/datum/sprite_accessory/hair/head/countryponytailalt, /datum/sprite_accessory/hair/head/barbarian, /datum/sprite_accessory/hair/head/messy)) + else + H.set_hair_style(pick(/datum/sprite_accessory/hair/head/majestic_human, /datum/sprite_accessory/hair/head/messy, /datum/sprite_accessory/hair/head/barbarian)) + H.set_facial_hair_style(pick(/datum/sprite_accessory/hair/facial/viking, /datum/sprite_accessory/hair/facial/pick, /datum/sprite_accessory/hair/facial/manly)) + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) // Trash mobs, untrained. + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +/mob/living/carbon/human/species/human/northern/militia/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/militia/guard //variant that doesn't wander, if you want to place them as set dressing. will aggro enemies and animals + wander = FALSE + +/mob/living/carbon/human/species/human/northern/militia/deserter // Bad deserter, trash mob + faction = list("viking", "station") + +/mob/living/carbon/human/species/human/northern/militia/after_creation() + ..() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 20 // Gobbo sellprice diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm new file mode 100644 index 00000000000..3eb56068cfa --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm @@ -0,0 +1,127 @@ +GLOBAL_LIST_INIT(searaider_aggro, world.file2list("strings/rt/searaideraggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/searaider + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/searaider/ambush + ambushable = TRUE + +/mob/living/carbon/human/species/human/northern/searaider/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/searaider/after_creation() + ..() + job = "Sea Raider" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/searaider) + gender = pick(MALE, FEMALE) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list(/datum/sprite_accessory/hair/head/barbarian, + /datum/sprite_accessory/hair/head/countryponytailalt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/barbarian)) + var/beard = pick(list(/datum/sprite_accessory/hair/facial/viking, + /datum/sprite_accessory/hair/facial/manly, + /datum/sprite_accessory/hair/facial/pick)) + head.sellprice = 30 // 50% More than gobbo + + var/datum/bodypart_feature/hair/head/new_hair = new() + var/datum/bodypart_feature/hair/facial/new_facial = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + new_facial.set_accessory_type(beard, null, src) + + if(prob(50)) + new_hair.accessory_colors = "#C1A287" + new_hair.hair_color = "#C1A287" + new_facial.accessory_colors = "#C1A287" + new_facial.hair_color = "#C1A287" + set_hair_color("#C1A287") + else + new_hair.accessory_colors = "#A56B3D" + new_hair.hair_color = "#A56B3D" + new_facial.accessory_colors = "#A56B3D" + new_facial.hair_color = "#A56B3D" + set_hair_color("#A56B3D") + + head.add_bodypart_feature(new_hair) + head.add_bodypart_feature(new_facial) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#336699" + organ_eyes.accessory_colors = "#336699#336699" + + if(gender == FEMALE) + real_name = pick(world.file2list("strings/rt/names/human/vikingf.txt")) + else + real_name = pick(world.file2list("strings/rt/names/human/vikingm.txt")) + update_body() + + +/datum/outfit/job/human/species/human/northern/searaider/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + armor = /obj/item/clothing/armor/chainmail/iron + shirt = /obj/item/clothing/shirt/undershirt/colored/vagrant + if(prob(50)) + shirt = /obj/item/clothing/shirt/tunic + pants = /obj/item/clothing/pants/tights + if(prob(50)) + pants = /obj/item/clothing/pants/chainlegs/iron + head = /obj/item/clothing/head/helmet/leather + if(prob(50)) + head = /obj/item/clothing/head/helmet/horned + if(prob(50)) + neck = /obj/item/clothing/neck/gorget + if(prob(50)) + gloves = /obj/item/clothing/gloves/leather + switch(rand(1, 4)) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/wood + if(2) + r_hand = /obj/item/weapon/polearm/spear + if(3) + r_hand = /obj/item/weapon/greataxe + if(4) + r_hand = /obj/item/weapon/sword/long/greatsword + + shoes = /obj/item/clothing/shoes/boots/leather + H.base_speed = 9 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 15 + H.base_perception = 10 + H.base_intelligence = 1 + H.base_strength = 14 + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm new file mode 100644 index 00000000000..19525f3cd0f --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm @@ -0,0 +1,108 @@ +/mob/living/carbon/human/species/human/northern/thief //I'm a thief, give me your shit + faction = list("thieves") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + m_intent = MOVE_INTENT_SNEAK + d_intent = INTENT_DODGE + ai_controller = /datum/ai_controller/human_npc + +/mob/living/carbon/human/species/human/northern/thief/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + +/mob/living/carbon/human/species/human/northern/thief/after_creation() + ..() + job = "Thief" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LIGHT_STEP, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DODGEEXPERT, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/thief) + gender = pick(MALE, FEMALE) + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/bob)) + var/hairm = pick(list( + /datum/sprite_accessory/hair/head/shaved)) + var/beard = pick(list(/datum/sprite_accessory/hair/facial/vandyke)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + var/datum/bodypart_feature/hair/facial/new_facial = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + new_facial.set_accessory_type(beard, null, src) + + if(prob(50)) + new_hair.accessory_colors = "#96403d" + new_hair.hair_color = "#96403d" + new_facial.accessory_colors = "#96403d" + new_facial.hair_color = "#96403d" + set_hair_color("#96403d") + else + new_hair.accessory_colors = "#C7C755" + new_hair.hair_color = "#C7C755" + new_facial.accessory_colors = "#C7C755" + new_facial.hair_color = "#C7C755" + set_hair_color("#C7C755") + + head.add_bodypart_feature(new_hair) + head.add_bodypart_feature(new_facial) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#336699" + organ_eyes.accessory_colors = "#336699#336699" + + if(gender == FEMALE) + real_name = pick(world.file2list("strings/names/first_female.txt")) + else + real_name = pick(world.file2list("strings/names/first_male.txt")) + update_body() + head.sellprice = 30 + +/datum/outfit/job/human/species/human/northern/thief/pre_equip(mob/living/carbon/human/H) + cloak = /obj/item/clothing/cloak/raincloak/colored/mortus + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + wrists = /obj/item/clothing/wrists/bracers/copper + armor = /obj/item/clothing/armor/cuirass/copperchest + if(prob(50)) + armor = /obj/item/clothing/armor/leather + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + head = /obj/item/clothing/head/helmet/leather + mask = /obj/item/clothing/face/skullmask + neck = /obj/item/clothing/neck/gorget/copper + if(prob(50)) + neck = /obj/item/clothing/neck/leathercollar + gloves = /obj/item/clothing/gloves/leather + shoes = /obj/item/clothing/shoes/boots/leather + l_hand = /obj/item/weapon/knife/dagger + if(prob(50)) + l_hand = /obj/item/weapon/knife/copper + H.base_strength = 11 + H.base_speed = 16 + H.base_constitution = 11 + H.base_endurance = 11 + H.base_perception = 11 + H.base_intelligence = 1 + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm new file mode 100644 index 00000000000..73f9c757160 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm @@ -0,0 +1,113 @@ +/* +* based on pages from elden ring in terms of visual design, these guys are intended to be a speedbump to solo adventurers at mount decap +* deadly but small in numbers. come back with a party, chump +*/ + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 15 + flee_in_pain = FALSE + possible_rmb_intents = list() + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/after_creation() + ..() + job = "Mad-touched Treasure Hunter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_DISFIGURED, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_CRITICAL_RESISTANCE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/mad_touched_treasure_hunter) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 40 + +/datum/outfit/job/human/species/human/northern/mad_touched_treasure_hunter/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers + mask = /obj/item/clothing/face/facemask/steel/mad_touched + armor = /obj/item/clothing/armor/leather/heavy + shirt = /obj/item/clothing/armor/gambeson + if(prob(20)) + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/platelegs + belt = /obj/item/storage/belt/leather + if(prob(33)) + beltl = /obj/item/reagent_containers/glass/bottle/healthpot + head = /obj/item/clothing/head/menacing/mad_touched_treasure_hunter + neck = /obj/item/clothing/neck/chaincoif + gloves = /obj/item/clothing/gloves/plate + cloak = /obj/item/clothing/cloak/wickercloak + if(prob(33)) + r_hand = /obj/item/weapon/sword/long/greatsword + else if(prob(33)) + r_hand = /obj/item/weapon/shield/tower/buckleriron + l_hand = /obj/item/weapon/knife/dagger/steel/dirk + else + r_hand = /obj/item/weapon/sword/sabre/hook + l_hand = /obj/item/weapon/sword/sabre/hook + + shoes = /obj/item/clothing/shoes/boots/leather + //carbon ai is still pretty dumb so making them a threat to players requires pretty crazy looking stats. don't think too hard about it. + H.base_strength = 15 + H.base_speed = 15 + H.base_constitution = 15 + H.base_endurance = 15 + H.base_perception = 15 + H.base_intelligence = 12 + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.real_name = pick(world.file2list("strings/rt/names/human/mad_touched_names.txt")) + +/obj/item/clothing/head/menacing/mad_touched_treasure_hunter //its here so it doesnt wind up on some class' loadout. + name = "sack hood" + desc = "A ragged hood of thick jute fibres. The itchiness is unbearable." + sewrepair = TRUE + color = "#999999" + armor = ARMOR_LEATHER + +/obj/item/clothing/face/facemask/steel/mad_touched + name = "eerie ancient mask" + +/obj/item/clothing/face/facemask/steel/mad_touched/equipped(mob/user, slot) + . = ..() + if(slot == ITEM_SLOT_MASK) + ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + var/mob/living/carbon/human/mad_touched = user + mad_touched.apply_damage(25, BRUTE, BODY_ZONE_HEAD) + +/obj/item/clothing/face/facemask/steel/mad_touched/dropped(mob/user) + . = ..() + REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) diff --git a/code/modules/mob/living/carbon/human/npc/orc.dm b/code/modules/mob/living/carbon/human/npc/orc/_orc.dm similarity index 98% rename from code/modules/mob/living/carbon/human/npc/orc.dm rename to code/modules/mob/living/carbon/human/npc/orc/_orc.dm index 2e45dcc5b4b..f7498bcf3b3 100644 --- a/code/modules/mob/living/carbon/human/npc/orc.dm +++ b/code/modules/mob/living/carbon/human/npc/orc/_orc.dm @@ -38,6 +38,7 @@ canparry = TRUE flee_in_pain = FALSE + var/orc_outfit wander = FALSE /mob/living/carbon/human/species/orc/npc/Initialize() @@ -45,6 +46,11 @@ AddComponent(/datum/component/ai_aggro_system) AddComponent(/datum/component/combat_noise, list("aggro" = 2)) +/mob/living/carbon/human/species/orc/npc/after_creation() + ..() + if(orc_outfit) + equipOutfit(new orc_outfit) + /mob/living/carbon/human/species/orc/ambush ai_controller = /datum/ai_controller/human_npc @@ -183,7 +189,7 @@ id = SPEC_ID_ORC species_traits = list(NO_UNDERWEAR) inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_RADIMMUNE,TRAIT_CRITICAL_WEAKNESS, TRAIT_NASTY_EATER, TRAIT_LEECHIMMUNE, TRAIT_INHUMENCAMP) - no_equip = list(ITEM_SLOT_SHIRT, ITEM_SLOT_MASK, ITEM_SLOT_GLOVES, ITEM_SLOT_SHOES, ITEM_SLOT_PANTS) + //no_equip = list(ITEM_SLOT_SHIRT, ITEM_SLOT_MASK, ITEM_SLOT_GLOVES, ITEM_SLOT_SHOES, ITEM_SLOT_PANTS) nojumpsuit = 1 sexes = 1 damage_overlay_type = "" diff --git a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm new file mode 100644 index 00000000000..1e316b20ba0 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm @@ -0,0 +1,179 @@ +/mob/living/carbon/human/species/orc/npc/footsoldier + orc_outfit = /datum/outfit/job/orc/npc/footsoldier + +/mob/living/carbon/human/species/orc/npc/marauder + orc_outfit = /datum/outfit/job/orc/npc/marauder + +/mob/living/carbon/human/species/orc/npc/berserker + orc_outfit = /datum/outfit/job/orc/npc/berserker + +/mob/living/carbon/human/species/orc/npc/warlord + orc_outfit = /datum/outfit/job/orc/npc/warlord + +/mob/living/carbon/human/species/orc/npc/archer_test + orc_outfit = /datum/outfit/job/orc/npc/archer_test + +// Underarmored orc with incomplete protection, bone axe / spear, and slow speed +/datum/outfit/job/orc/npc/footsoldier/pre_equip(mob/living/carbon/human/H) + name = "Orc Footsoldier" + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + armor = /obj/item/clothing/armor/leather/hide + else + armor = /obj/item/clothing/armor/leather + pants = /obj/item/clothing/pants/loincloth + if(prob(50)) + head = /obj/item/clothing/head/helmet/leather + shoes = /obj/item/clothing/shoes/gladiator + var/wepchoice = rand(1, 3) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/axe/boneaxe + if(2) + l_hand = /obj/item/weapon/polearm/spear/bonespear + r_hand = /obj/item/weapon/shield/wood // Help preserve integrity + if(3) + l_hand = /obj/item/weapon/mace/cudgel + H.base_strength = 11 + H.base_speed = 8 + H.base_constitution = 11 + H.base_endurance = 11 + H.base_intelligence = 4 // Very dumb + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + +// Slightly armored orc with slight facial protection, incomplete chainmail and spear / sword +/datum/outfit/job/orc/npc/marauder/pre_equip(mob/living/carbon/human/H) + name = "Orc Marauder" + wrists = /obj/item/clothing/wrists/bracers/leather + armor = /obj/item/clothing/armor/chainmail + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/chainlegs/iron + neck = /obj/item/clothing/neck/coif + head = /obj/item/clothing/head/helmet/leather + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 5) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/polearm/spear + if(2) + l_hand = /obj/item/weapon/sword/scimitar/falchion + r_hand = /obj/item/weapon/shield/wood // Help preserve integrity + if(3) + l_hand = /obj/item/weapon/mace // Threat to parry-er + if(4) + l_hand = /obj/item/weapon/greataxe + if(5) + l_hand = /obj/item/weapon/pick + H.base_strength = 12 // GAGGER GAGGER GAGGER + H.base_speed = 8 + H.base_constitution = 12 + H.base_endurance = 10 + H.base_intelligence = 4 + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/labor/mining, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + +// Lightly armored orc in light armor with no pain stun, and grappling oriented weapons +/datum/outfit/job/orc/npc/berserker/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + armor = /obj/item/clothing/armor/leather/hide + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + head = /obj/item/clothing/head/helmet/leather + neck = /obj/item/clothing/neck/coif + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 2) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/knife/dagger + if(2) + l_hand = /obj/item/weapon/pick + H.base_strength = 13 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 // Minmax department + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/labor/mining, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_CRITICAL_RESISTANCE, INNATE_TRAIT) + +// Heavily armored orc with complete iron protection, heavy armor, and a two hander. +/datum/outfit/job/orc/npc/warlord/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + armor = /obj/item/clothing/armor/chainmail + shirt = /obj/item/clothing/armor/gambeson + pants = /obj/item/clothing/pants/chainlegs/iron + head = /obj/item/clothing/head/helmet/skullcap + neck = /obj/item/clothing/neck/chaincoif/iron + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 6) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/polearm/halberd/bardiche + if(2) + l_hand = /obj/item/weapon/polearm/halberd + if(3) + l_hand = /obj/item/weapon/greataxe + if(4) + l_hand = /obj/item/weapon/polearm/eaglebeak/lucerne + if(5) + l_hand = /obj/item/weapon/mace/goden + if(6) + l_hand = /obj/item/weapon/sword/scimitar/falchion + r_hand = /obj/item/weapon/sword/scimitar/falchion // intrusive thoughts + H.base_strength = 14 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + + +// Heavily armored orc with complete iron protection, heavy armor, and a two hander. +/datum/outfit/job/orc/npc/archer_test/pre_equip(mob/living/carbon/human/H) + backr = /obj/item/gun/ballistic/revolver/grenadelauncher/bow + beltr = /obj/item/ammo_holder/quiver/arrows + l_hand = /obj/item/weapon/sword/short/iron + armor = /obj/item/clothing/armor/leather/hide + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + belt = /obj/item/storage/belt/leather + H.base_strength = 14 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 diff --git a/code/modules/mob/living/carbon/human/npc/skeleton.dm b/code/modules/mob/living/carbon/human/npc/skeleton/_skeleton.dm similarity index 100% rename from code/modules/mob/living/carbon/human/npc/skeleton.dm rename to code/modules/mob/living/carbon/human/npc/skeleton/_skeleton.dm diff --git a/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm new file mode 100644 index 00000000000..25aa49e20b5 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm @@ -0,0 +1,233 @@ +// Ultra easy tier skeleton with no armor and just a single weapon. +/mob/living/carbon/human/species/skeleton/npc/supereasy + skel_outfit = /datum/outfit/job/skeleton/npc/supereasy + + +/datum/outfit/job/skeleton/npc/supereasy/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 10 + H.base_speed = 8 + H.base_constitution = 4 + H.base_endurance = 10 + H.base_intelligence = 1 + name = "Skeleton" + if(prob(50)) + shirt = /obj/item/clothing/shirt/rags + else + shirt = /obj/item/clothing/shirt/tunic/colored/random + if(prob(50)) + pants = /obj/item/clothing/pants/tights/colored/random + else + pants = /obj/item/clothing/pants/loincloth + var/weapon_choice = rand(1, 4) + switch(weapon_choice) + if(1) + r_hand = /obj/item/weapon/axe/iron + if(2) + r_hand = /obj/item/weapon/sword/short/iron + if(3) + r_hand = /obj/item/weapon/polearm/spear/bonespear + if(4) + r_hand = /obj/item/weapon/mace + + H.adjust_skillrank(/datum/skill/combat/polearms, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 1, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Easy tier skeleton, with only incomplete chainmail and kilt +// Ambushes people in "safe" route. A replacement for old skeletons that were effectively naked. +/mob/living/carbon/human/species/skeleton/npc/easy + skel_outfit = /datum/outfit/job/skeleton/npc/easy + + +/datum/outfit/job/skeleton/npc/easy/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 9 + H.base_speed = 8 + H.base_constitution = 4 // Same statblock as before easily killed + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Footsoldier" + shirt = /obj/item/clothing/armor/chainmail + pants = /obj/item/clothing/pants/chainlegs/kilt + shoes = /obj/item/clothing/shoes/boots/armor/light + var/weapon_choice = rand(1, 4) + switch(weapon_choice) + if(1) + r_hand = /obj/item/weapon/axe/iron + if(2) + r_hand = /obj/item/weapon/sword/short/iron + if(3) + r_hand = /obj/item/weapon/polearm/spear/bonespear + if(4) + r_hand = /obj/item/weapon/mace + + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Also an "easy" tier skeleton, pirate themed, with a free hand to grab you +/mob/living/carbon/human/species/skeleton/npc/pirate + skel_outfit = /datum/outfit/job/skeleton/npc/pirate + +/datum/outfit/job/skeleton/npc/pirate/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 9 + H.base_speed = 8 + H.base_constitution = 4 // Same statblock as before easily killed + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Pirate" + head = /obj/item/clothing/head/helmet/leather/tricorn + wrists = /obj/item/clothing/wrists/bracers/ancient + shirt = /obj/item/clothing/armor/chainmail/iron + pants = /obj/item/clothing/pants/tights/sailor + shoes = /obj/item/clothing/shoes/boots/armor/light + if(prob(50)) + r_hand = /obj/item/weapon/knife/dagger + else + r_hand = /obj/item/weapon/knuckles + + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Medium tier skeleton, 3 skills. +/mob/living/carbon/human/species/skeleton/npc/medium + skel_outfit = /datum/outfit/job/skeleton/npc/medium + + +/datum/outfit/job/skeleton/npc/medium/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 11 + H.base_speed = 8 + H.base_constitution = 6 // Slightly tougher now! + H.base_endurance = 10 + H.base_intelligence = 1 + name = "Skeleton Soldier" + cloak = /obj/item/clothing/cloak/heartfelt // Ooo Spooky Old Dead MAA + head = /obj/item/clothing/head/helmet/heavy/ancient + armor = /obj/item/clothing/armor/cuirass/copperchest + shirt = /obj/item/clothing/armor/chainmail/iron + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/chainlegs/kilt/iron + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/chaincoif/iron + gloves = /obj/item/clothing/gloves/chain + belt = /obj/item/storage/belt/leather/rope + if(prob(33)) // 33% chance of shield, so ranged don't get screwed over entirely + l_hand = /obj/item/weapon/shield/tower/metal/ancient + if(prob(33)) + r_hand = /obj/item/weapon/polearm/spear/bonespear + else if(prob(33)) + r_hand = /obj/item/weapon/sword/gladius + else + r_hand = /obj/item/weapon/flail + + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 3, TRUE) + +// High tier skeleton, 4 skills. Heavy Armor. +/mob/living/carbon/human/species/skeleton/npc/hard + skel_outfit = /datum/outfit/job/skeleton/npc/hard + +/datum/outfit/job/skeleton/npc/hard/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 12 + H.base_constitution = 8 // Woe, actual limb health. + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Dreadnought" + // This combines the khopesh and withered dreadknight + var/skeletonclass = rand(1, 2) + if(skeletonclass == 1) // Khopesh Knight + H.base_speed = 12 // Hue + cloak = /obj/item/clothing/cloak/heartfelt + mask = /obj/item/clothing/face/facemask/copper + armor = /obj/item/clothing/armor/cuirass/copperchest + shirt = /obj/item/clothing/armor/chainmail/iron + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/platelegs/iron + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/psycross/zizo + gloves = /obj/item/clothing/gloves/chain/iron + r_hand = /obj/item/weapon/sword/sabre/cutlass + l_hand = /obj/item/weapon/sword/sabre/cutlass + else // Withered Dreadknight + H.base_speed = 8 + cloak = /obj/item/clothing/cloak/tabard/blkknight + head = /obj/item/clothing/head/helmet/heavy/ironplate + armor = /obj/item/clothing/armor/plate/ancient + shirt = /obj/item/clothing/armor/chainmail/hauberk/fluted + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/platelegs/ancient + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/gorget/ancient + gloves = /obj/item/clothing/gloves/plate/ancient + belt = /obj/item/storage/belt/leather + if(prob(50)) + r_hand = /obj/item/weapon/sword/long/greatsword + else + r_hand = /obj/item/weapon/mace/goden + + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 4, TRUE) + +// For Duke Manor & Zizo Manor - Ground based spread, so no pirate in pool! +/mob/living/carbon/human/species/skeleton/npc/mediumspread/Initialize() + var/outfit = rand(1, 4) + switch(outfit) + if(1) + skel_outfit = /datum/outfit/job/skeleton/npc/supereasy + if(2) + skel_outfit = /datum/outfit/job/skeleton/npc/easy + if(3) + skel_outfit = /datum/outfit/job/skeleton/npc/medium + if(4) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + ..() + +/mob/living/carbon/human/species/skeleton/npc/hardspread/Initialize() + var/outfit = rand(1,4) + switch(outfit) + if(1) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + if(2) + skel_outfit = /datum/outfit/job/skeleton/npc/medium + if(3) + skel_outfit = /datum/outfit/job/skeleton/npc/pirate + if(4) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + ..() diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm new file mode 100644 index 00000000000..e2c8f16d6b3 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm @@ -0,0 +1,393 @@ +/mob/living/simple_animal/hostile/retaliate/mirespider + icon = 'icons/mob/mirespider_small.dmi' + desc = "Said to have originated from the decapitated heads of fallen legionnaires from eons past, grown legs and a voracious appetite, mire crawlers are common pests in many a wetland. Occasionally hunted for their silk." + name = "mire crawler" + icon_state = "crawler" + icon_living = "crawler" + icon_dead = "crawler_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_hear = null + emote_see = null + speak_chance = 1 + see_in_dark = 10 + move_to_delay = 3 + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 2, + /obj/item/alch/viscera = 1) + + health = 80 + maxHealth = 80 + melee_damage_lower = 17 + melee_damage_upper = 26 + vision_range = 10 + aggro_vision_range = 9 + environment_smash = ENVIRONMENT_SMASH_NONE + retreat_distance = 0 + minimum_distance = 0 + + base_constitution = 7 + base_strength = 7 + base_speed = 13 + footstep_type = FOOTSTEP_MOB_BAREFOOT + defprob = 40 + retreat_health = 0 + ai_controller = /datum/ai_controller/mirespider + +/mob/living/simple_animal/hostile/retaliate/mirespider/Initialize() + . = ..() + update_icon() + AddElement(/datum/element/ai_retaliate) + AddComponent(/datum/component/ai_aggro_system) + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, INNATE_TRAIT) + + addtimer(CALLBACK(src, PROC_REF(find_lurker_to_follow)), 10) + +/mob/living/simple_animal/hostile/retaliate/mirespider/proc/find_lurker_to_follow() + var/mob/living/simple_animal/hostile/mirespider_lurker/lurker = null + for(var/mob/living/simple_animal/hostile/mirespider_lurker/L in view(10, src)) + { + lurker = L + break + } + + if(lurker && ai_controller) + ai_controller.set_blackboard_key(BB_FOLLOW_TARGET, lurker) + +/mob/living/simple_animal/hostile/retaliate/mirespider/death(gibbed) + ..() + update_icon() + +/datum/intent/simple/bite/mirespider + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/retaliate/mirespider/get_sound(input) + switch(input) + if("aggro") + return pick('sound/vo/mobs/spider/aggro (1).ogg','sound/vo/mobs/spider/aggro (2).ogg','sound/vo/mobs/spider/aggro (3).ogg') + if("pain") + return pick('sound/vo/mobs/spider/pain.ogg') + if("death") + return pick('sound/vo/mobs/spider/death.ogg') + if("idle") + return pick('sound/vo/mobs/spider/idle (1).ogg','sound/vo/mobs/spider/idle (2).ogg','sound/vo/mobs/spider/idle (3).ogg','sound/vo/mobs/spider/idle (4).ogg') + +/mob/living/simple_animal/hostile/retaliate/mirespider/taunted(mob/user) + emote("aggro") + return + +/mob/living/simple_animal/hostile/retaliate/mirespider/simple_limb_hit(zone) + if(!zone) + return "" + switch(zone) + if(BODY_ZONE_PRECISE_R_EYE) + return "head" + if(BODY_ZONE_PRECISE_L_EYE) + return "head" + if(BODY_ZONE_PRECISE_NOSE) + return "nose" + if(BODY_ZONE_PRECISE_MOUTH) + return "mouth" + if(BODY_ZONE_PRECISE_SKULL) + return "head" + if(BODY_ZONE_PRECISE_EARS) + return "head" + if(BODY_ZONE_PRECISE_NECK) + return "neck" + if(BODY_ZONE_PRECISE_L_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_R_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_L_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_R_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_STOMACH) + return "stomach" + if(BODY_ZONE_PRECISE_GROIN) + return "tail" + if(BODY_ZONE_HEAD) + return "head" + if(BODY_ZONE_R_LEG) + return "leg" + if(BODY_ZONE_L_LEG) + return "leg" + if(BODY_ZONE_R_ARM) + return "foreleg" + if(BODY_ZONE_L_ARM) + return "foreleg" + return ..() + +/mob/living/simple_animal/hostile/mirespider_lurker + icon = 'icons/mob/mirespider_big.dmi' + desc = "An unusually large and dangerous mire crawler, these lumbering creatures tend to find smaller specimens gravitating to them for safety - or perhaps simply to hunt more efficiently." + name = "mire lurker" + icon_state = "lurker" + icon_living = "lurker" + icon_dead = "lurker_dead" + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider_lurker) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 2, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 3, + /obj/item/natural/silk = 3, + /obj/item/alch/viscera = 4) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 4, + /obj/item/natural/silk = 5, // You killed the mire lurker. You get all the figgy pudding . . . + /obj/item/alch/viscera = 4) + + health = 200 + maxHealth = 200 + melee_damage_lower = 35 + melee_damage_upper = 70 + + base_constitution = 9 + base_strength = 9 + base_speed = 14 + // These things will crit. Slow attacks, devestating consequences. + base_perception = 15 + pixel_x = -4 + + ai_controller = /datum/ai_controller/mirespider_lurker + projectiletype = /obj/projectile/bullet/spider + + ranged = 1 + minimum_distance = 1 + ranged_cooldown_time = 100 + var/list/mob/living/simple_animal/hostile/retaliate/mirespider/followers = list() + +/mob/living/simple_animal/hostile/mirespider_lurker/mushroom + icon = 'icons/mob/mirespider_shroom.dmi' + desc = "While recognizable as a mire lurker, this specimen appears to suffer a gigantic \ + fungal growth over its rear end. It reeks of the smell of mold, and tar-like secretions \ + drip from its mandibles. Something here is horribly wrong." + name = "mire lurker?" + icon_state = "mushroom" + icon_living = "mushroom" + icon_dead = "mushroom_dead" + health = 400 + maxHealth = 400 + pixel_x = -8 + + projectiletype = /obj/projectile/bullet/spider_shroom + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 2, + /obj/item/natural/silk = 1, + /obj/item/reagent_containers/powder/ozium = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 3, + /obj/item/natural/silk = 3, + /obj/item/reagent_containers/powder/ozium = 2, + /obj/item/alch/viscera = 4) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 4, + /obj/item/natural/silk = 5, // You killed the mire lurker. You get all the figgy pudding . . . + /obj/item/reagent_containers/powder/ozium = 2, + /obj/item/reagent_containers/powder/moondust = 1, + /obj/item/alch/viscera = 4) + +/mob/living/simple_animal/hostile/mirespider_lurker/Initialize() + . = ..() + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, INNATE_TRAIT) + AddComponent(/datum/component/ai_aggro_system) + // I'll replace this with something better later. Stopgap for now to make killing them more than just a nuisance. + +/mob/living/simple_animal/hostile/mirespider_lurker/death(gibbed) + ..() + if(prob(40)) + new /obj/item/reagent_containers/food/snacks/spiderhoney(loc) + if(prob(10)) + new /obj/item/gem/violet(loc) + update_icon() + +/datum/intent/simple/bite/mirespider_lurker + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/mirespider_lurker/proc/add_follower(mob/living/simple_animal/hostile/retaliate/mirespider) + if (!(mirespider in followers)) + followers += mirespider + +/mob/living/simple_animal/hostile/mirespider_lurker/proc/clear_followers_if_any() + if (!followers || !length(followers)) + return + + for (var/mob/living/simple_animal/hostile/retaliate/mirespider/follower in followers) + follower.ai_controller.clear_blackboard_key(BB_FOLLOW_TARGET) + follower.ai_controller.clear_blackboard_key(BB_TRAVEL_DESTINATION) + follower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + follower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST) + follower.ai_controller.CancelActions() + followers.Cut() + +/mob/living/simple_animal/hostile/mirespider_paralytic + icon = 'icons/mob/mirespider_small.dmi' + name = "aragn" + desc = "A gigantic species of spider accompanied always by a strong sulphuric stench. Its fangs carry \ + a dangerous paralytic; a danger for the common traveller, and an opportunity to any aspiring poisoner." + icon_state = "aragn" + icon_living = "aragn" + icon_dead = "aragn_dead" + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider_paralytic) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1, + /obj/item/reagent_containers/spidervenom_inert = 1) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1, + /obj/item/reagent_containers/spidervenom_inert = 2) + + health = 140 + maxHealth = 140 + melee_damage_lower = 21 + melee_damage_upper = 42 + + base_constitution = 9 + base_strength = 9 + base_speed = 12 + base_perception = 7 + + ai_controller = /datum/ai_controller/mirespider_paralytic + +/mob/living/simple_animal/hostile/mirespider_paralytic/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + +/datum/intent/simple/bite/mirespider_paralytic + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/mirespider_paralytic/AttackingTarget() + . = ..() + if(. && isliving(target)) + var/mob/living/L = target + if(L.reagents && prob(50)) + L.reagents.add_reagent(/datum/reagent/toxin/spidervenom_paralytic, 5) + +/obj/random/spider + icon = 'icons/mob/mirespider_small.dmi' + name = "random spider spawner" + desc = "YOU SHOULD NOT BE SEEING THIS, GO YELL AT KETRAI." + icon_state = "crawler" + +/obj/random/spider/Initialize() + . = ..() + spawn_random_spider_at(loc) + qdel(src) + +/obj/random/spider/proc/spawn_random_spider_at(turf/T) + var/newspider = list(/mob/living/simple_animal/hostile/mirespider_paralytic = 10, /mob/living/simple_animal/hostile/retaliate/mirespider = 90) + var/spider_to_spawn = pickweight(newspider) + new spider_to_spawn(get_turf(src)) + qdel(src) + +/mob/living/simple_animal/hostile/retaliate/mirespider/angry + faction = list("mad", "zombie") + +/mob/living/simple_animal/hostile/mirespider_paralytic/angry + faction = list("mad", "zombie") + +/mob/living/simple_animal/hostile/mirespider_lurker/angry + faction = list("mad", "zombie") + +/obj/projectile/bullet/spider + name = "web glob" + damage = 10 + damage_type = BRUTE + icon = 'icons/obj/webbing.dmi' + icon_state = "webglob" + range = 15 + hitsound = 'sound/combat/hits/hi_arrow2.ogg' + embedchance = 0 + //Will not cause wounds. + woundclass = null + flag = "piercing" + speed = 2 // I guess slower to be slightly more forgiving to players since they're otherwise aimbots + +/obj/projectile/bullet/spider_shroom + name = "web glob" + damage = 10 + damage_type = BRUTE + icon = 'icons/obj/webbing.dmi' + icon_state = "webglob" + range = 15 + hitsound = 'sound/combat/hits/hi_arrow2.ogg' + embedchance = 0 + //Will not cause wounds. + woundclass = null + flag = "piercing" + speed = 2 + +/obj/projectile/bullet/spider/on_hit(target) + . = ..() + if(ismob(target)) + var/mob/living/M = target + //M.apply_status_effect(/datum/status_effect/debuff/vulnerable) + M.Immobilize(15) + var/turf/T + if(isturf(target)) + T = target + else + T = get_turf(target) + var/web = locate(/obj/structure/spider/stickyweb/mirespider) in T.contents + if(!(web in T.contents)) + new /obj/structure/spider/stickyweb/mirespider(T) + +/obj/projectile/bullet/spider_shroom/on_hit(target) + . = ..() + if(ismob(target)) + var/mob/living/M = target + //M.apply_status_effect(/datum/status_effect/debuff/vulnerable) + M.apply_status_effect(/datum/status_effect/buff/druqks) + var/turf/T + if(isturf(target)) + T = target + else + T = get_turf(target) + var/web = locate(/obj/structure/spider/stickyweb/mirespider) in T.contents + if(!(web in T.contents)) + new /obj/structure/spider/stickyweb/mirespider(T) + +//Mirespider webs are thinner and will not stop projectiles or obstruct movement as often. +/obj/structure/spider/stickyweb/mirespider + opacity = 0 + pass_flags = LETPASSTHROW + debris = null + +/obj/structure/spider/stickyweb/mirespider/CanPass(atom/movable/mover, turf/target) + . = ..() + if(isliving(mover)) + if(prob(25) && !HAS_TRAIT(mover, TRAIT_WEBWALK)) + to_chat(mover, "I get stuck in \the [src] for a moment.") + return FALSE + else if(istype(mover, /obj/projectile)) + return prob(85) + return TRUE diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 4680d4edd80..6c9a5983c2f 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -812,7 +812,7 @@ xo = targloc.x - curloc.x setAngle(get_angle(src, targloc) + spread) - if(isliving(source) && modifiers) + if(isliving(source) && length(modifiers)) var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, modifiers) p_x = calculated[2] p_y = calculated[3] diff --git a/code/modules/questing/contract_machine.dm b/code/modules/questing/contract_machine.dm new file mode 100644 index 00000000000..b51266d26be --- /dev/null +++ b/code/modules/questing/contract_machine.dm @@ -0,0 +1,376 @@ +/obj/structure/fake_machine/contractledger + name = "Grand Contract Ledger" + desc = "A massive ledger book with gilded edges, sitting atop a pedestal with the Mercenary's Guild banner. Its myriad enchanted pages are filled with various contracts and bounties issued by Mercenary's Guild, with arcane scripts that appears and fades as contracts are issued and completed." + icon = 'icons/obj/questing.dmi' + icon_state = "contractledger" + density = TRUE + anchored = TRUE + max_integrity = 0 + layer = ABOVE_MOB_LAYER + layer = GAME_PLANE_UPPER + var/input_point + +/obj/structure/fake_machine/contractledger/Initialize() + . = ..() + input_point = locate(x, y - 1, z) + var/obj/effect/decal/marker_export/marker = new(get_turf(input_point)) + marker.desc = "Place completed contract scrolls here to turn them in." + marker.layer = ABOVE_OBJ_LAYER + +/obj/structure/fake_machine/contractledger/attackby(obj/item/P, mob/living/carbon/human/user, params) + . = .. () + if(istype(P, /obj/item/paper/scroll/quest)) + turn_in_contract(user, P) + return + return + +/obj/structure/fake_machine/contractledger/Topic(href, href_list) + . = ..() + if(href_list["consultcontracts"]) + consult_contracts(usr) + return attack_hand(usr) + if(href_list["turnincontract"]) + turn_in_contract(usr) + return attack_hand(usr) + if(href_list["abandoncontract"]) + abandon_contract(usr) + return attack_hand(usr) + if(href_list["printcontracts"]) + print_contracts(usr) + return attack_hand(usr) + return attack_hand(usr) + +/obj/structure/fake_machine/contractledger/attack_hand(mob/living/carbon/human/user) + if(!ishuman(user)) + return + // Inshallah I'll make this TGUI one day. + var/contents = "

Grand Contract Ledger

" + contents += "Consult Contracts
" + contents += "Turn in Contract
" + contents += "Abandon Contract
" + var/datum/job/mob_job = user.job ? SSjob.GetJob(user.job) : null + if(mob_job?.is_quest_giver) + contents += "Print Issued Contracts
" + contents += "
" + var/datum/browser/popup = new(user, "Grand Contract Ledger", "", 500, 300) + popup.set_content(contents) + popup.open() + +/obj/structure/fake_machine/contractledger/proc/consult_contracts(mob/user) + if(!(user in SStreasury.bank_accounts)) + say("You have no bank account.") + return + + var/list/difficulty_data = list( + QUEST_DIFFICULTY_EASY = list(deposit = QUEST_DEPOSIT_EASY), + QUEST_DIFFICULTY_MEDIUM = list(deposit = QUEST_DEPOSIT_MEDIUM), + QUEST_DIFFICULTY_HARD = list(deposit = QUEST_DEPOSIT_HARD) + ) + + // Create a list with formatted difficulty choices showing deposits + var/list/difficulty_choices = list() + for(var/difficulty in difficulty_data) + var/deposit = difficulty_data[difficulty]["deposit"] + difficulty_choices["[difficulty] ([deposit] mammon deposit)"] = difficulty + + var/selection = tgui_input_list(user, "Select contract difficulty (deposit required)", "CONTRACTS", difficulty_choices) + if(!selection) + return + + // Get the actual difficulty key from our formatted choice + var/actual_difficulty = difficulty_choices[selection] + var/deposit = difficulty_data[actual_difficulty]["deposit"] + + if(SStreasury.bank_accounts[user] < deposit) + say("Insufficient balance funds. You need [deposit] mammons in your meister.") + return + + var/type_choices = GLOB.global_quest_types + + var/type_selection = tgui_input_list(user, "Select contract type", "CONTRACTS", type_choices[actual_difficulty]) + + if(!type_selection) + return + + var/datum/job/mob_job = user.job ? SSjob.GetJob(user.job) : null + if(!mob_job?.is_quest_giver) + var/quest_number = 0 + for(var/obj/item/paper/scroll/quest/quest_scroll in GLOB.quest_scrolls) + var/datum/weakref/weakref_datum = WEAKREF(user) + if(quest_scroll.assigned_quest && !quest_scroll.assigned_quest.complete && quest_scroll.assigned_quest.quest_receiver_reference == weakref_datum) + quest_number++ + var/max_quests_for_job = mob_job?.max_active_quests || 3 + if(quest_number >= max_quests_for_job) + say("You have reached the maximum number of active quests. You can take up to [max_quests_for_job] active quests at a time.") + return + + // Instantiate appropriate quest subtype + var/datum/quest/attached_quest + switch(type_selection) + if(QUEST_RETRIEVAL) + attached_quest = new /datum/quest/retrieval() + if(QUEST_KILL_EASY) + attached_quest = new /datum/quest/kill/easy() + if(QUEST_COURIER) + attached_quest = new /datum/quest/courier() + if(QUEST_CLEAR_OUT) + attached_quest = new /datum/quest/kill/clearout() + if(QUEST_RAID) + attached_quest = new /datum/quest/kill/raid() + if(QUEST_OUTLAW) + attached_quest = new /datum/quest/kill/outlaw() + + if(!attached_quest) + to_chat(user, span_warning("Invalid quest type selected!")) + return + + // Configure quest + attached_quest.quest_difficulty = actual_difficulty + attached_quest.deposit_amount = attached_quest.calculate_deposit() + + // Set giver or receiver + if(mob_job?.is_quest_giver) + attached_quest.quest_giver_name = user.real_name + attached_quest.quest_giver_reference = WEAKREF(user) + else + attached_quest.quest_receiver_reference = WEAKREF(user) + attached_quest.quest_receiver_name = user.real_name + + // Find appropriate landmark + var/obj/effect/landmark/quest_spawner/chosen_landmark = find_quest_landmark(actual_difficulty, type_selection) + if(!chosen_landmark) + to_chat(user, span_warning("No suitable location found for this contract!")) + qdel(attached_quest) + return + + // Generate quest content (spawns mobs/items) + if(!attached_quest.generate(chosen_landmark)) + to_chat(user, span_warning("Failed to generate quest content!")) + qdel(attached_quest) + return + + // Create scroll + var/obj/item/paper/scroll/quest/spawned_scroll = new(get_turf(src)) + user.put_in_hands(spawned_scroll) + log_quest(user.ckey, user.mind, user, "Take [attached_quest.quest_type]") + spawned_scroll.base_icon_state = attached_quest.get_scroll_icon() + spawned_scroll.assigned_quest = attached_quest + attached_quest.quest_scroll = spawned_scroll + attached_quest.quest_scroll_ref = WEAKREF(spawned_scroll) + + // Reward calculation comes after generation & scroll creation to factor in distance for courier quests + attached_quest.reward_amount = attached_quest.calculate_reward(get_turf(chosen_landmark)) + + // Update scroll text + spawned_scroll.update_quest_text() + + // Charge deposit + SStreasury.bank_accounts[user] -= deposit + SStreasury.treasury_value += deposit + SStreasury.log_entries += "+[deposit] to treasury (quest deposit)" + +/obj/structure/fake_machine/contractledger/proc/find_quest_landmark(difficulty, type) + // First try to find landmarks that match both difficulty AND type + var/list/correctest_landmarks = list() + GLOB.quest_landmarks_list = shuffle(GLOB.quest_landmarks_list) + for(var/obj/effect/landmark/quest_spawner/landmark in GLOB.quest_landmarks_list) + if(landmark.quest_difficulty != difficulty || !(type in landmark.quest_type)) + continue + + var/has_clients_around = FALSE + for(var/mob/M in get_hearers_in_view(world.view, landmark)) + if(!M.client) + continue + + has_clients_around = TRUE + + if(has_clients_around) + continue + + correctest_landmarks += landmark + + if(length(correctest_landmarks)) + return pick(correctest_landmarks) + + // If none found, try landmarks that match just the difficulty + var/list/correcter_landmarks = list() + for(var/obj/effect/landmark/quest_spawner/landmark in GLOB.quest_landmarks_list) + if(landmark.quest_difficulty != difficulty) + continue + + var/has_clients_around = FALSE + for(var/mob/M in get_hearers_in_view(world.view, landmark)) + if(!M.client) + continue + + has_clients_around = TRUE + + if(has_clients_around) + continue + + correcter_landmarks += landmark + + if(length(correcter_landmarks)) + return pick(correcter_landmarks) + + return null + +/obj/structure/fake_machine/contractledger/proc/turn_in_contract(mob/user, obj/item/paper/scroll/quest/scroll_in_hand) + if(scroll_in_hand) + var/list/mob/quest_assignees = scroll_in_hand.get_quest_assignees(user, TRUE) + if(!(user in quest_assignees)) + to_chat(user, span_warning("You are not the assigned quest receiver for this contract!")) + return + turn_in_scroll(user, scroll_in_hand) + else + for(var/obj/item/paper/scroll/quest/floor_scroll in input_point) + var/list/mob/quest_assignees = floor_scroll.get_quest_assignees(user, TRUE) + if(!(user in quest_assignees)) + continue + turn_in_scroll(user, floor_scroll) + + +/obj/structure/fake_machine/contractledger/proc/turn_in_scroll(mob/user, obj/item/paper/scroll/quest/scroll) + var/reward = 0 + var/original_reward = 0 + var/total_deposit_return = 0 + var/tax_rate = SStreasury.tax_value + var/tax_amt = 0 + + if(scroll.assigned_quest?.complete) + // Calculate base reward + var/base_reward = scroll.assigned_quest.reward_amount + original_reward += base_reward + + // Calculate deposit return + var/deposit_return = scroll.assigned_quest.calculate_deposit() + total_deposit_return += deposit_return + + // Apply Steward/Mechant bonus if applicable (only to the base reward) + var/datum/job/mob_job = user.job ? SSjob.GetJob(user.job) : null + if(mob_job?.is_quest_giver) + reward += base_reward * QUEST_HANDLER_REWARD_MULTIPLIER + else + reward += base_reward + + // Add deposit return to both reward totals + reward += deposit_return + original_reward += deposit_return + + qdel(scroll.assigned_quest) + qdel(scroll) + + // Tax payment + tax_amt = round(tax_rate * reward) + if(tax_amt > 0) + reward -= tax_amt + SStreasury.give_money_treasury(tax_amt, "quest completion tax - [src.name]") + record_featured_stat(FEATURED_STATS_TAX_PAYERS, user, tax_amt) + record_round_statistic(STATS_TAXES_COLLECTED, tax_amt) + + cash_in(round(reward), original_reward, tax_amt) + +/obj/structure/fake_machine/contractledger/proc/cash_in(reward, original_reward, tax_amt) + var/list/coin_types = list( + /obj/item/coin/gold = FLOOR(reward / 10, 1), + /obj/item/coin/silver = FLOOR(reward % 10 / 5, 1), + /obj/item/coin/copper = reward % 5 + ) + + for(var/coin_type in coin_types) + var/amount = coin_types[coin_type] + if(amount > 0) + var/obj/item/coin/coin_stack = new coin_type(get_turf(src)) + coin_stack.quantity = amount + coin_stack.update_icon() + coin_stack.update_transform() + + if(reward > 0) + say(reward > original_reward ? \ + "Your handler assistance-increased reward of [reward] mammons has been dispensed! The difference is [reward - original_reward] mammons. ([tax_amt] mammons taxed.)" : \ + "Your reward of [reward] mammons has been dispensed. ([tax_amt] mammons taxed.)") + +/obj/structure/fake_machine/contractledger/proc/abandon_contract(mob/user) + var/obj/item/paper/scroll/quest/abandoned_scroll = locate() in input_point + if(!abandoned_scroll) + to_chat(user, span_warning("No contract scroll found in the input area!")) + return + + var/datum/quest/quest = abandoned_scroll.assigned_quest + if(!quest) + to_chat(user, span_warning("This scroll doesn't have an assigned contract!")) + return + + if(quest.complete) + turn_in_contract(user) + return + + var/refund = quest.calculate_deposit() + + // First try to return to quest giver + var/mob/giver = quest.quest_giver_reference?.resolve() + if(giver && (giver in SStreasury.bank_accounts)) + SStreasury.bank_accounts[giver] += refund + SStreasury.treasury_value -= refund + SStreasury.log_entries += "-[refund] from treasury (contract refund to handler)" + to_chat(user, span_notice("The deposit has been returned to the contract giver.")) + // Otherwise try quest receiver + else if(quest.quest_receiver_reference) + var/mob/receiver = quest.quest_receiver_reference.resolve() + if(receiver && (receiver in SStreasury.bank_accounts)) + SStreasury.bank_accounts[receiver] += refund + SStreasury.treasury_value -= refund + SStreasury.log_entries += "-[refund] from treasury (contract refund to volunteer)" + to_chat(user, span_notice("You receive a [refund] mammon refund for abandoning the contract.")) + else + cash_in(refund) + SStreasury.treasury_value -= refund + SStreasury.log_entries += "-[refund] from treasury (contract refund)" + to_chat(user, span_notice("Your refund of [refund] mammon has been dispensed.")) + + // Clean up quest items + if(quest.quest_type == QUEST_COURIER && quest.target_delivery_item) + quest.target_delivery_item = null + for(var/obj/item/I in world) + if(istype(I, quest.target_delivery_item)) + var/datum/component/quest_object/Q = I.GetComponent(/datum/component/quest_object) + if(Q && Q.quest_ref == WEAKREF(quest)) + I.remove_filter("quest_item_outline") + qdel(Q) + qdel(I) + + log_quest(user.ckey, user.mind, user, "Abandon [abandoned_scroll.assigned_quest.quest_type]") + abandoned_scroll.assigned_quest = null + qdel(quest) + qdel(abandoned_scroll) + +/obj/structure/fake_machine/contractledger/proc/print_contracts(mob/user) + var/list/active_quests = list() + for(var/obj/item/paper/scroll/quest/quest_scroll in GLOB.quest_scrolls) + if(quest_scroll.assigned_quest && !quest_scroll.assigned_quest.complete) + active_quests += quest_scroll + + if(!length(active_quests)) + say("No active contracts found.") + return + + var/obj/item/paper/scroll/report = new(get_turf(src)) + report.name = "Guild Contract Report" + report.desc = "A list of currently active contracts issued by the Mercenary's Guild." + + var/report_text = "
MERCENARY'S GUILD - ACTIVE CONTRACTS


" + report_text += "Generated on [station_time_timestamp()]

" + + for(var/obj/item/paper/scroll/quest/quest_scroll in active_quests) + var/datum/quest/quest = quest_scroll.assigned_quest + var/area/quest_area = get_area(quest_scroll) + report_text += "Title: [quest.title].
" + report_text += "Issuer: [quest.quest_giver_name ? quest.quest_giver_name : "Mercenary's Guild"].
" + report_text += "Recipient: [quest.quest_receiver_name ? quest.quest_receiver_name : "Unclaimed"].
" + report_text += "Type: [quest.quest_type].
" + report_text += "Difficulty: [quest.quest_difficulty].
" + report_text += "Last Known Location: [quest_area ? quest_area.name : "Unknown Location"].
" + report_text += "Reward: [quest.reward_amount] mammons.

" + + report.info = report_text + say("Contract report printed.") diff --git a/code/modules/questing/items/parcel.dm b/code/modules/questing/items/parcel.dm new file mode 100644 index 00000000000..b883563b676 --- /dev/null +++ b/code/modules/questing/items/parcel.dm @@ -0,0 +1,120 @@ +/obj/item/parcel + name = "parcel wrapping paper" + desc = "A sturdy piece of paper used to wrap items for secure delivery. The final size of the parcel depends on the size of the original item." + icon = 'icons/obj/ration.dmi' + icon_state = "ration_wrapper" + w_class = WEIGHT_CLASS_TINY + grid_height = 32 + grid_width = 32 + dropshrink = 0.6 + var/obj/item/contained_item = null + var/list/allowed_jobs = list() + var/list/job_names = list() + var/delivery_area_type + var/datum/proximity_monitor/proximity_monitor + +/obj/item/parcel/Initialize(mapload) + . = ..() + var/datum/component/quest_object/courier_quest = GetComponent(/datum/component/quest_object) + if(courier_quest) + var/datum/quest/quest = courier_quest.quest_ref?.resolve() + if(quest && quest.quest_type == QUEST_COURIER && quest.target_delivery_location) + delivery_area_type = quest.target_delivery_location + allowed_jobs = get_area_jobs(delivery_area_type) + for(var/datum/job/job as anything in allowed_jobs) + job_names |= initial(job.title) + + RegisterSignal(courier_quest, COMSIG_PARENT_QDELETING, PROC_REF(on_quest_component_deleted)) + + invisibility = INVISIBILITY_OBSERVER + proximity_monitor = new(src, 5) + +/obj/item/parcel/HasProximity(mob/nearby) + if(!istype(nearby)) + return + + var/datum/component/quest_object/quest_component = GetComponent(/datum/component/quest_object) + if(!istype(quest_component)) + return + + var/datum/quest/quest = quest_component.quest_ref?.resolve() + if(!istype(quest)) + return + + if(get_dist(get_turf(src), get_turf(quest.quest_scroll_ref?.resolve())) > 5) + return + + var/image/I = image(icon = 'icons/effects/effects.dmi', loc = get_turf(src), icon_state = "hidden", layer = 18) + I.layer = 18 + I.plane = 18 + if(!I) + return + I.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + flick_overlay_view(I, 5 SECONDS) + invisibility = initial(invisibility) + QDEL_NULL(proximity_monitor) + +/obj/item/parcel/proc/get_area_jobs(area_type) + var/static/list/area_jobs = list( + /area/indoors/town/tavern = list(/datum/job/innkeep, /datum/job/innkeep_son, /datum/job/cook), + /area/indoors/town/bath = list(/datum/job/clinicapprentice, /datum/job/apothecary), + /area/indoors/town/church = list(/datum/job/priest, /datum/job/templar, /datum/job/churchling), + /area/indoors/town/dwarfin = list(/datum/job/innkeep, /datum/job/innkeep_son, /datum/job/cook), + /area/indoors/town/shop = list(/datum/job/merchant, /datum/job/shophand), + /area/indoors/town/noble_manor = list(/datum/job/servant, /datum/job/hand, /datum/job/royalknight, /datum/job/men_at_arms, /datum/job/steward, /datum/job/lord), + /area/indoors/town/keep/magician = list(/datum/job/magician, /datum/job/mageapprentice, /datum/job/archivist), + /area/indoors/town = list(/datum/job/innkeep, /datum/job/innkeep_son, /datum/job/cook) + ) + return area_jobs[area_type] || list(/datum/job/steward, /datum/job/merchant) + +/obj/item/parcel/proc/on_quest_component_deleted(datum/source) + SIGNAL_HANDLER + return + +/obj/item/parcel/attackby(obj/item/I, mob/user) + if(istype(I, /obj/item/parcel) || I.w_class > WEIGHT_CLASS_BULKY || contained_item) + to_chat(user, span_warning("You can't wrap this in [src].")) + return + + if(do_after(user, 2 SECONDS, target = src)) + user.transferItemToLoc(I, src) + contained_item = I + name = "parcel ([I.name])" + desc = "A securely wrapped parcel containing [I.name]." + icon_state = I.w_class >= WEIGHT_CLASS_NORMAL ? "ration_large" : "ration_small" + dropshrink = 1 + update_icon() + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + to_chat(user, span_notice("You wrap [I] in the parcel wrapper.")) + +/obj/item/parcel/attack_self(mob/user) + if(!contained_item) + return + + if(delivery_area_type) + var/area/quest_area = delivery_area_type + if(ispath(quest_area, /area) && !(user.job_type in allowed_jobs)) + to_chat(user, span_warning("This parcel is sealed for delivery to [initial(quest_area.name)] and can only be opened by: [english_list(job_names)]!")) + return FALSE + + if(do_after(user, 2 SECONDS, target = src)) + to_chat(user, span_notice("You unwrap [contained_item] from the parcel.")) + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + user.put_in_hands(contained_item) + contained_item.update_icon() + contained_item = null + qdel(src) + +/obj/item/parcel/examine(mob/user) + . = ..() + if(!delivery_area_type) + return + + var/area/delivery_area = delivery_area_type + if(!ispath(delivery_area, /area)) + return + + . += span_info("This parcel is addressed to [initial(delivery_area.name)].") + . += (user.job_type in allowed_jobs) ? \ + span_notice("As [user.job], you're authorized to open this.") : \ + span_warning("It's sealed with an official guild mark - only authorized personnel should open this!") diff --git a/code/modules/questing/items/quest_scroll.dm b/code/modules/questing/items/quest_scroll.dm new file mode 100644 index 00000000000..103325d84d7 --- /dev/null +++ b/code/modules/questing/items/quest_scroll.dm @@ -0,0 +1,336 @@ +GLOBAL_LIST_EMPTY(quest_scrolls) + +#define WHISPER_COOLDOWN 10 SECONDS +/obj/item/paper/scroll/quest + name = "enchanted contract scroll" + desc = "A scroll oft known as a \"whispering scroll\". Enchanted with magicks to make it whisper to its bearer when opened the location of its target.\n\ + The magical protections make it resistant to damage and tampering. It will only whisper when carried on the person of the contract bearer." + icon = 'icons/obj/questing.dmi' + icon_state = "scroll_quest" + base_icon_state = "scroll_quest" + var/datum/quest/assigned_quest + var/last_compass_direction = "" + var/last_z_level_hint = "" + var/last_whisper = 0 // Last time the scroll whispered to the user + resistance_flags = FIRE_PROOF | LAVA_PROOF | INDESTRUCTIBLE | UNACIDABLE + max_integrity = 1000 + armor = list("blunt" = 100, "slash" = 100, "stab" = 100, "piercing" = 100, "fire" = 100, "acid" = 100) + + COOLDOWN_DECLARE(next_compass_scan) + +/obj/item/paper/scroll/quest/Initialize() + . = ..() + if(assigned_quest) + assigned_quest.quest_scroll = src + update_quest_text() + START_PROCESSING(SSprocessing, src) + GLOB.quest_scrolls += src + +/obj/item/paper/scroll/quest/Destroy() + GLOB.quest_scrolls -= src + if(assigned_quest) + // Return deposit if scroll is destroyed before completion + if(!assigned_quest.complete) + var/refund = assigned_quest.calculate_deposit() + + // First try to return to quest giver if available + var/mob/giver = assigned_quest.quest_giver_reference?.resolve() + if(giver && (giver in SStreasury.bank_accounts)) + SStreasury.bank_accounts[giver] += refund + SStreasury.treasury_value -= refund + SStreasury.log_entries += "-[refund] from treasury (contract scroll destroyed refund to giver [giver.real_name])" + // Otherwise try quest receiver + else if(assigned_quest.quest_receiver_reference) + var/mob/receiver = assigned_quest.quest_receiver_reference.resolve() + if(receiver && (receiver in SStreasury.bank_accounts)) + SStreasury.bank_accounts[receiver] += refund + SStreasury.treasury_value -= refund + SStreasury.log_entries += "-[refund] from treasury (contract scroll destroyed refund to receiver [receiver.real_name])" + + // Clean up the quest + qdel(assigned_quest) + assigned_quest = null + STOP_PROCESSING(SSprocessing, src) + return ..() + +/obj/item/paper/scroll/quest/update_icon_state() + . = ..() + if(open) + icon_state = info ? "[base_icon_state]_info" : "[base_icon_state]" + else + icon_state = "[base_icon_state]_closed" + + +/obj/item/paper/scroll/quest/process() + if(world.time > last_whisper + WHISPER_COOLDOWN) + last_whisper = world.time + target_whisper() + +/obj/item/paper/scroll/quest/proc/target_whisper() + if(!assigned_quest || assigned_quest.complete || !assigned_quest.quest_receiver_reference) + return + var/obj/itemloc = src.loc + var/mob/quest_bearer = assigned_quest.quest_receiver_reference?.resolve() + // I should refactor this out at some point + if(!istype(itemloc, /mob/living)) + while(!istype(itemloc, /mob/living)) + if(isnull(itemloc)) + return + itemloc = itemloc.loc + if(istype(itemloc, /turf)) + return + if(itemloc != quest_bearer) + return + if(open && quest_bearer) + update_compass(quest_bearer) + var/message = "" + message = "[last_compass_direction]" + if(last_z_level_hint) + message += " ([last_z_level_hint])" + to_chat(quest_bearer, span_info("The scroll whispers to you, the target is [message]")) + +/obj/item/paper/scroll/quest/examine(mob/user) + . = ..() + if(!assigned_quest) + return + if(!assigned_quest.quest_receiver_reference) + . += span_notice("This contract hasn't been claimed yet. Open it to claim it for yourself!") + else if(assigned_quest.complete) + . += span_notice("\nThis contract is complete! Return it to the Notice Board to claim your reward.") + . += span_info("\nPlace it on the marked area next to the book.") + else + . += span_notice("\nThis contract is still in progress.") + +/obj/item/paper/scroll/quest/attackby(obj/item/P, mob/living/carbon/human/user, params) + if(P.get_sharpness()) + to_chat(user, span_warning("The enchanted scroll resists your attempts to tear it.")) + return + if(istype(P, /obj/item/paper)) // Prevent merging with other papers/scrolls + to_chat(user, span_warning("The magical energies prevent you from combining this with other scrolls.")) + return + if(istype(P, /obj/item/natural/thorn) || istype(P, /obj/item/natural/feather)) + if(!open) + to_chat(user, span_warning("You need to open the scroll first.")) + return + if(!assigned_quest) + to_chat(user, span_warning("This contract scroll doesn't accept modifications.")) + return + ..() + +/obj/item/paper/scroll/quest/proc/get_quest_assignees(var/mob/user, var/include_giver = FALSE) + var/list/assignees = list() + + var/mob/quest_receiver = assigned_quest?.quest_receiver_reference?.resolve() + if(quest_receiver) + assignees += quest_receiver + + if(include_giver) + var/mob/quest_giver = assigned_quest?.quest_giver_reference?.resolve() + if(quest_giver) + assignees += quest_giver + + return assignees + +/obj/item/paper/scroll/quest/fire_act(exposed_temperature, exposed_volume) + return // Immune to fire + +/obj/item/paper/scroll/quest/extinguish() + return // No fire to extinguish + +/obj/item/paper/scroll/quest/read(mob/user) + refresh_compass(user) + return ..() + +/obj/item/paper/scroll/quest/attack_self(mob/user) + . = ..() + if(.) + return + + // Only do claim logic if unclaimed + if(!assigned_quest || assigned_quest.quest_receiver_reference) + refresh_compass(user) // Refresh compass when opened by claimed user + update_quest_text() + return + + // Claim the quest + assigned_quest.quest_receiver_reference = WEAKREF(user) + assigned_quest.quest_receiver_name = user.real_name + + to_chat(user, span_notice("You claim this contract for yourself!")) + update_quest_text() + refresh_compass(user) // Update compass after claiming + +/obj/item/paper/scroll/quest/attack_self_secondary(mob/user) + if(!assigned_quest || assigned_quest.complete) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + if(!COOLDOWN_FINISHED(src, next_compass_scan)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + COOLDOWN_START(src, next_compass_scan, 2 SECONDS) + + var/turf/target_turf = assigned_quest.get_target_location() + if(!target_turf) + to_chat(user, span_warning("The scroll cannot sense its target.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/turf/user_turf = get_turf(user) + if(!user_turf) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if(target_turf.z != user_turf.z) + to_chat(user, span_info("The scroll pulses faintly - the target is on a different level.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/dir = get_dir(user_turf, target_turf) + if(!dir) + to_chat(user, span_info("The scroll pulses warmly - you are nearby.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/distance = get_dist(user_turf, target_turf) + var/arrow_color + switch(distance) + if(1 to 15) + arrow_color = COLOR_GREEN + if(16 to 40) + arrow_color = COLOR_YELLOW + if(41 to 100) + arrow_color = COLOR_ORANGE + else + arrow_color = COLOR_RED + + var/datum/hud/user_hud = user.hud_used + if(!user_hud || !istype(user_hud, /datum/hud) || !islist(user_hud.infodisplay)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/atom/movable/screen/multitool_arrow/arrow = new(null, user_hud) + arrow.color = arrow_color + arrow.screen_loc = "CENTER-1,CENTER-1" + arrow.transform = matrix(dir2angle(dir), MATRIX_ROTATE) + + user_hud.infodisplay += arrow + user_hud.show_hud(user_hud.hud_version) + + QDEL_IN(arrow, 1.5 SECONDS) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/item/paper/scroll/quest/proc/update_quest_text() + if(!assigned_quest) + return + + var/scroll_text = "
HELP NEEDED

" + scroll_text += "
[assigned_quest.get_title()]

" + scroll_text += "Issued by: [assigned_quest.quest_giver_name ? "[assigned_quest.quest_giver_name]" : "The Mercenary's Guild"].
" + scroll_text += "Issued to: [assigned_quest.quest_receiver_name ? assigned_quest.quest_receiver_name : "whoever it may concern"].
" + scroll_text += "Type: [assigned_quest.quest_type] contract.
" + scroll_text += "Difficulty: [assigned_quest.quest_difficulty].

" + + if(last_compass_direction) + scroll_text += "Direction: The target is [last_compass_direction]. " + if(last_z_level_hint) + scroll_text += " ([last_z_level_hint])" + scroll_text += "
" + + scroll_text += "Objective: [assigned_quest.get_objective_text()]
" + + // Show progress if applicable + if(assigned_quest.progress_required > 1) + scroll_text += "Progress: [assigned_quest.progress_current]/[assigned_quest.progress_required]
" + + scroll_text += "Location: [assigned_quest.get_location_text()]
" + scroll_text += "
Reward: [assigned_quest.reward_amount] mammon upon completion
" + + if(assigned_quest.complete) + scroll_text += "
CONTRACT COMPLETE
" + scroll_text += "
Return this scroll to the Notice Board to claim your reward!" + scroll_text += "
Place it on the marked area next to the book." + if(assigned_quest.quest_giver_reference) + scroll_text += "

Return this to [assigned_quest.quest_giver_name] for increased pay!" + else + scroll_text += "

Consider getting in touch with a Merchant or a Steward for your next quest for increased pay!" + else + scroll_text += "
The magic in this scroll will update as you progress." + if(assigned_quest.quest_giver_reference) + scroll_text += "

Returning this to [assigned_quest.quest_giver_name] upon completion will yield increased pay!" + else + scroll_text += "

Consider getting in touch with a Merchant or a Steward for your next quest for increased pay!" + + info = scroll_text + update_icon() + +/obj/item/paper/scroll/quest/proc/refresh_compass(mob/user) + if(!assigned_quest || assigned_quest.complete) + return FALSE + + // Update compass with precise directions + update_compass(user) + + // Only update text if we have a valid direction + if(last_compass_direction) + update_quest_text() + return TRUE + + return FALSE + +/obj/item/paper/scroll/quest/proc/update_compass(mob/user) + if(!assigned_quest || assigned_quest.complete) + return + + var/turf/user_turf = user ? get_turf(user) : get_turf(src) + if(!user_turf) + last_compass_direction = "No signal detected" + last_z_level_hint = "" + return + + // Reset compass values + last_compass_direction = "Searching for target..." + last_z_level_hint = "" + + // Get target location from quest datum + var/turf/target_turf = assigned_quest.get_target_location() + if(!target_turf) + last_compass_direction = "location unknown" + last_z_level_hint = "" + return + + // We want the target to know z level differences but verticality exists + // We don't want to frustrate player by forcing them to track on the same z level + // Especially cuz of how many transitions exist + if(target_turf.z != user_turf.z) + var/z_diff = abs(target_turf.z - user_turf.z) + last_z_level_hint = target_turf.z > user_turf.z ? \ + "[z_diff] level\s above you" : \ + "[z_diff] level\s below you" + + // Calculate direction from user to target + var/dx = target_turf.x - user_turf.x // EAST direction + var/dy = target_turf.y - user_turf.y // NORTH direction + var/distance = sqrt(dx*dx + dy*dy) + + // If very close, don't show direction + if(distance <= 7) + last_compass_direction = "is nearby" + last_z_level_hint = "" + return + + // Get precise direction text + var/direction_text = get_precise_direction_between(user_turf, target_turf) + if(!direction_text) + direction_text = "unknown direction" + + // Determine distance description + var/distance_text + switch(distance) + if(0 to 14) + distance_text = "very close" + if(15 to 40) + distance_text = "close" + if(41 to 100) + distance_text = "" + if(101 to INFINITY) + distance_text = "far away" + + last_compass_direction = "[distance_text] to the [direction_text]" + if(!last_z_level_hint) + last_z_level_hint = "on this level" + +#undef WHISPER_COOLDOWN diff --git a/code/modules/questing/items/spawn_effect.dm b/code/modules/questing/items/spawn_effect.dm new file mode 100644 index 00000000000..92f03042bd5 --- /dev/null +++ b/code/modules/questing/items/spawn_effect.dm @@ -0,0 +1,55 @@ +/obj/effect/quest_spawn + name = "quest spawner" + icon = 'icons/effects/landmarks_static.dmi' + icon_state = "x" + anchored = TRUE + layer = MID_LANDMARK_LAYER + invisibility = INVISIBILITY_OBSERVER + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + + var/atom/movable/contained_atom + var/datum/proximity_monitor/proximity_monitor + +/obj/effect/quest_spawn/Initialize(mapload) + . = ..() + proximity_monitor = new(src, 7) + +/obj/effect/quest_spawn/Destroy(force) + . = ..() + QDEL_NULL(contained_atom) + proximity_monitor = null + +/obj/effect/quest_spawn/HasProximity(mob/nearby) + if(!contained_atom) + return + + if(!istype(nearby)) + return + + var/datum/component/quest_object/quest_component = GetComponent(/datum/component/quest_object) + if(!istype(quest_component)) + return + + var/datum/quest/quest = quest_component.quest_ref?.resolve() + if(!istype(quest)) + return + + if(get_dist(get_turf(src), get_turf(quest.quest_scroll_ref?.resolve())) > 7) + return + + var/image/I = image(icon = 'icons/effects/effects.dmi', loc = get_turf(src), icon_state = "mobwarning", layer = 18) + I.layer = 18 + I.plane = 18 + I.alpha = 125 + I.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + flick_overlay_view(I, 5 SECONDS) + + contained_atom.forceMove(get_turf(src)) + contained_atom = null + + playsound(loc, "plantcross", 100, FALSE, 3) + + qdel(src) + +/obj/effect/quest_spawn/ex_act() + return diff --git a/code/modules/questing/landmarks.dm b/code/modules/questing/landmarks.dm new file mode 100644 index 00000000000..04fa4fb2df0 --- /dev/null +++ b/code/modules/questing/landmarks.dm @@ -0,0 +1,64 @@ +/obj/effect/landmark/quest_spawner + name = "quest landmark" + icon = 'icons/obj/questing.dmi' + icon_state = "quest_marker" + var/quest_difficulty = list(QUEST_DIFFICULTY_EASY, QUEST_DIFFICULTY_MEDIUM, QUEST_DIFFICULTY_HARD) + var/quest_type = list(QUEST_RETRIEVAL, QUEST_COURIER, QUEST_CLEAR_OUT, QUEST_RAID, QUEST_KILL_EASY, QUEST_BEACON, QUEST_OUTLAW) + +/obj/effect/landmark/quest_spawner/Initialize() + . = ..() + GLOB.quest_landmarks_list += src + +/obj/effect/landmark/quest_spawner/Destroy() + GLOB.quest_landmarks_list -= src + return ..() + +/obj/effect/landmark/quest_spawner/proc/add_quest_faction_to_nearby_mobs(turf/center) + for(var/mob/living/M in view(7, center)) + if(!M.ckey && !("quest" in M.faction)) + M.faction |= "quest" + +/obj/effect/landmark/quest_spawner/proc/get_safe_spawn_turf() + var/list/possible_landmarks = list() + for(var/obj/effect/landmark/quest_spawner/landmark in GLOB.quest_landmarks_list) + if((quest_difficulty in landmark.quest_difficulty) || (landmark.quest_difficulty in quest_difficulty)) + possible_landmarks += landmark + + if(!length(possible_landmarks)) + possible_landmarks += src + + var/obj/effect/landmark/quest_spawner/selected_landmark = pick(possible_landmarks) + var/list/possible_turfs = list() + + for(var/turf/open/floor/T in view(7, selected_landmark)) + if(T.density || istransparentturf(T)) + continue + + for(var/obj/O in get_turf(T)) + if(O.density) //No more spawning in metal bars or trees... + continue + + if(get_area(T) != get_area(selected_landmark)) //No more spawning in guild room... + continue + + possible_turfs += T + + return length(possible_turfs) ? pick(possible_turfs) : get_turf(src) + +/obj/effect/landmark/quest_spawner/easy + name = "easy quest landmark" + icon_state = "quest_marker_low" + quest_difficulty = "Easy" + quest_type = list(QUEST_RETRIEVAL, QUEST_COURIER, QUEST_KILL_EASY, QUEST_BEACON) + +/obj/effect/landmark/quest_spawner/medium + name = "medium quest landmark" + icon_state = "quest_marker_mid" + quest_difficulty = "Medium" + quest_type = list(QUEST_KILL_EASY, QUEST_CLEAR_OUT, QUEST_BEACON) + +/obj/effect/landmark/quest_spawner/hard + name = "hard quest landmark" + icon_state = "quest_marker_high" + quest_difficulty = "Hard" + quest_type = list(QUEST_CLEAR_OUT, QUEST_RAID, QUEST_BEACON, QUEST_OUTLAW) diff --git a/code/modules/questing/quest_object.dm b/code/modules/questing/quest_object.dm new file mode 100644 index 00000000000..56ccc60b58d --- /dev/null +++ b/code/modules/questing/quest_object.dm @@ -0,0 +1,215 @@ +/obj/effect/decal/marker_export + icon = 'icons/obj/questing.dmi' + icon_state = "marker_export" + +/datum/component/quest_object + var/datum/weakref/quest_ref + var/is_mob = FALSE + var/no_outline = FALSE + var/override_compatibility = FALSE + var/outline_filter_id = "quest_item_outline" + +/datum/component/quest_object/Initialize(datum/quest/target_quest) + if(!override_compatibility && !isitem(parent) && !ismob(parent)) + return COMPONENT_INCOMPATIBLE + + quest_ref = WEAKREF(target_quest) + is_mob = ismob(parent) + + if(!no_outline) + if(is_mob) + var/mob/M = parent + M.add_filter(outline_filter_id, 2, list("type" = "outline", "color" = "#ff0000", "size" = 0.5)) + RegisterSignal(parent, COMSIG_MOB_DEATH, PROC_REF(on_target_death)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_mob_examine)) + else + var/obj/item/I = parent + I.add_filter(outline_filter_id, 2, list("type" = "outline", "color" = "#008cff", "size" = 0.5)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_item_dropped)) + + RegisterSignal(target_quest, COMSIG_PARENT_QDELETING, PROC_REF(on_quest_deleted)) + +/datum/component/quest_object/Destroy() + if(QDELETED(parent)) + return ..() + + var/datum/quest/Q = quest_ref?.resolve() + if(Q && !Q.complete && isitem(parent)) + var/obj/item/I = parent + I.remove_filter(outline_filter_id) + if(Q.quest_type == QUEST_COURIER && (Q.target_delivery_item && istype(I, Q.target_delivery_item)) && !QDELETED(I)) + Q.target_delivery_item = null + qdel(I) + + return ..() + +/datum/component/quest_object/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + var/datum/quest/Q = quest_ref.resolve() + if(!Q || Q.complete) + return + + var/list/user_scrolls = find_quest_scrolls(user) + for(var/obj/item/paper/scroll/quest/scroll in user_scrolls) + var/datum/quest/user_quest = scroll.assigned_quest + if(user_quest && ((user_quest.quest_type == QUEST_RETRIEVAL && istype(parent, user_quest.target_item_type)) || \ + (user_quest.quest_type == QUEST_COURIER && istype(parent, user_quest.target_delivery_item)))) + examine_list += span_notice("This looks like an item you need for your quest: [user_quest.title]!") + break + +/datum/component/quest_object/proc/on_mob_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + var/datum/quest/Q = quest_ref.resolve() + if(!Q || Q.complete) + return + + var/list/user_scrolls = find_quest_scrolls(user) + for(var/obj/item/paper/scroll/quest/scroll in user_scrolls) + var/datum/quest/user_quest = scroll.assigned_quest + if(user_quest && (user_quest.quest_type in list(QUEST_KILL_EASY, QUEST_CLEAR_OUT, QUEST_RAID, QUEST_OUTLAW)) && istype(parent, user_quest.target_mob_type)) + examine_list += span_notice("This looks like the target of your quest: [user_quest.title]!") + if(Q.target_spawn_area != get_area(get_turf(src))) + examine_list += span_notice("It was last reported in the [Q.target_spawn_area] area, however.") + break + +/datum/component/quest_object/proc/find_quest_scrolls(atom/container) + var/list/scrolls = list() + for(var/obj/item/paper/scroll/quest/Q in container) + scrolls += Q + for(var/obj/item/storage/S in container) + scrolls += find_quest_scrolls(S) + return scrolls + +/datum/component/quest_object/proc/on_target_death(mob/living/dead_mob, gibbed) + SIGNAL_HANDLER + + var/datum/quest/Q = quest_ref.resolve() + if(!Q || Q.complete || !istype(dead_mob, Q.target_mob_type)) + return + + dead_mob.remove_filter("quest_item_outline") + + // Notify quest of progress + Q.progress_current++ + Q.on_progress_update() + +/datum/component/quest_object/proc/on_item_dropped(obj/item/dropped_item, mob/user) + SIGNAL_HANDLER + // Override in subtypes + +/datum/component/quest_object/proc/on_quest_deleted(datum/source) + SIGNAL_HANDLER + + if(QDELETED(parent)) + return + + var/datum/quest/Q = quest_ref?.resolve() + + if(ismob(parent)) + var/mob/M = parent + M.remove_filter(outline_filter_id) + else if(isitem(parent)) + var/obj/item/I = parent + I.remove_filter(outline_filter_id) + // Only delete the item if it's part of an incomplete fetch or courier quest + if(Q && !Q.complete && ((Q.quest_type == QUEST_RETRIEVAL && istype(I, Q.target_item_type)) || (Q.quest_type == QUEST_COURIER && istype(I, Q.target_delivery_item)))) + qdel(I) + qdel(src) + +// ==================== SPECIALIZED COMPONENT SUBTYPES ==================== + +/// Component for kill/clearout/outlaw quests - handles mob death +/datum/component/quest_object/kill + +/datum/component/quest_object/kill/Initialize(datum/quest/target_quest) + . = ..() + if(. == COMPONENT_INCOMPATIBLE) + return + +/// Component for retrieval quests - handles item collection +/datum/component/quest_object/retrieval + +/datum/component/quest_object/retrieval/Initialize(datum/quest/target_quest) + . = ..() + if(. == COMPONENT_INCOMPATIBLE) + return + +/datum/component/quest_object/retrieval/on_item_dropped(obj/item/dropped_item, mob/user) + var/datum/quest/Q = quest_ref.resolve() + if(!Q || Q.complete) + return + + var/turf/drop_turf = get_turf(dropped_item) + + var/obj/effect/decal/marker_export/marker = locate() in drop_turf + + if(marker) + if(Q.target_item_type && istype(dropped_item, Q.target_item_type)) + // Notify quest of progress + Q.progress_current++ + Q.on_progress_update() + do_sparks(3, TRUE, get_turf(dropped_item)) + qdel(dropped_item) + return + +/// Component for courier quests - handles delivery +/datum/component/quest_object/courier + +/datum/component/quest_object/courier/Initialize(datum/quest/target_quest) + . = ..() + if(. == COMPONENT_INCOMPATIBLE) + return + +/datum/component/quest_object/courier/on_item_dropped(obj/item/dropped_item, mob/user) + var/datum/quest/Q = quest_ref.resolve() + if(!Q || Q.complete) + return + + var/turf/drop_turf = get_turf(dropped_item) + var/area/drop_area = get_area(drop_turf) + + if(!istype(drop_area, Q.target_delivery_location)) + return + + // Handle parcel delivery + if(istype(dropped_item, /obj/item/parcel)) + var/obj/item/parcel/parcel = dropped_item + if(parcel.contained_item && istype(parcel.contained_item, Q.target_delivery_item)) + parcel.remove_filter(outline_filter_id) + if(parcel.contained_item) + parcel.contained_item.remove_filter(outline_filter_id) + + // Notify quest of progress + Q.progress_current++ + Q.on_progress_update() + return + + // Handle direct item delivery + else if(istype(dropped_item, Q.target_delivery_item)) + dropped_item.remove_filter(outline_filter_id) + + // Notify quest of progress + Q.progress_current++ + Q.on_progress_update() + return + +/// Component for kill quest spawners +/datum/component/quest_object/mob_spawner + override_compatibility = TRUE + no_outline = TRUE + +/datum/component/quest_object/mob_spawner/Initialize(datum/quest/target_quest) + . = ..() + if(. == COMPONENT_INCOMPATIBLE) + return + +/datum/component/quest_object/mob_spawner/on_quest_deleted(datum/source) + if(QDELETED(parent)) + return + + qdel(parent) + qdel(src) diff --git a/code/modules/questing/quests/__quest_list.dm b/code/modules/questing/quests/__quest_list.dm new file mode 100644 index 00000000000..2902f4cf50e --- /dev/null +++ b/code/modules/questing/quests/__quest_list.dm @@ -0,0 +1,5 @@ +GLOBAL_LIST_INIT(global_quest_types, list( + QUEST_DIFFICULTY_EASY = list(QUEST_RETRIEVAL, QUEST_COURIER, QUEST_KILL_EASY), + QUEST_DIFFICULTY_MEDIUM = list(QUEST_CLEAR_OUT), + QUEST_DIFFICULTY_HARD = list(QUEST_RAID, QUEST_OUTLAW), +)) diff --git a/code/modules/questing/quests/_quest.dm b/code/modules/questing/quests/_quest.dm new file mode 100644 index 00000000000..311123c4303 --- /dev/null +++ b/code/modules/questing/quests/_quest.dm @@ -0,0 +1,182 @@ +/datum/quest + var/title = "" + var/datum/weakref/quest_giver_reference + var/quest_giver_name = "" + var/datum/weakref/quest_receiver_reference + var/quest_receiver_name = "" + var/quest_type = "" + var/quest_difficulty = "" + var/reward_amount = 0 + var/deposit_amount = 0 + var/complete = FALSE + + /// Progress tracking + var/progress_current = 0 + var/progress_required = 1 + + /// Target item type for fetch quests + var/obj/item/target_item_type + /// Target item type for courier quests + var/obj/item/target_delivery_item + /// Target mob type for kill quests + var/mob/target_mob_type + /// Location for courier quests + var/area/indoors/town/target_delivery_location + /// Location name for kill/clear quests + var/target_spawn_area = "" + + /// Scroll icon state + var/quest_icon = "scroll_quest" + + /// Fallback reference to the spawned scroll + var/obj/item/paper/scroll/quest/quest_scroll + /// Weak reference to the quest scroll + var/datum/weakref/quest_scroll_ref + /// List of weakrefs to actual quest items/mobs for reducing overhead of compass. + var/list/datum/weakref/tracked_atoms = list() + +/datum/quest/Destroy() + // Clean up mobs with quest components + for(var/mob/living/M in GLOB.mob_list) + var/datum/component/quest_object/Q = M.GetComponent(/datum/component/quest_object) + if(Q && Q.quest_ref?.resolve() == src) + M.remove_filter("quest_item_outline") + qdel(Q) + + for(var/datum/weakref/tracked_weakref in tracked_atoms) + var/atom/target_atom = tracked_weakref.resolve() + if(QDELETED(target_atom)) + continue + + // Only delete the item if it's part of a fetch or courier quest + if(quest_type == QUEST_RETRIEVAL && istype(target_atom, target_item_type)) + qdel(target_atom) + else if(quest_type == QUEST_COURIER && istype(target_atom, target_delivery_item)) + qdel(target_atom) + + tracked_atoms -= tracked_weakref + qdel(tracked_weakref) + + // Clean up references + quest_scroll = null + if(quest_scroll_ref) + var/obj/item/paper/scroll/quest/Q = quest_scroll_ref.resolve() + if(Q && !QDELETED(Q)) + Q.assigned_quest = null + qdel(Q) + quest_scroll_ref = null + + return ..() + +/datum/quest/proc/add_tracked_atom(atom/movable/to_track) + tracked_atoms += WEAKREF(to_track) + +/// Generate quest content - override in subtypes +/datum/quest/proc/generate(obj/effect/landmark/quest_spawner/landmark) + if(!title) + title = get_title() + return TRUE + +/// Get the quest title - override in subtypes for dynamic titles +/datum/quest/proc/get_title() + return title + +/// Get objective text for scroll display +/datum/quest/proc/get_objective_text() + return "Complete the objective." + +/// Get location text for scroll display +/datum/quest/proc/get_location_text() + return target_spawn_area ? "Reported sighting in [target_spawn_area] region." : "Location unknown." + +/// Check if quest objectives are complete +/datum/quest/proc/check_completion() + return progress_current >= progress_required + +/// Called when progress is updated +/datum/quest/proc/on_progress_update() + if(check_completion()) + mark_complete() + else + quest_scroll?.update_quest_text() + +/// Mark quest as complete +/datum/quest/proc/mark_complete() + complete = TRUE + quest_scroll?.update_quest_text() + +// Base reward scaled only to difficulty +/datum/quest/proc/get_base_reward() + switch(quest_difficulty) + if(QUEST_DIFFICULTY_EASY) + return rand(QUEST_REWARD_EASY_LOW, QUEST_REWARD_EASY_HIGH) + if(QUEST_DIFFICULTY_MEDIUM) + return rand(QUEST_REWARD_MEDIUM_LOW, QUEST_REWARD_MEDIUM_HIGH) + if(QUEST_DIFFICULTY_HARD) + return rand(QUEST_REWARD_HARD_LOW, QUEST_REWARD_HARD_HIGH) + +// Additional reward, override in subtypes for specific calculations. Called AFTER generation. +/datum/quest/proc/get_additional_reward(turf/target_turf) + return 0 + +/// Calculate reward based on base + additional reward. Called AFTER generation. +/datum/quest/proc/calculate_reward(turf/target_turf) + var/base = get_base_reward() + var/additional = get_additional_reward(target_turf) + return base + additional + +/// Calculate deposit based on difficulty +/datum/quest/proc/calculate_deposit() + switch(quest_difficulty) + if(QUEST_DIFFICULTY_EASY) + return QUEST_DEPOSIT_EASY + if(QUEST_DIFFICULTY_MEDIUM) + return QUEST_DEPOSIT_MEDIUM + if(QUEST_DIFFICULTY_HARD) + return QUEST_DEPOSIT_HARD + return 0 + +/// Get icon for scroll based on difficulty +/datum/quest/proc/get_scroll_icon() + switch(quest_difficulty) + if(QUEST_DIFFICULTY_EASY) + return "scroll_quest_low" + if(QUEST_DIFFICULTY_MEDIUM) + return "scroll_quest_mid" + if(QUEST_DIFFICULTY_HARD) + return "scroll_quest_high" + return quest_icon + +/// Get target location for compass - returns turf of nearest tracked atom +/datum/quest/proc/get_target_location() + var/turf/user_turf = quest_scroll ? get_turf(quest_scroll) : null + if(!user_turf) + return null + + var/turf/closest + var/min_dist = INFINITY + + for(var/datum/weakref/ref in tracked_atoms) + var/atom/A = ref.resolve() + if(!A || QDELETED(A)) + continue + + var/turf/A_turf = get_turf(A) + if(!A_turf) + continue + + var/dist = get_dist(user_turf, A_turf) + if(dist < min_dist) + min_dist = dist + closest = A_turf + + return closest + +/// Check if a user can claim this quest - override for restrictions +/datum/quest/proc/can_claim(mob/user) + return TRUE + +/// Called when quest is claimed by a user +/datum/quest/proc/on_claim(mob/user) + quest_receiver_reference = WEAKREF(user) + quest_receiver_name = user.real_name diff --git a/code/modules/questing/quests/courier.dm b/code/modules/questing/quests/courier.dm new file mode 100644 index 00000000000..3b2e2368e69 --- /dev/null +++ b/code/modules/questing/quests/courier.dm @@ -0,0 +1,123 @@ +/datum/quest/courier + quest_type = QUEST_COURIER + var/list/target_delivery_locations = list( + /area/indoors/town/tavern, + /area/indoors/town/church, + /area/indoors/town/dwarfin, + /area/indoors/town/shop, + /area/indoors/town/noble_manor, + /area/indoors/town/keep/magician, + ) + +/datum/quest/courier/get_title() + if(title) + return title + return "Deliver [pick("an important", "a sealed", "a confidential", "a valuable")] [pick("package", "parcel", "letter", "delivery")]" + +/datum/quest/courier/get_objective_text() + return "Deliver [initial(target_delivery_item.name)] to [initial(target_delivery_location.name)]." + +/datum/quest/courier/get_location_text() + var/text = "" + if(target_spawn_area) + text += "Pickup location: Reported sighting in [target_spawn_area] region.
" + text += "Destination: [initial(target_delivery_location.name)]." + return text + +/datum/quest/courier/get_additional_reward(target_turf) + var/turf/scroll_turf = get_turf(quest_scroll) + var/distance = CLAMP(get_dist(scroll_turf, target_turf), 0, 200) // Avoid infinity rewards if it bugs out + var/distance_reward = (distance / QUEST_DELIVERY_DISTANCE_DIVISOR) * QUEST_DELIVERY_DISTANCE_BONUS + return ROUND_UP(distance_reward + QUEST_COURIER_BONUS_FLAT) + +/datum/quest/courier/proc/spawn_courier_item(area/delivery_area, obj/effect/landmark/quest_spawner/landmark) + if(!delivery_area) + return null + + var/turf/spawn_turf = landmark.get_safe_spawn_turf() + if(!spawn_turf) + return + + var/obj/item/parcel/delivery_parcel = new(spawn_turf) + var/static/list/area_delivery_items = list( + /area/indoors/town/tavern = list( + /obj/item/cooking/pan, + /obj/item/reagent_containers/glass/bottle/beer/aurorian, + /obj/item/reagent_containers/food/snacks/cheddar, + ), + /area/indoors/town/bath = list( + /obj/item/reagent_containers/glass/bottle/beer/aurorian, + /obj/item/reagent_containers/food/snacks/pie/cooked/meat/fish, + /obj/item/perfume/random, + ), + /area/indoors/town/church = list( + /obj/item/natural/cloth, + /obj/item/reagent_containers/powder/ozium, + /obj/item/reagent_containers/food/snacks/hardtack, + ), + /area/indoors/town/dwarfin = list( + /obj/item/ingot/iron, + /obj/item/ingot/bronze, + /obj/item/ore/coal, + ), + /area/indoors/town/shop = list( + /obj/item/coin/gold, + /obj/item/clothing/ring/silver, + /obj/item/scomstone/bad, + ), + /area/indoors/town/noble_manor = list( + /obj/item/clothing/cloak/raincloak/furcloak, + /obj/item/reagent_containers/glass/bottle/whitewine, + /obj/item/reagent_containers/food/snacks/cheddar/aged, + /obj/item/perfume/random, + ), + /area/indoors/town/keep/magician = list( + /obj/item/book/granter/spellbook, + /obj/item/gem/yellow, + /obj/item/reagent_containers/glass/bottle/manapot, + ), + /area/indoors/town = list( + /obj/item/ration, + ) + ) + + var/list/possible_items = area_delivery_items[delivery_area] || list( + /obj/item/natural/cloth, + /obj/item/ration, + /obj/item/reagent_containers/food/snacks/hardtack, + ) + + var/contained_item_type = pick(possible_items) + var/obj/item/contained_item = new contained_item_type(delivery_parcel) + delivery_parcel.contained_item = contained_item + delivery_parcel.delivery_area_type = delivery_area + delivery_parcel.allowed_jobs = delivery_parcel.get_area_jobs(delivery_area) + delivery_parcel.name = "Delivery for [initial(delivery_area.name)]" + delivery_parcel.desc = "A securely wrapped parcel addressed to [initial(delivery_area.name)]. [pick("Handle with care.", "Do not bend.", "Confidential contents.", "Urgent delivery.")]" + delivery_parcel.icon_state = contained_item.w_class >= WEIGHT_CLASS_NORMAL ? "ration_large" : "ration_small" + delivery_parcel.dropshrink = 1 + delivery_parcel.update_icon() + + target_delivery_item = contained_item_type + delivery_parcel.AddComponent(/datum/component/quest_object/courier, src) + contained_item.AddComponent(/datum/component/quest_object/courier, src) + add_tracked_atom(delivery_parcel) + + return delivery_parcel + +/datum/quest/courier/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + + // Select delivery location + target_delivery_location = pick(target_delivery_locations) + progress_required = 1 + target_spawn_area = get_area_name(get_turf(landmark)) + + // Spawn parcel + var/obj/item/parcel/delivery_parcel = spawn_courier_item(target_delivery_location, landmark) + if(!delivery_parcel) + return FALSE + + return TRUE diff --git a/code/modules/questing/quests/kill/_kill_base.dm b/code/modules/questing/quests/kill/_kill_base.dm new file mode 100644 index 00000000000..75828ca6b64 --- /dev/null +++ b/code/modules/questing/quests/kill/_kill_base.dm @@ -0,0 +1,34 @@ +// Base for kill quests +/datum/quest/kill + var/list/mob_types_to_spawn = list() + var/count_min = 1 + var/count_max = 3 + +/datum/quest/kill/proc/spawn_kill_mobs(obj/effect/landmark/quest_spawner/landmark) + target_mob_type = pick(mob_types_to_spawn) + progress_required = rand(count_min, count_max) + target_spawn_area = get_area_name(get_turf(landmark)) + + // Spawn mobs + for(var/i in 1 to progress_required) + var/turf/spawn_turf = landmark.get_safe_spawn_turf() + if(!spawn_turf) + continue + + var/obj/effect/quest_spawn/spawn_effect = new /obj/effect/quest_spawn(spawn_turf) + var/mob/living/new_mob = new target_mob_type(spawn_effect) + new_mob.faction |= "quest" + new_mob.AddComponent(/datum/component/quest_object/kill, src) + ADD_TRAIT(new_mob, TRAIT_FRESHSPAWN, "[type]") + addtimer(TRAIT_CALLBACK_REMOVE(new_mob, TRAIT_FRESHSPAWN, "[type]"), 60 SECONDS) + spawn_effect.contained_atom = new_mob + spawn_effect.AddComponent(/datum/component/quest_object/mob_spawner, src) + add_tracked_atom(new_mob) + landmark.add_quest_faction_to_nearby_mobs(spawn_turf) + sleep(1) + +/datum/quest/kill/get_additional_reward() + ..() + // Additional reward based on mob difficulty and number required + var/mob_type_difficulty = QUEST_KILL_MOBS_LIST[target_mob_type] + return progress_required * mob_type_difficulty diff --git a/code/modules/questing/quests/kill/clearout.dm b/code/modules/questing/quests/kill/clearout.dm new file mode 100644 index 00000000000..8bd86da22f0 --- /dev/null +++ b/code/modules/questing/quests/kill/clearout.dm @@ -0,0 +1,24 @@ +/datum/quest/kill/clearout + quest_type = QUEST_CLEAR_OUT + mob_types_to_spawn = QUEST_KILL_MEDIUM_LIST + count_min = 3 + count_max = 6 + +/datum/quest/kill/clearout/get_title() + if(title) + return title + return "Clear out [pick("a nest of", "a den of", "a group of", "a pack of")] [pick("monsters", "bandits", "creatures", "vermin")]" + +/datum/quest/kill/clearout/get_objective_text() + return "Eliminate [progress_required] [initial(target_mob_type.name)]." + +/datum/quest/kill/clearout/get_location_text() + return target_spawn_area ? "Reported infestation in [target_spawn_area] region." : "Reported infestations in Azuria region." + +/datum/quest/kill/clearout/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + spawn_kill_mobs(landmark) + + return TRUE diff --git a/code/modules/questing/quests/kill/easy_kill.dm b/code/modules/questing/quests/kill/easy_kill.dm new file mode 100644 index 00000000000..e0771cadcb6 --- /dev/null +++ b/code/modules/questing/quests/kill/easy_kill.dm @@ -0,0 +1,22 @@ +// Easy kill quests +/datum/quest/kill/easy + quest_type = QUEST_KILL_EASY + mob_types_to_spawn = QUEST_KILL_MOBS_LIST + count_min = 1 + count_max = 3 + +/datum/quest/kill/easy/get_title() + if(title) + return title + return "Slay [pick("a dangerous", "a fearsome", "a troublesome", "an elusive")] [pick("beast", "monster", "brigand", "creature")]" + +/datum/quest/kill/easy/get_objective_text() + return "Slay [progress_required] [initial(target_mob_type.name)]." + +/datum/quest/kill/easy/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + spawn_kill_mobs(landmark) + + return TRUE diff --git a/code/modules/questing/quests/kill/outlaw.dm b/code/modules/questing/quests/kill/outlaw.dm new file mode 100644 index 00000000000..1386ae506aa --- /dev/null +++ b/code/modules/questing/quests/kill/outlaw.dm @@ -0,0 +1,37 @@ +/datum/quest/kill/outlaw + quest_type = QUEST_OUTLAW + mob_types_to_spawn = list( + /mob/living/carbon/human/species/human/northern/deranged_knight + ) + count_min = 1 + count_max = 1 + +/datum/quest/kill/outlaw/get_title() + if(title) + return title + return "Defeat [pick("the terrible", "the dreadful", "the monstrous", "the infamous")] [pick("warlord", "beast", "sorcerer", "abomination")]" + +/datum/quest/kill/outlaw/get_objective_text() + return "Slay [initial(target_mob_type.name)]." + +/datum/quest/kill/outlaw/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + spawn_kill_mobs(landmark) + spawn_goons(landmark) + return TRUE + +/// Spawns proximity-gated goons near the quest landmark to accompany the outlaw target. +/datum/quest/kill/outlaw/proc/spawn_goons(obj/effect/landmark/quest_spawner/landmark) + for(var/i in 1 to rand(2, 5)) + var/turf/spawn_turf = landmark.get_safe_spawn_turf() + if(!spawn_turf) + continue + var/obj/effect/quest_spawn/spawn_effect = new /obj/effect/quest_spawn(spawn_turf) + var/mob/living/goon = new /mob/living/carbon/human/species/human/northern/highwayman/dk_goon(spawn_effect) + goon.faction |= "quest" + spawn_effect.contained_atom = goon + spawn_effect.AddComponent(/datum/component/quest_object/mob_spawner, src) + ADD_TRAIT(goon, TRAIT_FRESHSPAWN, "[type]") + addtimer(TRAIT_CALLBACK_REMOVE(goon, TRAIT_FRESHSPAWN, "[type]"), 60 SECONDS) diff --git a/code/modules/questing/quests/kill/raid.dm b/code/modules/questing/quests/kill/raid.dm new file mode 100644 index 00000000000..ef827cef88c --- /dev/null +++ b/code/modules/questing/quests/kill/raid.dm @@ -0,0 +1,24 @@ +/datum/quest/kill/raid + quest_type = QUEST_RAID + mob_types_to_spawn = QUEST_RAID_LIST + count_min = 4 + count_max = 6 + +/datum/quest/kill/raid/get_title() + if(title) + return title + return "Stop a raid of [pick("slavers", "bandits", "brigands", "raiders")]" + +/datum/quest/kill/raid/get_objective_text() + return "Eliminate [progress_required] [initial(target_mob_type.name)]." + +/datum/quest/kill/raid/get_location_text() + return target_spawn_area ? "Reported raid in [target_spawn_area] region." : "Reported infestations in Azuria region." + +/datum/quest/kill/raid/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + spawn_kill_mobs(landmark) + + return TRUE diff --git a/code/modules/questing/quests/retrieval.dm b/code/modules/questing/quests/retrieval.dm new file mode 100644 index 00000000000..65f128ed435 --- /dev/null +++ b/code/modules/questing/quests/retrieval.dm @@ -0,0 +1,46 @@ +/datum/quest/retrieval + quest_type = QUEST_RETRIEVAL + var/list/fetch_items = list( + /obj/item/weapon/knife/throwingknife/steel, + /obj/item/weapon/knife, + /obj/item/reagent_containers/glass/bottle/whitewine + ) + +/datum/quest/retrieval/get_title() + if(title) + return title + return "Retrieve [pick("an ancient", "a rare", "a stolen", "a magical")] [pick("artifact", "relic", "doohickey", "treasure")]" + +/datum/quest/retrieval/get_objective_text() + return "Retrieve [progress_required] [initial(target_item_type.name)]." + + +/datum/quest/retrieval/get_additional_reward(target_turf) + var/turf/scroll_turf = get_turf(quest_scroll) + var/distance = CLAMP(get_dist(scroll_turf, target_turf), 0, 200) // Avoid infinity rewards if it bugs out + var/distance_reward = (distance / QUEST_DELIVERY_DISTANCE_DIVISOR) * QUEST_DELIVERY_DISTANCE_BONUS + var/item_bonus = progress_required * QUEST_DELIVERY_PER_ITEM_BONUS + + return ROUND_UP(distance_reward + item_bonus) + +/datum/quest/retrieval/generate(obj/effect/landmark/quest_spawner/landmark) + ..() + if(!landmark) + return FALSE + + // Select random item type from landmark's list + target_item_type = pick(fetch_items) + progress_required = rand(1, 3) + target_spawn_area = get_area_name(get_turf(landmark)) + + // Spawn items + for(var/i in 1 to progress_required) + var/turf/spawn_turf = landmark.get_safe_spawn_turf() + if(!spawn_turf) + continue + + var/obj/item/new_item = new target_item_type(spawn_turf) + new_item.AddComponent(/datum/component/quest_object/retrieval, src) + add_tracked_atom(new_item) + + return TRUE diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index da84fb22c1b..8bec7d5f025 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -213,3 +213,57 @@ return L.mana_pool.restore_mana_disperse("manabloom") + + +/datum/reagent/toxin/spidervenom_paralytic + name = "Aragn Essence" + description = "A strong neurotoxin that makes muscles stiffen up and spasm." + silent_toxin = TRUE + reagent_state = SOLID + color = "#99005e" + toxpwr = 0 + taste_description = "raspberry" + metabolization_rate = 0.01 + var/venom_resistance + +/obj/item/reagent_containers/glass/bottle/spidervenom_paralytic + list_reagents = list(/datum/reagent/toxin/spidervenom_paralytic = 1) + desc = "An ominous vial, filled with venom of the deadly Aragn spider. Feels hot to the touch." + +/datum/reagent/toxin/spidervenom_paralytic/on_mob_metabolize(mob/living/L) + ..() + venom_resistance += ((L.STACON - 10) * 5) + venom_resistance += ((L.STAEND - 10) * 3) + venom_resistance += ((L.STASTR - 10) * 2) + venom_resistance += (L.STALUC) + + if(venom_resistance <= 0) + venom_resistance = 0 + venom_resistance += (L.STALUC * 5) + +/datum/reagent/toxin/spidervenom_paralytic/on_mob_end_metabolize(mob/living/L) + ..() + +/datum/reagent/toxin/spidervenom_paralytic/on_mob_life(mob/living/carbon/M) + ..() + if(!(current_cycle % 5) && !(prob(venom_resistance / 5))) + M.Paralyze(50) + if(current_cycle >= 60 && !(current_cycle % 5) && prob(venom_resistance)) + M.reagents.remove_reagent(/datum/reagent/toxin/spidervenom_paralytic, 100) + +/datum/reagent/toxin/spidervenom_inert + name = "Inert Aragn Essence" + description = "Without the spider, the venom has weakened. It must be strengthened with a binding catalyst first." + silent_toxin = TRUE + reagent_state = SOLID + color = "#003d99" + toxpwr = 0 + taste_description = "blueberry" + metabolization_rate = 10 + +/obj/item/reagent_containers/spidervenom_inert + list_reagents = list(/datum/reagent/toxin/spidervenom_inert = 10) + name = "Pale spider gland" + desc = "A squishy pale gland, filled to the brim with venom of the deadly Aragn spider. Feels cold to the touch." + icon = 'icons/obj/webbing.dmi' + icon_state = "gland" diff --git a/code/modules/surgery/bodyparts/bodypart_dismemberment.dm b/code/modules/surgery/bodyparts/bodypart_dismemberment.dm index 4da4310e8d0..b64b826ffa2 100644 --- a/code/modules/surgery/bodyparts/bodypart_dismemberment.dm +++ b/code/modules/surgery/bodyparts/bodypart_dismemberment.dm @@ -35,6 +35,8 @@ return FALSE if(HAS_TRAIT(C, TRAIT_NODISMEMBER)) return FALSE + if(SEND_SIGNAL(src, COMSIG_MOB_DISMEMBER, src) & COMPONENT_CANCEL_DISMEMBER) + return FALSE //signal handled the dropping if(ishuman(owner)) var/mob/living/carbon/human/human_owner = owner var/obj/item/clothing/checked_armor = human_owner.check_crit_armor(zone_precise, bclass) diff --git a/icons/mob/mirespider_big.dmi b/icons/mob/mirespider_big.dmi new file mode 100644 index 0000000000000000000000000000000000000000..96371e506dc7d3fbd3d42cbe76a3e6435e12a18b GIT binary patch literal 9057 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 TI>#bo00000NkvXXu0mjfTw?e3 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 literal 0 HcmV?d00001 diff --git a/icons/obj/ration.dmi b/icons/obj/ration.dmi new file mode 100644 index 0000000000000000000000000000000000000000..fb911988f08da9b9b3e2ac921531a4223e3b5ec8 GIT binary patch literal 1976 zcmV;p2S@mcP)V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+ z(=$pSoZ^zil2jm5sVK1os64*BD6ya*wMdDJGbOXA7${-L#hF%=n41b=qbo1YP0Yz5 zR&h>ZQF<*HnZEiQ|Ac zrHy&88>b@0NEt+=y66Y(Zz2K-q$C<7sf?n_C^Ev3>B1t2AfhSLi|QgmhLDJ46#Tg< zraYxN2s6%AL^mg&b$a)FXYF;)*=OeH-owlbhrQ2Q`)9rH_pNWOy}s2kJUr~?GOo$H z0eNFyfV>-!H|7P%y8(IQx_T9#9N1g2|F`el(?JKSH$kM=$8hQpK z7!{^NUg8}-I+~CZQG2|-)aC2ARt9?iqwg99%IG`*GCV}&fhA0%R40#6zXTdJB%It9 zVHqy7ke4T7RS|~Z(#{*)!9&9}WtO_n{x?HP0Nei+$t^^_YUy?VmrlLN4vE$! zasBOUnzqO7d2LsP3C^yk`uzK=UC4P6fJpmT&{s;H&vpM?`a21}nx-E=^{d<7iU2W7 z+x`ry?miXy6)zO9>_|b~*GEoY{_U#leRF@){ttO3INU2s(v}6C)#pnHx&=Xz{%^?x zOa8sV4;#KSC4eIJ7B^H;pbG;bS7cWuq6qZw!x@*p(~(n#C?1lynLjpz0n8~xP2~{0(fEInF;}H zLFd9mcq=*<|7Q4S_wMWc$$UdGAS?Nl0P26Zpq&iS06VtEL{MG$(WFM{dq5&&=?;j% z(F$Qbf>8qRvhQhyJV?5L$TM#85P%n0GNDzi2veiQR2|2!4j-#&kUSs@be|PMamjvr zC_UTGP5fx>Ly0AKvu>x+S8n? zkG7=7iX7ycVb13U=LN{S0eNFyfV>-!H|D$mndbqmBpv(v2WHK%*i-<`_5drfLcR4u zR)7y4swO38!*sZz0OSZh-Z$XSkDdM{NlLN~7p@N~g@s6~y8Dv*v!=rGLR|rDf`nv* z&RK`Ii;+MnED){}7F~p>0Q=r~rSkaw>ryExk%PQ4wR{z_G;I+skMq3UFTY(#5nB2f zvMFb~x=Yf7JUvKnrkHf+%k63Gl|V`WtffS!1>_Y2usEavBH$5p1H3^S7Q)>lMfHNH z2iV*n-9Q0wTVKg_ZQbC@t=n^r0wdmGcWv;Ta0^Y55ETFqkO+`sXoH}Q2})7|l|dTv z5&>1@7Tl&w`|E|G)Zc6fed@+%YXX&2$XnFxS^^@ zNC{vq#ZY%s&x#~Fj3vh=YF%Idb@ZDgw3*^^_mL%x*EoeD4HXBJc!kk|eofjbQ2IP%-0rGA@-uMsSCIuIKXrj&l0000< KMNUMnLSTY8537g( literal 0 HcmV?d00001 diff --git a/icons/obj/webbing.dmi b/icons/obj/webbing.dmi new file mode 100644 index 0000000000000000000000000000000000000000..586b4c759935f2b55945c247004fed472ccdacde GIT binary patch literal 18718 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 literal 0 HcmV?d00001 diff --git a/vanderlin.dme b/vanderlin.dme index ba359759e07..89f624f3944 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -60,6 +60,7 @@ #include "code\__DEFINES\cooldowns.dm" #include "code\__DEFINES\coven.dm" #include "code\__DEFINES\cult.dm" +#include "code\__DEFINES\danger.dm" #include "code\__DEFINES\delver.dm" #include "code\__DEFINES\devotion.dm" #include "code\__DEFINES\directional.dm" @@ -139,6 +140,7 @@ #include "code\__DEFINES\pronouns.dm" #include "code\__DEFINES\putrid.dm" #include "code\__DEFINES\qdel.dm" +#include "code\__DEFINES\quests.dm" #include "code\__DEFINES\quirks.dm" #include "code\__DEFINES\radial_defines.dm" #include "code\__DEFINES\radio.dm" @@ -266,6 +268,7 @@ #include "code\__HELPERS\config.dm" #include "code\__HELPERS\customization.dm" #include "code\__HELPERS\dates.dm" +#include "code\__HELPERS\directions.dm" #include "code\__HELPERS\dna.dm" #include "code\__HELPERS\files.dm" #include "code\__HELPERS\filters.dm" @@ -462,6 +465,7 @@ #include "code\controllers\subsystem\property_management.dm" #include "code\controllers\subsystem\radio.dm" #include "code\controllers\subsystem\randomized_travel_tiles.dm" +#include "code\controllers\subsystem\regional_threat.dm" #include "code\controllers\subsystem\roguemachine.dm" #include "code\controllers\subsystem\roguerot.dm" #include "code\controllers\subsystem\rogueroundsplayed.dm" @@ -711,6 +715,7 @@ #include "code\datums\ai\controllers\lycan.dm" #include "code\datums\ai\controllers\mimic.dm" #include "code\datums\ai\controllers\minoutaur.dm" +#include "code\datums\ai\controllers\mirespider.dm" #include "code\datums\ai\controllers\mole.dm" #include "code\datums\ai\controllers\pig.dm" #include "code\datums\ai\controllers\poltergeist.dm" @@ -740,14 +745,18 @@ #include "code\datums\ai\idle_behaviors\nothing.dm" #include "code\datums\ai\idle_behaviors\random_bum.dm" #include "code\datums\ai\idle_behaviors\random_walk.dm" +#include "code\datums\ai\subtrees\__bow_base.dm" +#include "code\datums\ai\subtrees\be_a_minion.dm" #include "code\datums\ai\subtrees\beg_for_food.dm" #include "code\datums\ai\subtrees\behemoth_abilities.dm" +#include "code\datums\ai\subtrees\bow_usage.dm" #include "code\datums\ai\subtrees\cat_attack.dm" #include "code\datums\ai\subtrees\cat_food_for_babies.dm" #include "code\datums\ai\subtrees\cat_racism.dm" #include "code\datums\ai\subtrees\cat_rest.dm" #include "code\datums\ai\subtrees\cat_territory.dm" #include "code\datums\ai\subtrees\check_environment.dm" +#include "code\datums\ai\subtrees\climb_tree.dm" #include "code\datums\ai\subtrees\colossus_ability.dm" #include "code\datums\ai\subtrees\defend_bonepile.dm" #include "code\datums\ai\subtrees\defend_leyline.dm" @@ -768,6 +777,7 @@ #include "code\datums\ai\subtrees\find_person_with_item.dm" #include "code\datums\ai\subtrees\find_weapon.dm" #include "code\datums\ai\subtrees\fishing.dm" +#include "code\datums\ai\subtrees\flank.dm" #include "code\datums\ai\subtrees\flee_nearest_target.dm" #include "code\datums\ai\subtrees\flee_target.dm" #include "code\datums\ai\subtrees\flesh_ambush.dm" @@ -776,7 +786,10 @@ #include "code\datums\ai\subtrees\flesh_melee.dm" #include "code\datums\ai\subtrees\generic_resist.dm" #include "code\datums\ai\subtrees\generic_stand.dm" +#include "code\datums\ai\subtrees\generic_wield.dm" #include "code\datums\ai\subtrees\glimmerwing.dm" +#include "code\datums\ai\subtrees\harass.dm" +#include "code\datums\ai\subtrees\human_basic_attack.dm" #include "code\datums\ai\subtrees\lay_egg.dm" #include "code\datums\ai\subtrees\leyline_abilities.dm" #include "code\datums\ai\subtrees\leyline_attack.dm" @@ -799,6 +812,7 @@ #include "code\datums\ai\subtrees\raccoon_fights.dm" #include "code\datums\ai\subtrees\random_speech.dm" #include "code\datums\ai\subtrees\retaliate_subtree.dm" +#include "code\datums\ai\subtrees\retreve_arrow.dm" #include "code\datums\ai\subtrees\self_recovery.dm" #include "code\datums\ai\subtrees\simple_find_priority.dm" #include "code\datums\ai\subtrees\simple_find_target.dm" @@ -1474,6 +1488,13 @@ #include "code\datums\stress\negative_events.dm" #include "code\datums\stress\neutral_events.dm" #include "code\datums\stress\positive_events.dm" +#include "code\datums\threat_regions\_region.dm" +#include "code\datums\threat_regions\basin.dm" +#include "code\datums\threat_regions\coast.dm" +#include "code\datums\threat_regions\mount_decap.dm" +#include "code\datums\threat_regions\north_grove.dm" +#include "code\datums\threat_regions\outer_grove.dm" +#include "code\datums\threat_regions\terror_bog.dm" #include "code\datums\world_factions\_base.dm" #include "code\datums\world_factions\coastal.dm" #include "code\datums\world_factions\mountain.dm" @@ -2095,6 +2116,12 @@ #include "code\modules\admin\view_variables\topic_basic.dm" #include "code\modules\admin\view_variables\topic_list.dm" #include "code\modules\admin\view_variables\view_variables.dm" +#include "code\modules\ambush\_ambush_config.dm" +#include "code\modules\ambush\bog_ambush.dm" +#include "code\modules\ambush\coast.dm" +#include "code\modules\ambush\mount_decap.dm" +#include "code\modules\ambush\treasure_hunters.dm" +#include "code\modules\ambush\wilderness.dm" #include "code\modules\antagonists\_common\antag_datum.dm" #include "code\modules\antagonists\_common\antag_helpers.dm" #include "code\modules\antagonists\_common\antag_hud.dm" @@ -2738,6 +2765,7 @@ #include "code\modules\fishing\tackle\lure.dm" #include "code\modules\flufftext\Dreaming.dm" #include "code\modules\food_and_drinks\food.dm" +#include "code\modules\food_and_drinks\rations.dm" #include "code\modules\food_and_drinks\food\snacks.dm" #include "code\modules\goonchat\browserOutput.dm" #include "code\modules\holiday\holidays.dm" @@ -3208,10 +3236,20 @@ #include "code\modules\mob\living\carbon\human\npc\_npc.dm" #include "code\modules\mob\living\carbon\human\npc\bum.dm" #include "code\modules\mob\living\carbon\human\npc\goblin.dm" -#include "code\modules\mob\living\carbon\human\npc\orc.dm" #include "code\modules\mob\living\carbon\human\npc\rousman.dm" -#include "code\modules\mob\living\carbon\human\npc\skeleton.dm" #include "code\modules\mob\living\carbon\human\npc\zizombies.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\bog_deserters.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\deranged.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\drow_raiders.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\highwaymen.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\northern_milita.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\searaider.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\thieves.dm" +#include "code\modules\mob\living\carbon\human\npc\human_soldiers\treasure_hunters.dm" +#include "code\modules\mob\living\carbon\human\npc\orc\_orc.dm" +#include "code\modules\mob\living\carbon\human\npc\orc\soldiers.dm" +#include "code\modules\mob\living\carbon\human\npc\skeleton\_skeleton.dm" +#include "code\modules\mob\living\carbon\human\npc\skeleton\difficulties.dm" #include "code\modules\mob\living\carbon\human\npc\species_hostile\aasimar_hostile.dm" #include "code\modules\mob\living\carbon\human\npc\species_hostile\dark_elf_hostile.dm" #include "code\modules\mob\living\carbon\human\npc\species_hostile\dwarf_hostile.dm" @@ -3317,6 +3355,7 @@ #include "code\modules\mob\living\simple_animal\hostile\retaliate\dragger.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\frog.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\gator.dm" +#include "code\modules\mob\living\simple_animal\hostile\retaliate\mirespider.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\red_dragon.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\robo_spider.dm" @@ -3415,6 +3454,21 @@ #include "code\modules\projectiles\projectile\bullets.dm" #include "code\modules\projectiles\projectile\magic.dm" #include "code\modules\projectiles\projectile\reusable\_reusable.dm" +#include "code\modules\questing\contract_machine.dm" +#include "code\modules\questing\landmarks.dm" +#include "code\modules\questing\quest_object.dm" +#include "code\modules\questing\items\parcel.dm" +#include "code\modules\questing\items\quest_scroll.dm" +#include "code\modules\questing\items\spawn_effect.dm" +#include "code\modules\questing\quests\__quest_list.dm" +#include "code\modules\questing\quests\_quest.dm" +#include "code\modules\questing\quests\courier.dm" +#include "code\modules\questing\quests\retrieval.dm" +#include "code\modules\questing\quests\kill\_kill_base.dm" +#include "code\modules\questing\quests\kill\clearout.dm" +#include "code\modules\questing\quests\kill\easy_kill.dm" +#include "code\modules\questing\quests\kill\outlaw.dm" +#include "code\modules\questing\quests\kill\raid.dm" #include "code\modules\reagents\chem_splash.dm" #include "code\modules\reagents\reagent_containers.dm" #include "code\modules\reagents\roguespill.dm" From 1e5bb4cebef4f64b6e47e59fd5ba7374cfe2a7ca Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:56:13 -0800 Subject: [PATCH 26/73] ,ompr foxes --- code/__DEFINES/ai/_ai.dm | 1 + code/datums/ai/controllers/human_npc.dm | 1 + code/datums/ai/subtrees/human_basic_attack.dm | 3 +++ .../living/carbon/human/npc/human_soldiers/deranged.dm | 8 ++++---- .../carbon/human/npc/human_soldiers/drow_raiders.dm | 6 +++--- .../living/carbon/human/npc/human_soldiers/highwaymen.dm | 2 +- .../living/carbon/human/npc/human_soldiers/searaider.dm | 6 +++--- .../mob/living/carbon/human/npc/human_soldiers/thieves.dm | 4 ++-- .../carbon/human/npc/human_soldiers/treasure_hunters.dm | 2 +- vanderlin.dme | 2 +- 10 files changed, 20 insertions(+), 15 deletions(-) diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index efe8b01cba8..ab2bbeda596 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -232,6 +232,7 @@ #define BB_HUMAN_NPC_HARASS_MODE "human_npc_harass_mode" #define BB_HUMAN_NPC_HARASS_RETREATING "human_npc_harass_retreating" #define BB_HUMAN_NPC_HARASS_COOLDOWN "human_npc_harass_cooldown" +#define BB_HUMAN_NPC_JUKE_COOLDOWN "human_npc_juke_cooldown" #define BB_BEGGING_FOOD_ITEM "item_beg_target" #define BB_ARCHER_NPC_TARGET_ARROW "archer_target_arrow" #define BB_ARCHER_NPC_STASHED_WEAPON "archer_stashed_weapon" diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 5eaba11af79..519729796c5 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -17,6 +17,7 @@ BB_HUMAN_NPC_HARASS_MODE = FALSE, // TRUE when in hit-and-run mode BB_HUMAN_NPC_HARASS_RETREATING = FALSE,// TRUE when in the back-off phase of harass BB_HUMAN_NPC_HARASS_COOLDOWN = 0, // world.time before we can dart in again + BB_HUMAN_NPC_JUKE_COOLDOWN = 0, // world.time before we can juke again ) planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index 7aa59822a9d..bb79e9b7e19 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -245,6 +245,9 @@ if(!target || !isturf(pawn.loc) || !isturf(target.loc)) return FALSE + if(world.time < pawn.ai_controller.blackboard[BB_HUMAN_NPC_JUKE_COOLDOWN]) + return FALSE + var/juke_chance = HUMAN_NPC_BASE_JUKE_CHANCE if(pawn.STASPD > HUMAN_NPC_JUKE_MIN_SPD) juke_chance += (pawn.STASPD - HUMAN_NPC_JUKE_MIN_SPD) * HUMAN_NPC_JUKE_PER_OVERSPD diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm index 00a2e99a3bb..6db3e0514cf 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm @@ -4,10 +4,10 @@ * Uses fuckoff gear that should not be looted - hence snowflake dismemberment code. */ -GLOBAL_LIST_INIT(matthios_aggro, world.file2list("strings/rt/matthiosaggrolines.txt")) -GLOBAL_LIST_INIT(zizo_aggro, world.file2list("strings/rt/zizoaggrolines.txt")) -GLOBAL_LIST_INIT(graggar_aggro, world.file2list("strings/rt/graggaraggrolines.txt")) -GLOBAL_LIST_INIT(hedgeknight_aggro, world.file2list("strings/rt/hedgeknightaggrolines.txt")) +GLOBAL_LIST_INIT(matthios_aggro, file2list("strings/rt/matthiosaggrolines.txt")) +GLOBAL_LIST_INIT(zizo_aggro, file2list("strings/rt/zizoaggrolines.txt")) +GLOBAL_LIST_INIT(graggar_aggro, file2list("strings/rt/graggaraggrolines.txt")) +GLOBAL_LIST_INIT(hedgeknight_aggro, file2list("strings/rt/hedgeknightaggrolines.txt")) /mob/living/carbon/human/species/human/northern/deranged_knight ai_controller = /datum/ai_controller/human_npc diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm index acfaa070633..e12cf1b5b3b 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm @@ -1,4 +1,4 @@ -GLOBAL_LIST_INIT(drowraider_aggro, world.file2list("strings/rt/drowaggrolines.txt")) +GLOBAL_LIST_INIT(drowraider_aggro, file2list("strings/rt/drowaggrolines.txt")) /mob/living/carbon/human/species/elf/dark/drowraider ai_controller = /datum/ai_controller/human_npc @@ -76,9 +76,9 @@ GLOBAL_LIST_INIT(drowraider_aggro, world.file2list("strings/rt/drowaggrolines.tx skin_tone = "5f5f70" if(gender == FEMALE) - real_name = pick(world.file2list("strings/rt/names/elf/elfdf.txt")) + real_name = pick(file2list("strings/rt/names/elf/elfdf.txt")) else - real_name = pick(world.file2list("strings/rt/names/elf/elfdm.txt")) + real_name = pick(file2list("strings/rt/names/elf/elfdm.txt")) faction += "spider_lowers" diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm index c2a86ffa5c9..61f522752e6 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm @@ -1,4 +1,4 @@ -GLOBAL_LIST_INIT(highwayman_aggro, world.file2list("strings/rt/highwaymanaggrolines.txt")) +GLOBAL_LIST_INIT(highwayman_aggro, file2list("strings/rt/highwaymanaggrolines.txt")) /mob/living/carbon/human/species/human/northern/highwayman ai_controller = /datum/ai_controller/human_npc diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm index 3eb56068cfa..2e3b714714b 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm @@ -1,4 +1,4 @@ -GLOBAL_LIST_INIT(searaider_aggro, world.file2list("strings/rt/searaideraggrolines.txt")) +GLOBAL_LIST_INIT(searaider_aggro, file2list("strings/rt/searaideraggrolines.txt")) /mob/living/carbon/human/species/human/northern/searaider ai_controller = /datum/ai_controller/human_npc @@ -75,9 +75,9 @@ GLOBAL_LIST_INIT(searaider_aggro, world.file2list("strings/rt/searaideraggroline organ_eyes.accessory_colors = "#336699#336699" if(gender == FEMALE) - real_name = pick(world.file2list("strings/rt/names/human/vikingf.txt")) + real_name = pick(file2list("strings/rt/names/human/vikingf.txt")) else - real_name = pick(world.file2list("strings/rt/names/human/vikingm.txt")) + real_name = pick(file2list("strings/rt/names/human/vikingm.txt")) update_body() diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm index 19525f3cd0f..e48ad98b7d2 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm @@ -68,9 +68,9 @@ organ_eyes.accessory_colors = "#336699#336699" if(gender == FEMALE) - real_name = pick(world.file2list("strings/names/first_female.txt")) + real_name = pick(file2list("strings/names/first_female.txt")) else - real_name = pick(world.file2list("strings/names/first_male.txt")) + real_name = pick(file2list("strings/names/first_male.txt")) update_body() head.sellprice = 30 diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm index 73f9c757160..dcf446be8cf 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm @@ -89,7 +89,7 @@ H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) - H.real_name = pick(world.file2list("strings/rt/names/human/mad_touched_names.txt")) + H.real_name = pick(file2list("strings/rt/names/human/mad_touched_names.txt")) /obj/item/clothing/head/menacing/mad_touched_treasure_hunter //its here so it doesnt wind up on some class' loadout. name = "sack hood" diff --git a/vanderlin.dme b/vanderlin.dme index 7c649010c69..61f36eb7496 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -812,7 +812,7 @@ #include "code\datums\ai\subtrees\raccoon_fights.dm" #include "code\datums\ai\subtrees\random_speech.dm" #include "code\datums\ai\subtrees\retaliate_subtree.dm" -#include "code\datums\ai\subtrees\retreve_arrow.dm" +#include "code\datums\ai\subtrees\retrieve_arrow.dm" #include "code\datums\ai\subtrees\self_recovery.dm" #include "code\datums\ai\subtrees\simple_find_priority.dm" #include "code\datums\ai\subtrees\simple_find_target.dm" From 26cc85c37bfa437e6c676023317ddad9ba052a0f Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:16:34 -0800 Subject: [PATCH 27/73] made it more grounded --- code/datums/ai/subtrees/harass.dm | 5 +---- code/datums/ai/subtrees/human_basic_attack.dm | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/code/datums/ai/subtrees/harass.dm b/code/datums/ai/subtrees/harass.dm index a840f4bb5f2..f2ee5693d9c 100644 --- a/code/datums/ai/subtrees/harass.dm +++ b/code/datums/ai/subtrees/harass.dm @@ -114,7 +114,7 @@ controller.ai_interact(target, TRUE, TRUE) if(pawn.next_click < world.time) - pawn.next_click = world.time + pawn.attack_speed + pawn.next_click = world.time + (pawn.used_intent?.clickcd * ( 1 + rand(0.2, 0.4))) SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) // Cooldown scales up with stamina damage and scales down with health - the more gassed/hurt, the longer we hide @@ -127,9 +127,6 @@ controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_COOLDOWN, world.time + cooldown) finish_action(controller, TRUE) -/datum/ai_behavior/human_npc_harass_strike/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key) - . = ..() - /datum/ai_behavior/human_npc_harass_retreat action_cooldown = 0.2 SECONDS behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index bb79e9b7e19..aca8a560bb6 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -75,7 +75,7 @@ controller.ai_interact(target, TRUE, TRUE) if(pawn.next_click < world.time) - pawn.next_click = world.time + (pawn.used_intent?.clickcd * 1.2) + pawn.next_click = world.time + (pawn.used_intent?.clickcd * ( 1 + rand(0.2, 0.4))) SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) if(prob(HUMAN_NPC_WEAKPOINT_SCAN_CHANCE) && isliving(target)) @@ -242,6 +242,8 @@ return FALSE if(pawn.body_position == LYING_DOWN) return FALSE + if(pawn.ai_controller.blackboard[BB_HUMAN_NPC_HARASS_MODE]) + return FALSE if(!target || !isturf(pawn.loc) || !isturf(target.loc)) return FALSE @@ -270,6 +272,7 @@ var/turf/juke_turf = pick(candidates) pawn.Move(juke_turf, get_dir(pawn, juke_turf), pawn.cached_multiplicative_slowdown) + pawn.ai_controller.set_blackboard_key(BB_HUMAN_NPC_JUKE_COOLDOWN, world.time + 1.5 SECONDS) pawn.tempfixeye = FALSE if(!was_fixedeye) pawn.fixedeye = FALSE From 5bdeb3863a0e202be94b8f851b94193b5c989829 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:30:23 -0800 Subject: [PATCH 28/73] Update human_basic_attack.dm --- code/datums/ai/subtrees/human_basic_attack.dm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index aca8a560bb6..290ce82468b 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -258,6 +258,7 @@ return FALSE pawn.tempfixeye = TRUE + pawn.atom_flags |= NO_DIR_CHANGE_ON_MOVE var/was_fixedeye = pawn.fixedeye if(!was_fixedeye) pawn.fixedeye = TRUE @@ -271,6 +272,8 @@ var/turf/juke_turf = pick(candidates) pawn.Move(juke_turf, get_dir(pawn, juke_turf), pawn.cached_multiplicative_slowdown) + pawn.atom_flags &= ~NO_DIR_CHANGE_ON_MOVE + pawn.face_atom(target) pawn.ai_controller.set_blackboard_key(BB_HUMAN_NPC_JUKE_COOLDOWN, world.time + 1.5 SECONDS) pawn.tempfixeye = FALSE From 172d44803fe77ff31ac4440ac71358bb744915c5 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:30:58 -0800 Subject: [PATCH 29/73] Update human_npc.dm --- code/datums/ai/controllers/human_npc.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 519729796c5..5bc0c7cb7de 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -29,7 +29,7 @@ /datum/ai_planning_subtree/archer_base, /datum/ai_planning_subtree/ranged_attack_subtree, /datum/ai_planning_subtree/aggro_find_target, - /datum/ai_planning_subtree/wounded_harass, + //datum/ai_planning_subtree/wounded_harass, /datum/ai_planning_subtree/squad_flank, /datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc, /datum/ai_planning_subtree/find_weapon, From f58a79cf406d8245c29a929c88bac3b05be760f3 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 12:02:20 -0800 Subject: [PATCH 30/73] Update bows.dm --- code/game/objects/items/weapons/ranged/bows.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/objects/items/weapons/ranged/bows.dm b/code/game/objects/items/weapons/ranged/bows.dm index eb874a342bb..61518a64f25 100644 --- a/code/game/objects/items/weapons/ranged/bows.dm +++ b/code/game/objects/items/weapons/ranged/bows.dm @@ -79,7 +79,7 @@ spread = 0 for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE)) var/obj/projectile/BB = CB.BB - if(!user.client || user.client?.chargedprog < 100) + if(user.client?.chargedprog < 100) var/charge_prob = 1 if(user.client) charge_prob = (user.client.chargedprog / 100) From 943447e2ad525aa94a105490926cffcd5d057324 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 12:14:03 -0800 Subject: [PATCH 31/73] undefs --- code/__DEFINES/danger.dm | 13 +++++++++++++ code/controllers/subsystem/regional_threat.dm | 14 -------------- code/datums/ai/subtrees/flank.dm | 6 ++++++ code/datums/ai/subtrees/harass.dm | 6 ++++++ code/datums/ai/subtrees/human_basic_attack.dm | 7 +++++++ 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/code/__DEFINES/danger.dm b/code/__DEFINES/danger.dm index 8791a1db713..bd5e949564c 100644 --- a/code/__DEFINES/danger.dm +++ b/code/__DEFINES/danger.dm @@ -9,3 +9,16 @@ #define DANGER_DANGEROUS_LIMIT 40 #define DANGER_DIRE_FLOOR 41 #define DANGER_DIRE_LIMIT 60 + +#define DANGER_LEVEL_SAFE "Safe" +#define DANGER_LEVEL_LOW "Low" +#define DANGER_LEVEL_MODERATE "Moderate" +#define DANGER_LEVEL_DANGEROUS "Dangerous" +#define DANGER_LEVEL_BLEAK "Bleak" + +#define THREAT_REGION_BASIN "Basin" +#define THREAT_REGION_NORTHERN_GROVE "Northern Grove" +#define THREAT_REGION_OUTER_GROVE "Outer Grove" // Grove west of the road +#define THREAT_REGION_MOUNT_DECAP "Mount Decapitation" +#define THREAT_REGION_TERRORBOG "Terrorbog" +#define THREAT_REGION_COAST "Coast" diff --git a/code/controllers/subsystem/regional_threat.dm b/code/controllers/subsystem/regional_threat.dm index 6b779d1b2cd..b7c117e7e0c 100644 --- a/code/controllers/subsystem/regional_threat.dm +++ b/code/controllers/subsystem/regional_threat.dm @@ -1,17 +1,3 @@ -// Danger levels. Each danger level is defined as an ambush that can happen. Every time this fire, this number iterates. -#define DANGER_LEVEL_SAFE "Safe" -#define DANGER_LEVEL_LOW "Low" -#define DANGER_LEVEL_MODERATE "Moderate" -#define DANGER_LEVEL_DANGEROUS "Dangerous" -#define DANGER_LEVEL_BLEAK "Bleak" - -#define THREAT_REGION_BASIN "Basin" -#define THREAT_REGION_NORTHERN_GROVE "Northern Grove" -#define THREAT_REGION_OUTER_GROVE "Outer Grove" // Grove west of the road -#define THREAT_REGION_MOUNT_DECAP "Mount Decapitation" -#define THREAT_REGION_TERRORBOG "Terrorbog" -#define THREAT_REGION_COAST "Coast" - SUBSYSTEM_DEF(regionthreat) name = "Regional Threat" wait = 15 MINUTES diff --git a/code/datums/ai/subtrees/flank.dm b/code/datums/ai/subtrees/flank.dm index 75d0b1ef4d2..05f4908ff83 100644 --- a/code/datums/ai/subtrees/flank.dm +++ b/code/datums/ai/subtrees/flank.dm @@ -127,3 +127,9 @@ return finish_action(controller, FALSE) + +#undef FLANK_RADIUS +#undef FLANK_MIN_SEPARATION +#undef FLANK_ENGAGE_DIST +#undef FLANK_ATTACK_CHANCE +#undef FLANK_RECHECK_INTERVAL diff --git a/code/datums/ai/subtrees/harass.dm b/code/datums/ai/subtrees/harass.dm index f2ee5693d9c..e2263079847 100644 --- a/code/datums/ai/subtrees/harass.dm +++ b/code/datums/ai/subtrees/harass.dm @@ -156,3 +156,9 @@ if(!move_target || get_dist(pawn, move_target) <= 1) finish_action(controller, TRUE) return. + +#undef HARASS_HEALTH_THRESHOLD +#undef HARASS_STAMINA_THRESHOLD +#undef HARASS_RETREAT_DIST +#undef HARASS_BASE_COOLDOWN +#undef HARASS_STAMINA_COOLDOWN_SCALE diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index 290ce82468b..267375c31a3 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -322,3 +322,10 @@ if(BCLASS_BURN) return "fire" return "blunt" // safest fallback - everything has some blunt resistance defined + +#undef HUMAN_NPC_BASE_JUKE_CHANCE +#undef HUMAN_NPC_JUKE_MIN_SPD +#undef HUMAN_NPC_JUKE_PER_OVERSPD +#undef HUMAN_NPC_MAX_ATTACK_STAMINA +#undef HUMAN_NPC_WEAKPOINT_SCAN_CHANCE +#undef HUMAN_NPC_WEAKPOINT_CACHE_DURATION From 4a93be419c89cde2b9b39be90f2131c73bbb7757 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:46:17 -0800 Subject: [PATCH 32/73] inventory aware npcs currently limited to healing drinks and bandages. --- code/__DEFINES/ai/_ai.dm | 21 ++ code/__DEFINES/components.dm | 2 +- code/datums/ai/_ai_controller.dm | 61 ++-- code/datums/ai/controllers/human_npc.dm | 4 + code/datums/ai/subtrees/bow_usage.dm | 3 +- code/datums/ai/subtrees/human_basic_attack.dm | 4 +- code/datums/ai/subtrees/use_bandage.dm | 65 ++++ code/datums/ai/subtrees/use_healing_drink.dm | 68 ++++ code/datums/components/aggro_board.dm | 4 +- code/datums/components/inventory_manager.dm | 344 ++++++++++++++++++ code/datums/elements/interrupt_on_damage.dm | 42 +++ code/game/objects/items.dm | 3 + code/game/objects/items/cigs_lighters.dm | 4 + code/game/objects/items/natural/cloth.dm | 1 + .../objects/items/natural/clothfibersthorn.dm | 1 + .../living/carbon/human/npc/orc/soldiers.dm | 1 + .../reagent_containers/bottles/_bottle.dm | 5 +- .../reagent_containers/bottles/alchemy.dm | 2 + .../reagents/reagent_containers/powder.dm | 1 + vanderlin.dme | 4 + 20 files changed, 607 insertions(+), 33 deletions(-) create mode 100644 code/datums/ai/subtrees/use_bandage.dm create mode 100644 code/datums/ai/subtrees/use_healing_drink.dm create mode 100644 code/datums/components/inventory_manager.dm create mode 100644 code/datums/elements/interrupt_on_damage.dm diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index ab2bbeda596..39196d59db6 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -240,6 +240,11 @@ #define BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY "archer_npc_equipment_cache_expiry" #define BB_ARCHER_NPC_BOW "archer_npc_bow" #define BB_ARCHER_NPC_QUIVER "archer_npc_quiver" +#define BB_INVENTORY_MAP "inventory_map" // list(category = list(item_ref = slot_name)) +#define BB_CONTAINER_REFS "container_refs" // list(slot_name = item_ref) +#define BB_INVENTORY_DIRTY "inventory_dirty" // bool, triggers reappraisal +#define BB_HELD_CONSUMABLE "held_consumable" // item we pulled out to use +#define BB_TARGET_ZONE_OVERRIDE "bb_target_override" #define ARCHER_NPC_EQUIPMENT_CACHE_TIME (40 SECONDS) #define ARCHER_NPC_MIN_RANGE 3 // tiles - closer than this, prefer melee @@ -320,3 +325,19 @@ #define ACTION_STATE_CONTINUE 1 #define ACTION_STATE_COMPLETE 2 #define ACTION_STATE_FAILED 3 + +#define AI_ITEM_BANDAGE (1<<0) // stops bleeding, applied to self/others +#define AI_ITEM_HEALING_DRINK (1<<1) // drinkable healing reagent container +#define AI_ITEM_FOOD (1<<2) // edible +#define AI_ITEM_POWDER (1<<3) // snortable /obj/item/reagent_containers/powder +#define AI_ITEM_KEY (1<<4) +#define AI_ITEM_TOOL (1<<5) +#define AI_ITEM_AMMO (1<<6) +#define AI_ITEM_GRENADE (1<<7) +#define AI_ITEM_MELEE (1<<8) +#define AI_ITEM_GUN (1<<9) +#define AI_ITEM_DRINK (1<<10) // generic drinkable (not necessarily healing) + +#define AI_INVENTORY_WATCHED_SLOTS (ITEM_SLOT_BELT | ITEM_SLOT_BACK_L | ITEM_SLOT_BACK_R | \ + ITEM_SLOT_BELT_L | ITEM_SLOT_BELT_R | ITEM_SLOT_ARMOR | ITEM_SLOT_PANTS | \ + ITEM_SLOT_SHIRT | ITEM_SLOT_CLOAK | ITEM_SLOT_BACK) diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index fbe584351c8..5cf6be3365f 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -167,7 +167,7 @@ #define COMSIG_MACHINERY_POWER_LOST "machinery_power_lost" //from base power_change() when power is lost #define COMSIG_MACHINERY_POWER_RESTORED "machinery_power_restored" //from base power_change() when power is restored - +#define COMSIG_MOB_DROPITEM "mob_dropitem" /// A mob has just equipped an item. Called on [/mob] from base of [/obj/item/equipped()]: (/obj/item/equipped_item, slot) #define COMSIG_MOB_EQUIPPED_ITEM "mob_equipped_item" /// A mob has just unequipped an item. diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 493fd210856..b27e47c17ec 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -168,7 +168,7 @@ have ways of interacting with a specific atom and control it. They posses a blac return !QDELETED(pawn) ///Interact with objects -/datum/ai_controller/proc/ai_interact(target, combat_mode, nextmove = FALSE, list/modifiers) +/datum/ai_controller/proc/ai_interact(target, combat_mode, nextmove = FALSE, list/modifiers, maintain_position = FALSE) if(!ai_can_interact()) return FALSE @@ -181,10 +181,11 @@ have ways of interacting with a specific atom and control it. They posses a blac if(nextmove && living_pawn.next_move > world.time) return FALSE - if(living_pawn.body_position == LYING_DOWN) - living_pawn.aimheight_change(rand(1,9)) - else - living_pawn.aimheight_change(rand(10,19)) + if(!maintain_position) + if(living_pawn.body_position == LYING_DOWN) + living_pawn.aimheight_change(rand(1,9)) + else + living_pawn.aimheight_change(rand(10,19)) var/params = list2params(modifiers) @@ -513,7 +514,14 @@ have ways of interacting with a specific atom and control it. They posses a blac behavior_args -= behavior_type SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments) +/datum/ai_controller/proc/get_inventory() + RETURN_TYPE(/datum/component/ai_inventory_manager) + return pawn?.GetComponent(/datum/component/ai_inventory_manager) + /datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) + var/mob/living/liver = pawn + if(liver.doing()) + return var/list/arguments = list(delta_time, src) var/list/stored_arguments = behavior_args[behavior.type] if(stored_arguments) @@ -788,25 +796,30 @@ have ways of interacting with a specific atom and control it. They posses a blac while(index <= length(remove_queue)) var/list/next_to_clear = remove_queue[index] for(var/inner_value in next_to_clear) - var/associated_value = next_to_clear[inner_value] - // We are a lists of lists, add the next value to the queue so we can handle references in there - // (But we only need to bother checking the list if it's not empty.) - if(islist(inner_value) && length(inner_value)) - UNTYPED_LIST_ADD(remove_queue, inner_value) - - // We found the value that's been deleted. Clear it out from this list - else if(inner_value == source) - next_to_clear -= inner_value - - // We are an assoc lists of lists, the list at the next value so we can handle references in there - // (But again, we only need to bother checking the list if it's not empty.) - if(islist(associated_value) && length(associated_value)) - UNTYPED_LIST_ADD(remove_queue, associated_value) - - // We found the value that's been deleted, it was an assoc value. Clear it out entirely - else if(associated_value == source) - next_to_clear -= inner_value - SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) + if(isnum(inner_value)) + if(inner_value == source) + next_to_clear -= inner_value + SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) + else + var/associated_value = next_to_clear[inner_value] + // We are a lists of lists, add the next value to the queue so we can handle references in there + // (But we only need to bother checking the list if it's not empty.) + if(islist(inner_value) && length(inner_value)) + UNTYPED_LIST_ADD(remove_queue, inner_value) + + // We found the value that's been deleted. Clear it out from this list + else if(inner_value == source) + next_to_clear -= inner_value + + // We are an assoc lists of lists, the list at the next value so we can handle references in there + // (But again, we only need to bother checking the list if it's not empty.) + if(islist(associated_value) && length(associated_value)) + UNTYPED_LIST_ADD(remove_queue, associated_value) + + // We found the value that's been deleted, it was an assoc value. Clear it out entirely + else if(associated_value == source) + next_to_clear -= inner_value + SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) index += 1 diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 5bc0c7cb7de..6b433c142e8 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -21,6 +21,8 @@ ) planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/use_bandage, + /datum/ai_planning_subtree/use_healing_drink, /datum/ai_planning_subtree/generic_wield, /datum/ai_planning_subtree/generic_resist, /datum/ai_planning_subtree/generic_stand, @@ -43,6 +45,8 @@ var/mob/living/living_pawn = new_pawn RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed)) movement_delay = living_pawn.cached_multiplicative_slowdown + new_pawn.AddComponent(/datum/component/ai_inventory_manager) + new_pawn.AddElement(/datum/element/interrupt_on_damage) /datum/ai_controller/human_npc/UnpossessPawn(destroy) UnregisterSignal(pawn, list( diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm index 3447fe9802e..671c78bd956 100644 --- a/code/datums/ai/subtrees/bow_usage.dm +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -18,7 +18,6 @@ return SUBTREE_RETURN_FINISH_PLANNING /datum/ai_behavior/ranged_attack_bow - // No REQUIRE_REACH — we want to stay at range behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION action_cooldown = 0.2 SECONDS @@ -101,7 +100,7 @@ finish_action(controller, FALSE, target_key) return - // Break off if target closed to melee range — let melee tree handle it + // Break off if target closed to melee range if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) finish_action(controller, FALSE, target_key) return diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index 267375c31a3..3af535162e1 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -148,7 +148,7 @@ if(!part) continue - // Wound exploitation — requires trained eye AND good perception + //requires trained eye AND good perception if(skill_level >= SKILL_LEVEL_JOURNEYMAN && pawn.STAPER >= 10) if(part.brute_dam > 20 || part.burn_dam > 20) wounded += part.body_zone @@ -195,7 +195,7 @@ return // Skill scales how long the targeting solution stays valid - // Reach also matters — longer weapons can maintain solutions longer + //longer weapons can maintain solutions longer // since the fighter isn't scrambling to stay close var/cache_duration = HUMAN_NPC_WEAKPOINT_CACHE_DURATION switch(skill_level) diff --git a/code/datums/ai/subtrees/use_bandage.dm b/code/datums/ai/subtrees/use_bandage.dm new file mode 100644 index 00000000000..226f3aa1bb4 --- /dev/null +++ b/code/datums/ai/subtrees/use_bandage.dm @@ -0,0 +1,65 @@ +/datum/ai_planning_subtree/use_bandage + +/datum/ai_planning_subtree/use_bandage/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(controller.blackboard[BB_HELD_CONSUMABLE] || controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + var/real = FALSE + var/mob/living/carbon/human/human_pawn = controller.pawn + for(var/obj/item/bodypart/bodypart as anything in human_pawn.bodyparts) + if((length(bodypart.wounds) || length(bodypart.embedded_objects)) && !bodypart.bandage) + real = TRUE + controller.set_blackboard_key(BB_TARGET_ZONE_OVERRIDE, bodypart.body_zone) + break + + if(!real) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/bandage = inv.get_item(AI_ITEM_BANDAGE) + if(!bandage) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, bandage) + controller.queue_behavior(/datum/ai_behavior/apply_bandage, BB_HELD_CONSUMABLE) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/apply_bandage + action_cooldown = 30 SECONDS + +/datum/ai_behavior/apply_bandage/perform(delta_time, datum/ai_controller/controller, consumable_key) + var/obj/item/bandage = controller.blackboard[consumable_key] + if(!bandage) + finish_action(controller, FALSE, consumable_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + if(H.get_active_held_item() != bandage) + var/obj/item/usable = inv?.draw_usable_item(bandage, AI_ITEM_BANDAGE) + if(!usable) + finish_action(controller, FALSE, consumable_key) + return + + // Cache the extracted item so finish_action can clean it up + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + + var/old_zone = H.zone_selected + H.zone_selected = controller.blackboard[BB_TARGET_ZONE_OVERRIDE] + controller.ai_interact(H, maintain_position = TRUE) + controller.clear_blackboard_key(BB_TARGET_ZONE_OVERRIDE) + H.zone_selected = old_zone + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/apply_bandage/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_BANDAGE)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + + inv?.restore_hands() diff --git a/code/datums/ai/subtrees/use_healing_drink.dm b/code/datums/ai/subtrees/use_healing_drink.dm new file mode 100644 index 00000000000..256135e0b88 --- /dev/null +++ b/code/datums/ai/subtrees/use_healing_drink.dm @@ -0,0 +1,68 @@ +/datum/ai_planning_subtree/use_healing_drink/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/mob/living/carbon/human/H = controller.pawn + if(H.getBruteLoss() < 20 && H.getFireLoss() < 20) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + // Find a drink that actually has reagents, purge empties along the way + var/obj/item/reagent_containers/drink = null + for(var/obj/item/reagent_containers/candidate as anything in inv.inventory_map[AI_ITEM_HEALING_DRINK]) + if(!candidate.reagents?.total_volume) + inv.drop_empty_container(candidate) + continue + drink = candidate + break + + if(!drink) + return + + controller.set_blackboard_key(BB_HELD_CONSUMABLE, drink) + controller.queue_behavior(/datum/ai_behavior/consume_healing_drink, BB_HELD_CONSUMABLE) + +/datum/ai_behavior/consume_healing_drink + action_cooldown = 70 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/consume_healing_drink/perform(delta_time, datum/ai_controller/controller, consumable_key) + var/obj/item/reagent_containers/glass/bottle/drink = controller.blackboard[consumable_key] + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + // may have been used since queued + if(!drink || !drink.reagents?.total_volume) + if(drink) + inv?.drop_empty_container(drink) + finish_action(controller, FALSE, consumable_key) + return + + if(!inv?.draw_item(drink, AI_ITEM_HEALING_DRINK)) + finish_action(controller, FALSE, consumable_key) + return + + if(!drink.canconsume(H, H)) + finish_action(controller, FALSE, consumable_key) + return + + if(drink.closed) + drink.toggle_cork(H, FALSE) + + drink.attack(H, H, list()) + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/consume_healing_drink/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + // Check if what's now in hand is the drink we used and it's empty + var/obj/item/reagent_containers/held = H.get_active_held_item() + if(istype(held, /obj/item/reagent_containers)) + if(!held.reagents?.total_volume) + inv?.drop_empty_container(held) + else + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() + controller.clear_blackboard_key(consumable_key) diff --git a/code/datums/components/aggro_board.dm b/code/datums/components/aggro_board.dm index f8bc9851749..d206568be3b 100644 --- a/code/datums/components/aggro_board.dm +++ b/code/datums/components/aggro_board.dm @@ -58,7 +58,7 @@ var/list/aggro_table = living_mob.ai_controller.blackboard[BB_MOB_AGGRO_TABLE] if(!length(aggro_table)) add_threat(living_mob, target, amount) - var/aggro = aggro_table[living_mob] + var/aggro = aggro_table[target] if(aggro >= cap) return amount -= aggro @@ -121,7 +121,7 @@ /// Periodically decays threat levels /datum/component/ai_aggro_system/process() - var/decay_amount = default_decay_rate * 10 + var/decay_amount = default_decay_rate var/mob/living/living_mob = parent if(!living_mob?.ai_controller) return diff --git a/code/datums/components/inventory_manager.dm b/code/datums/components/inventory_manager.dm new file mode 100644 index 00000000000..0c960a3088d --- /dev/null +++ b/code/datums/components/inventory_manager.dm @@ -0,0 +1,344 @@ +/datum/component/ai_inventory_manager + /// list(ai_item_category_flag = list(obj/item = slot_bitflag)) + var/alist/inventory_map + + /// list(slot_bitflag = obj/item), only slots containing a storage container + var/alist/container_refs + + /// Cached flat list of all slot bitflags to iterate, built once + var/static/alist/all_slot_flags + + /// What was in each hand before we drew a consumable, keyed by hand slot flag + var/obj/item/cached_inactive_hand + var/obj/item/cached_active_hand + +/datum/component/ai_inventory_manager/Initialize(mapload) + if(!iscarbon(parent)) + return COMPONENT_INCOMPATIBLE + + _build_slot_flag_list() + + inventory_map = alist( + AI_ITEM_BANDAGE = list(), + AI_ITEM_HEALING_DRINK = list(), + AI_ITEM_FOOD = list(), + AI_ITEM_DRINK = list(), + AI_ITEM_POWDER = list(), + AI_ITEM_KEY = list(), + AI_ITEM_TOOL = list(), + AI_ITEM_AMMO = list(), + AI_ITEM_GRENADE = list(), + AI_ITEM_MELEE = list(), + AI_ITEM_GUN = list(), + ) + container_refs = alist() + + RegisterSignal(parent, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equip)) + RegisterSignal(parent, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_unequip)) + RegisterSignal(parent, COMSIG_MOB_DROPITEM, PROC_REF(on_drop)) + + full_reappraise() + +/datum/component/ai_inventory_manager/Destroy() + for(var/slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + container_refs = null + inventory_map = null + return ..() + +/datum/component/ai_inventory_manager/proc/_build_slot_flag_list() + if(all_slot_flags) + return + all_slot_flags = alist() + for(var/i in 0 to SLOTS_AMT - 1) + var/flag = (1 << i) + if(flag & AI_INVENTORY_WATCHED_SLOTS) + all_slot_flags += flag + +/datum/component/ai_inventory_manager/proc/full_reappraise() + var/mob/living/carbon/human/H = parent + + for(var/slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + container_refs = alist() + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + inventory_map[cat] = list() + + for(var/slot_flag in all_slot_flags) + var/obj/item/candidate = H.get_item_by_slot(slot_flag) + if(!candidate) + continue + _try_register_container(slot_flag, candidate) + if(!candidate.GetComponent(/datum/component/storage)) + _classify_item(candidate, slot_flag) + + for(var/slot_flag in container_refs) + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + if(STR) + _appraise_storage(STR, slot_flag) + +/// Scan inside a single storage component and classify contents +/datum/component/ai_inventory_manager/proc/_appraise_storage(datum/component/storage/STR, slot_flag) + for(var/obj/item/it in STR.contents()) + _classify_item(it, slot_flag) + +/// Register a container slot and watch it for deletion +/datum/component/ai_inventory_manager/proc/_try_register_container(slot_flag, obj/item/candidate) + if(!candidate.GetComponent(/datum/component/storage)) + return + container_refs[slot_flag] = candidate + RegisterSignal(candidate, COMSIG_PARENT_QDELETING, PROC_REF(on_container_delete), override = TRUE) + +/// Classify a single item into all matching categories +/datum/component/ai_inventory_manager/proc/_classify_item(obj/item/it, slot_flag) + RegisterSignal(it, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), override = TRUE) + if(it.flags_ai_inventory & AI_ITEM_BANDAGE) + inventory_map[AI_ITEM_BANDAGE][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_HEALING_DRINK) + inventory_map[AI_ITEM_HEALING_DRINK][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_FOOD) + inventory_map[AI_ITEM_FOOD][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_DRINK) + inventory_map[AI_ITEM_DRINK][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_POWDER) + inventory_map[AI_ITEM_POWDER][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_KEY) + inventory_map[AI_ITEM_KEY][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_TOOL) + inventory_map[AI_ITEM_TOOL][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_AMMO) + inventory_map[AI_ITEM_AMMO][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_GRENADE) + inventory_map[AI_ITEM_GRENADE][it] = slot_flag + if(it.flags_ai_inventory & AI_ITEM_MELEE) + inventory_map[AI_ITEM_MELEE][it] = slot_flag + if(isgun(it)) + inventory_map[AI_ITEM_GUN][it] = slot_flag + +/datum/component/ai_inventory_manager/proc/on_equip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + if(!(slot & AI_INVENTORY_WATCHED_SLOTS)) + return + // Partial rescan: just this slot + _purge_slot(slot) + _try_register_container(slot, equipment) + if(equipment.GetComponent(/datum/component/storage)) + var/datum/component/storage/STR = equipment.GetComponent(/datum/component/storage) + _appraise_storage(STR, slot) + else + _classify_item(equipment, slot) + +/datum/component/ai_inventory_manager/proc/on_unequip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + if(!(slot & AI_INVENTORY_WATCHED_SLOTS)) + return + _purge_slot(slot) + if(slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + container_refs -= slot + +/datum/component/ai_inventory_manager/proc/on_drop(datum/source, obj/item/dropped) + SIGNAL_HANDLER + _remove_item(dropped) + +/datum/component/ai_inventory_manager/proc/on_item_delete(datum/source, force) + SIGNAL_HANDLER + UnregisterSignal(source, COMSIG_PARENT_QDELETING) + _remove_item(source) + +/datum/component/ai_inventory_manager/proc/on_container_delete(datum/source, force) + SIGNAL_HANDLER + for(var/slot_flag in container_refs) + if(container_refs[slot_flag] == source) + _purge_slot(slot_flag) + container_refs -= slot_flag + return + +/// Remove all inventory_map entries for a given slot bitflag +/datum/component/ai_inventory_manager/proc/_purge_slot(slot_flag) + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + if(inventory_map[cat][it] == slot_flag) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + inventory_map[cat] -= it + +/datum/component/ai_inventory_manager/proc/_remove_item(obj/item/it) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + for(var/cat in inventory_map) + if(it in inventory_map[cat]) + inventory_map[cat] -= it + +/datum/component/ai_inventory_manager/proc/get_item(category) + RETURN_TYPE(/obj/item) + var/list/cat = inventory_map[category] + if(!length(cat)) + return null + return cat[1] + +/datum/component/ai_inventory_manager/proc/get_item_slot(obj/item/it, category) + return inventory_map[category]?[it] + +/datum/component/ai_inventory_manager/proc/find_space_for(obj/item/it) + for(var/slot_flag in container_refs) + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(STR?.can_be_inserted(it, stop_messages = TRUE)) + return slot_flag + return 0 + +/datum/component/ai_inventory_manager/proc/draw_item(obj/item/it, category) + var/mob/living/carbon/human/H = parent + + cached_active_hand = H.get_active_held_item() + cached_inactive_hand = H.get_inactive_held_item() + + if(istype(cached_active_hand, /obj/item/offhand)) + var/datum/component/two_handed/twohanded = cached_inactive_hand.GetComponent(/datum/component/two_handed) + twohanded.unwield(H) + cached_active_hand = null + if(istype(cached_inactive_hand, /obj/item/offhand)) + var/datum/component/two_handed/twohanded = cached_active_hand.GetComponent(/datum/component/two_handed) + twohanded.unwield(H) + cached_inactive_hand = null + + if(!_make_hand_free()) + return FALSE + + var/slot_flag = get_item_slot(it, category) + if(!slot_flag) + return FALSE + var/obj/item/container = container_refs[slot_flag] + if(!container) + return FALSE + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + if(!STR) + return FALSE + STR.remove_from_storage(it, H) + return H.put_in_active_hand(it) + +/datum/component/ai_inventory_manager/proc/restore_hands() + if(!cached_active_hand && !cached_inactive_hand) + return + var/mob/living/carbon/human/H = parent + + var/obj/item/active = H.get_active_held_item() + var/obj/item/inactive = H.get_inactive_held_item() + + // Snapshot and clear FIRST to prevent reentrant calls from re-running + var/obj/item/want_active = cached_active_hand + var/obj/item/want_inactive = cached_inactive_hand + cached_active_hand = null + cached_inactive_hand = null + + if(active && active != want_active && active != want_inactive) + if(!stow_item(active)) + H.dropItemToGround(active) + + if(inactive && inactive != want_active && inactive != want_inactive) + if(!stow_item(inactive)) + H.dropItemToGround(inactive) + + if(want_active && !H.get_active_held_item()) + H.put_in_active_hand(want_active) + + if(want_inactive && !H.get_inactive_held_item()) + H.swap_hand() + H.put_in_active_hand(want_inactive) + H.swap_hand() + +/datum/component/ai_inventory_manager/proc/_make_hand_free() + var/mob/living/carbon/human/H = parent + if(!H.get_active_held_item()) + return TRUE + H.swap_hand() + if(!H.get_active_held_item()) + return TRUE + var/obj/item/blocking = H.get_active_held_item() + if(stow_item(blocking)) + return TRUE + H.dropItemToGround(blocking) + return TRUE + +/datum/component/ai_inventory_manager/proc/stow_item(obj/item/it) + var/mob/living/carbon/human/H = parent + if(it.loc != H) + return FALSE + var/slot_flag = find_space_for(it) + if(!slot_flag) + return FALSE + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + STR.handle_item_insertion(it, prevent_warning = TRUE, user = H) + _classify_item(it, slot_flag) + return TRUE + +/// Remove an empty container from inventory tracking and drop it on the ground +/datum/component/ai_inventory_manager/proc/drop_empty_container(obj/item/reagent_containers/container) + var/mob/living/carbon/human/H = parent + _remove_item(container) + + // If it's in storage, pull it out and drop it + if(container.loc != H) + for(var/slot_flag in container_refs) + var/obj/item/storage_item = container_refs[slot_flag] + var/datum/component/storage/STR = storage_item?.GetComponent(/datum/component/storage) + if(!STR) + continue + if(container in STR.contents()) + STR.remove_from_storage(container, H) + break + + if(container.loc == H) + H.dropItemToGround(container) + +/// Returns the actual usable item (may differ from what's in inventory_map) +/datum/component/ai_inventory_manager/proc/draw_usable_item(obj/item/it, category) + var/mob/living/carbon/human/H = parent + + if(istype(it, /obj/item/natural/bundle)) + var/obj/item/natural/bundle/bundle = it + + if(!bundle.stacktype || bundle.amount <= 0) + return null + + // by spawning the stacktype item and putting it in hand (we don't use the actual handler because of random npc bs) + if(!_make_hand_free()) + return null + + var/turf/T = get_turf(H) + var/obj/item/extracted = new bundle.stacktype(T) + + if(!H.put_in_active_hand(extracted)) + qdel(extracted) + return null + + if(bundle.amount == 1) + _remove_item(bundle) + var/slot_flag = null + for(var/sf in container_refs) + var/obj/slot = container_refs[sf] + var/datum/component/storage/STR = slot?.GetComponent(/datum/component/storage) + if(STR && (bundle in STR.contents())) + slot_flag = sf + break + if(slot_flag) + var/obj/item = container_refs[slot_flag] + var/datum/component/storage/STR = item?.GetComponent(/datum/component/storage) + STR?.remove_from_storage(bundle, H) + H.dropItemToGround(bundle) + qdel(bundle) + else + bundle.amount-- + bundle.update_bundle() + + return extracted + + if(!draw_item(it, category)) + return null + return it diff --git a/code/datums/elements/interrupt_on_damage.dm b/code/datums/elements/interrupt_on_damage.dm new file mode 100644 index 00000000000..e9dd8f59dfa --- /dev/null +++ b/code/datums/elements/interrupt_on_damage.dm @@ -0,0 +1,42 @@ +/datum/element/interrupt_on_damage + element_flags = ELEMENT_BESPOKE + id_arg_index = 2 + +/datum/element/interrupt_on_damage/Attach(mob/living/target) + . = ..() + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + target.AddElement(/datum/element/relay_attackers) + RegisterSignal(target, COMSIG_DO_AFTER_BEGAN, PROC_REF(on_do_after_begin)) + +/datum/element/interrupt_on_damage/Detach(mob/living/target) + . = ..() + target.RemoveElement(/datum/element/relay_attackers) + UnregisterSignal(target, list( + COMSIG_DO_AFTER_BEGAN, + COMSIG_DO_AFTER_ENDED, + COMSIG_ATOM_WAS_ATTACKED, + )) + +/datum/element/interrupt_on_damage/proc/on_do_after_begin(mob/living/source) + SIGNAL_HANDLER + RegisterSignal(source, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked), override = TRUE) + RegisterSignal(source, COMSIG_DO_AFTER_ENDED, PROC_REF(on_do_after_end), override = TRUE) + +/datum/element/interrupt_on_damage/proc/on_do_after_end(mob/living/source) + SIGNAL_HANDLER + UnregisterSignal(source, list( + COMSIG_ATOM_WAS_ATTACKED, + COMSIG_DO_AFTER_ENDED, + )) + +/datum/element/interrupt_on_damage/proc/on_attacked(mob/living/source, atom/attacker, damage) + SIGNAL_HANDLER + if(!damage) + return + UnregisterSignal(source, list( + COMSIG_ATOM_WAS_ATTACKED, + COMSIG_DO_AFTER_ENDED, + )) + source.stop_all_doing() + on_do_after_end(source) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index a82c3f013ba..69fd5d88ac3 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -31,6 +31,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e var/inhand_x_dimension = 64 var/inhand_y_dimension = 64 + var/flags_ai_inventory = NONE + var/no_effect = FALSE max_integrity = 200 @@ -825,6 +827,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e animate(src, pixel_y = oldy, time = 0.5) item_flags &= ~IN_INVENTORY SEND_SIGNAL(src, COMSIG_ITEM_DROPPED,user) + SEND_SIGNAL(user, COMSIG_MOB_DROPITEM,src) if(!silent) playsound(src, drop_sound, DROP_SOUND_VOLUME, TRUE, ignore_walls = FALSE) toggle_altgrip(user, FALSE) diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm index 02de9947151..c547780e7e5 100644 --- a/code/game/objects/items/cigs_lighters.dm +++ b/code/game/objects/items/cigs_lighters.dm @@ -160,6 +160,10 @@ CIGARETTE PACKETS ARE IN FANCY.DM create_reagents(chem_volume, INJECTABLE | NO_REACT) if(list_reagents) reagents.add_reagent_list(list_reagents) + for(var/datum/reagent/reagent_type as anything in list_reagents) + if(ispath(reagent_type, /datum/reagent/medicine)) + flags_ai_inventory |= AI_ITEM_HEALING_DRINK + if(starts_lit) light() AddComponent(/datum/component/knockoff, 90, list(BODY_ZONE_PRECISE_MOUTH) ,list(ITEM_SLOT_MOUTH))//90% to knock off when wearing a mask diff --git a/code/game/objects/items/natural/cloth.dm b/code/game/objects/items/natural/cloth.dm index 4c2cac7f90c..90237523124 100644 --- a/code/game/objects/items/natural/cloth.dm +++ b/code/game/objects/items/natural/cloth.dm @@ -15,6 +15,7 @@ w_class = WEIGHT_CLASS_TINY spitoutmouth = FALSE bundletype = /obj/item/natural/bundle/cloth + flags_ai_inventory = AI_ITEM_BANDAGE var/datum/component/cleaner/cleaner_component = null var/clean_speed = 0.4 SECONDS diff --git a/code/game/objects/items/natural/clothfibersthorn.dm b/code/game/objects/items/natural/clothfibersthorn.dm index 222c11a85fc..17a11eac8ae 100644 --- a/code/game/objects/items/natural/clothfibersthorn.dm +++ b/code/game/objects/items/natural/clothfibersthorn.dm @@ -143,6 +143,7 @@ icon1step = 5 icon2 = "clothroll2" icon2step = 10 + flags_ai_inventory = AI_ITEM_BANDAGE /obj/item/natural/bundle/cloth/full/Initialize() . = ..() diff --git a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm index 1e316b20ba0..f02e905ac1f 100644 --- a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm +++ b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm @@ -172,6 +172,7 @@ shirt = /obj/item/clothing/armor/gambeson/light pants = /obj/item/clothing/pants/trou/leather belt = /obj/item/storage/belt/leather + beltl = /obj/item/storage/belt/pouch/medicine H.base_strength = 14 // GAGGER GAGGER GAGGER H.base_speed = 10 // Fast, for an orc H.base_constitution = 12 diff --git a/code/modules/reagents/reagent_containers/bottles/_bottle.dm b/code/modules/reagents/reagent_containers/bottles/_bottle.dm index f81edb41026..7933cabd2ce 100644 --- a/code/modules/reagents/reagent_containers/bottles/_bottle.dm +++ b/code/modules/reagents/reagent_containers/bottles/_bottle.dm @@ -86,9 +86,10 @@ GLOBAL_LIST_INIT(wisdoms, file2list("strings/rt/wisdoms.txt")) toggle_cork(user) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN -/obj/item/reagent_containers/glass/bottle/proc/toggle_cork(mob/user) +/obj/item/reagent_containers/glass/bottle/proc/toggle_cork(mob/user, modify_nextmove = TRUE) closed = !closed - user.changeNext_move(CLICK_CD_RAPID) + if(modify_nextmove) + user.changeNext_move(CLICK_CD_RAPID) if(closed) reagent_flags &= ~TRANSFERABLE reagents.flags = reagent_flags diff --git a/code/modules/reagents/reagent_containers/bottles/alchemy.dm b/code/modules/reagents/reagent_containers/bottles/alchemy.dm index 518bcc84826..44bf8186395 100644 --- a/code/modules/reagents/reagent_containers/bottles/alchemy.dm +++ b/code/modules/reagents/reagent_containers/bottles/alchemy.dm @@ -7,9 +7,11 @@ /obj/item/reagent_containers/glass/bottle/healthpot list_reagents = list(/datum/reagent/medicine/healthpot = 75) + flags_ai_inventory = AI_ITEM_HEALING_DRINK | AI_ITEM_DRINK /obj/item/reagent_containers/glass/bottle/stronghealthpot list_reagents = list(/datum/reagent/medicine/stronghealth = 75) + flags_ai_inventory = AI_ITEM_HEALING_DRINK | AI_ITEM_DRINK /obj/item/reagent_containers/glass/bottle/manapot list_reagents = list(/datum/reagent/medicine/manapot = 75) diff --git a/code/modules/reagents/reagent_containers/powder.dm b/code/modules/reagents/reagent_containers/powder.dm index 04186c87922..1aa918a814b 100644 --- a/code/modules/reagents/reagent_containers/powder.dm +++ b/code/modules/reagents/reagent_containers/powder.dm @@ -10,6 +10,7 @@ sellprice = 10 grid_height = 32 grid_width = 32 + flags_ai_inventory = AI_ITEM_POWDER /obj/item/reagent_containers/powder/canconsume(mob/eater, mob/user, silent) . = ..() diff --git a/vanderlin.dme b/vanderlin.dme index 61f36eb7496..e82735677f5 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -825,6 +825,8 @@ #include "code\datums\ai\subtrees\targeted_ability_use.dm" #include "code\datums\ai\subtrees\tip_reaction.dm" #include "code\datums\ai\subtrees\travel_to_point.dm" +#include "code\datums\ai\subtrees\use_bandage.dm" +#include "code\datums\ai\subtrees\use_healing_drink.dm" #include "code\datums\ai\targetting_datum\not_friends.dm" #include "code\datums\ai\targetting_datum\not_holding_item.dm" #include "code\datums\ai\targetting_datum\simpe_targetting_datum.dm" @@ -948,6 +950,7 @@ #include "code\datums\components\hostage.dm" #include "code\datums\components\hovering_data.dm" #include "code\datums\components\igniter.dm" +#include "code\datums\components\inventory_manager.dm" #include "code\datums\components\item_equipped_movement_rustle.dm" #include "code\datums\components\item_fear.dm" #include "code\datums\components\itembound.dm" @@ -1106,6 +1109,7 @@ #include "code\datums\elements\frozen.dm" #include "code\datums\elements\hat_wearer.dm" #include "code\datums\elements\holy_weakness.dm" +#include "code\datums\elements\interrupt_on_damage.dm" #include "code\datums\elements\mob_overlay_effect.dm" #include "code\datums\elements\movetype_handler.dm" #include "code\datums\elements\no_mouse_drop.dm" From b71cda3314aafea83abad5839cbc27c427ec4408 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:09:37 -0800 Subject: [PATCH 33/73] Update difficulties.dm --- .../mob/living/carbon/human/npc/skeleton/difficulties.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm index 25aa49e20b5..3fdc5ace5be 100644 --- a/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm +++ b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm @@ -217,7 +217,7 @@ skel_outfit = /datum/outfit/job/skeleton/npc/medium if(4) skel_outfit = /datum/outfit/job/skeleton/npc/hard - ..() + . = ..() /mob/living/carbon/human/species/skeleton/npc/hardspread/Initialize() var/outfit = rand(1,4) @@ -230,4 +230,4 @@ skel_outfit = /datum/outfit/job/skeleton/npc/pirate if(4) skel_outfit = /datum/outfit/job/skeleton/npc/hard - ..() + . = ..() From 3323cbad403ac9505e6cbaab36eb067411b1a4d3 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:43:57 -0800 Subject: [PATCH 34/73] Update use_bandage.dm --- code/datums/ai/subtrees/use_bandage.dm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/code/datums/ai/subtrees/use_bandage.dm b/code/datums/ai/subtrees/use_bandage.dm index 226f3aa1bb4..3fa887d3975 100644 --- a/code/datums/ai/subtrees/use_bandage.dm +++ b/code/datums/ai/subtrees/use_bandage.dm @@ -3,6 +3,13 @@ /datum/ai_planning_subtree/use_bandage/SelectBehaviors(datum/ai_controller/controller, delta_time) if(controller.blackboard[BB_HELD_CONSUMABLE] || controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/bandage = inv.get_item(AI_ITEM_BANDAGE) + if(!bandage) + return var/real = FALSE var/mob/living/carbon/human/human_pawn = controller.pawn for(var/obj/item/bodypart/bodypart as anything in human_pawn.bodyparts) @@ -13,12 +20,6 @@ if(!real) return - var/datum/component/ai_inventory_manager/inv = controller.get_inventory() - if(!inv) - return - var/obj/item/bandage = inv.get_item(AI_ITEM_BANDAGE) - if(!bandage) - return controller.set_blackboard_key(BB_HELD_CONSUMABLE, bandage) controller.queue_behavior(/datum/ai_behavior/apply_bandage, BB_HELD_CONSUMABLE) return SUBTREE_RETURN_FINISH_PLANNING From cc1b343bd8d469a3bf6493c77e87abe99048cb40 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:45:49 -0800 Subject: [PATCH 35/73] Update use_healing_drink.dm --- code/datums/ai/subtrees/use_healing_drink.dm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/datums/ai/subtrees/use_healing_drink.dm b/code/datums/ai/subtrees/use_healing_drink.dm index 256135e0b88..5fae8517af2 100644 --- a/code/datums/ai/subtrees/use_healing_drink.dm +++ b/code/datums/ai/subtrees/use_healing_drink.dm @@ -1,7 +1,4 @@ /datum/ai_planning_subtree/use_healing_drink/SelectBehaviors(datum/ai_controller/controller, delta_time) - var/mob/living/carbon/human/H = controller.pawn - if(H.getBruteLoss() < 20 && H.getFireLoss() < 20) - return var/datum/component/ai_inventory_manager/inv = controller.get_inventory() if(!inv) return @@ -18,6 +15,10 @@ if(!drink) return + var/mob/living/carbon/human/H = controller.pawn + if(H.getBruteLoss() < 20 && H.getFireLoss() < 20) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, drink) controller.queue_behavior(/datum/ai_behavior/consume_healing_drink, BB_HELD_CONSUMABLE) From 57f00e98079e8c464e18a7748f5df13c934a9965 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:46:06 -0800 Subject: [PATCH 36/73] I be snorting coke and shit --- code/datums/ai/controllers/human_npc.dm | 1 + code/datums/ai/subtrees/use_powders.dm | 50 +++++++++++++++++++++++++ code/modules/clothing/belt/misc.dm | 1 + vanderlin.dme | 1 + 4 files changed, 53 insertions(+) create mode 100644 code/datums/ai/subtrees/use_powders.dm diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 6b433c142e8..46186cd6456 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -21,6 +21,7 @@ ) planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/use_powder, /datum/ai_planning_subtree/use_bandage, /datum/ai_planning_subtree/use_healing_drink, /datum/ai_planning_subtree/generic_wield, diff --git a/code/datums/ai/subtrees/use_powders.dm b/code/datums/ai/subtrees/use_powders.dm new file mode 100644 index 00000000000..3f5c01ec876 --- /dev/null +++ b/code/datums/ai/subtrees/use_powders.dm @@ -0,0 +1,50 @@ +/datum/ai_planning_subtree/use_powder/SelectBehaviors(datum/ai_controller/controller, delta_time) + // ONLY use powder if we're actively fighting someone + if(!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + // Don't interrupt if already holding something to use + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/powder = inv.get_item(AI_ITEM_POWDER) + if(!powder) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, powder) + controller.queue_behavior(/datum/ai_behavior/use_powder, BB_HELD_CONSUMABLE) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/use_powder + action_cooldown = 3 MINUTES // Very long cooldown, this is a rare treat + +/datum/ai_behavior/use_powder/perform(delta_time, datum/ai_controller/controller, consumable_key) + var/obj/item/powder = controller.blackboard[consumable_key] + if(!powder) + finish_action(controller, FALSE, consumable_key) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + if(H.get_active_held_item() != powder) + var/obj/item/usable = inv?.draw_usable_item(powder, AI_ITEM_POWDER) + if(!usable) + finish_action(controller, FALSE, consumable_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + // Powder must be snorted + var/old_zone = H.zone_selected + H.zone_selected = BODY_ZONE_PRECISE_NOSE + controller.ai_interact(H, maintain_position = TRUE) + H.zone_selected = old_zone + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/use_powder/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_POWDER)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() diff --git a/code/modules/clothing/belt/misc.dm b/code/modules/clothing/belt/misc.dm index 55c84f4d255..b4794b705ce 100644 --- a/code/modules/clothing/belt/misc.dm +++ b/code/modules/clothing/belt/misc.dm @@ -179,6 +179,7 @@ /obj/item/storage/belt/pouch/medicine populate_contents = list( /obj/item/needle, + /obj/item/reagent_containers/powder/ozium, /obj/item/natural/bundle/cloth/bandage/full, /obj/item/reagent_containers/glass/bottle/healthpot ) diff --git a/vanderlin.dme b/vanderlin.dme index e82735677f5..f4dbf42e3cb 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -827,6 +827,7 @@ #include "code\datums\ai\subtrees\travel_to_point.dm" #include "code\datums\ai\subtrees\use_bandage.dm" #include "code\datums\ai\subtrees\use_healing_drink.dm" +#include "code\datums\ai\subtrees\use_powders.dm" #include "code\datums\ai\targetting_datum\not_friends.dm" #include "code\datums\ai\targetting_datum\not_holding_item.dm" #include "code\datums\ai\targetting_datum\simpe_targetting_datum.dm" From 37eca296716d25efcc23adccc9af16f94bac0a80 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:47:25 -0800 Subject: [PATCH 37/73] linter --- code/game/objects/items/signal_horn.dm | 3 +++ code/modules/unit_tests/craftable_clothes.dm | 2 ++ 2 files changed, 5 insertions(+) diff --git a/code/game/objects/items/signal_horn.dm b/code/game/objects/items/signal_horn.dm index 76ea558d18e..de46e262736 100644 --- a/code/game/objects/items/signal_horn.dm +++ b/code/game/objects/items/signal_horn.dm @@ -157,3 +157,6 @@ icon_state = "signal_horn_indicator" screen_loc = "CENTER:-16,CENTER:-16" alpha = 100 + +#undef WARDEN_AMBUSH_MIN +#undef WARDEN_AMBUSH_MAX diff --git a/code/modules/unit_tests/craftable_clothes.dm b/code/modules/unit_tests/craftable_clothes.dm index 8ffc6b91eab..aefaef55f8b 100644 --- a/code/modules/unit_tests/craftable_clothes.dm +++ b/code/modules/unit_tests/craftable_clothes.dm @@ -75,6 +75,8 @@ abstract types are automatically excluded. /obj/item/clothing/neck/gorget/ancient, /obj/item/clothing/pants/platelegs/ancient, /obj/item/clothing/wrists/bracers/ancient, + /obj/item/clothing/head/menacing/mad_touched_treasure_hunter, //cursed + /obj/item/clothing/face/facemask/steel/mad_touched, //cursed /obj/item/clothing/wrists/bracers/naledi //Inqstuff ) From 62ad9750715fa9ae22db28200a6987adedfd66f6 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:40:27 -0800 Subject: [PATCH 38/73] redux of the inventory map, lets them use bombs --- code/__DEFINES/ai/_ai.dm | 14 +++++ code/datums/ai/_ai_controller.dm | 9 ++- code/datums/ai/controllers/human_npc.dm | 2 +- code/datums/ai/subtrees/use_explosive.dm | 64 ++++++++++++++++++++ code/datums/ai/subtrees/use_healing_drink.dm | 2 +- code/datums/components/inventory_manager.dm | 42 +++---------- code/game/objects/items/explosives/_base.dm | 2 + code/modules/clothing/belt/misc.dm | 1 - vanderlin.dme | 1 + 9 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 code/datums/ai/subtrees/use_explosive.dm diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 39196d59db6..a6fa35ccfef 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -338,6 +338,20 @@ #define AI_ITEM_GUN (1<<9) #define AI_ITEM_DRINK (1<<10) // generic drinkable (not necessarily healing) +GLOBAL_LIST_INIT(ai_item_flags, list( + AI_ITEM_BANDAGE, + AI_ITEM_HEALING_DRINK, + AI_ITEM_FOOD, + AI_ITEM_POWDER, + AI_ITEM_KEY, + AI_ITEM_TOOL, + AI_ITEM_AMMO, + AI_ITEM_GRENADE, + AI_ITEM_MELEE, + AI_ITEM_GUN, + AI_ITEM_DRINK, +)) + #define AI_INVENTORY_WATCHED_SLOTS (ITEM_SLOT_BELT | ITEM_SLOT_BACK_L | ITEM_SLOT_BACK_R | \ ITEM_SLOT_BELT_L | ITEM_SLOT_BELT_R | ITEM_SLOT_ARMOR | ITEM_SLOT_PANTS | \ ITEM_SLOT_SHIRT | ITEM_SLOT_CLOAK | ITEM_SLOT_BACK) diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index b27e47c17ec..7765b366b00 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -47,6 +47,8 @@ have ways of interacting with a specific atom and control it. They posses a blac // Movement related things here ///Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards. var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb + /// this shouldn't be a component tbh but uh reusing code go brrr + var/datum/component/ai_inventory_manager/inventory_component ///Cooldown until next movement COOLDOWN_DECLARE(movement_cooldown) ///Delay between movements. This is on the controller so we can keep the movement datum singleton @@ -82,6 +84,7 @@ have ways of interacting with a specific atom and control it. They posses a blac /datum/ai_controller/Destroy(force, ...) UnpossessPawn(FALSE) our_cells = null + inventory_component = null set_movement_target(type, null) if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) @@ -515,8 +518,10 @@ have ways of interacting with a specific atom and control it. They posses a blac SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments) /datum/ai_controller/proc/get_inventory() - RETURN_TYPE(/datum/component/ai_inventory_manager) - return pawn?.GetComponent(/datum/component/ai_inventory_manager) + RETURN_TYPE(/datum/component/ai_inventory_manager) + if(!inventory_component) + return pawn?.GetComponent(/datum/component/ai_inventory_manager) + return inventory_component /datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) var/mob/living/liver = pawn diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 46186cd6456..965ec66b4ad 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -24,6 +24,7 @@ /datum/ai_planning_subtree/use_powder, /datum/ai_planning_subtree/use_bandage, /datum/ai_planning_subtree/use_healing_drink, + /datum/ai_planning_subtree/throw_grenade, /datum/ai_planning_subtree/generic_wield, /datum/ai_planning_subtree/generic_resist, /datum/ai_planning_subtree/generic_stand, @@ -32,7 +33,6 @@ /datum/ai_planning_subtree/archer_base, /datum/ai_planning_subtree/ranged_attack_subtree, /datum/ai_planning_subtree/aggro_find_target, - //datum/ai_planning_subtree/wounded_harass, /datum/ai_planning_subtree/squad_flank, /datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc, /datum/ai_planning_subtree/find_weapon, diff --git a/code/datums/ai/subtrees/use_explosive.dm b/code/datums/ai/subtrees/use_explosive.dm new file mode 100644 index 00000000000..e0bbb06dd85 --- /dev/null +++ b/code/datums/ai/subtrees/use_explosive.dm @@ -0,0 +1,64 @@ +/datum/ai_planning_subtree/throw_grenade/SelectBehaviors(datum/ai_controller/controller, delta_time) + // Only throw grenades if we have a target to throw at + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + // Don't interrupt if already doing something + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/explosive/grenade = inv.get_item(AI_ITEM_GRENADE) + if(!grenade) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, grenade) + controller.queue_behavior(/datum/ai_behavior/throw_grenade, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/throw_grenade + action_cooldown = 2 MINUTES // Long cooldown - grenades are precious and dangerous also cause fuck having smoke bomb spamming horcs + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/throw_grenade/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key) + var/obj/item/explosive/grenade = controller.blackboard[consumable_key] + if(!grenade) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/mob/living/carbon/human/H = controller.pawn + // Check we're in throwable range + if(get_dist(H, target) > grenade.throw_range) + finish_action(controller, FALSE, consumable_key, target_key) + return + // no throwing through walls hopefully + if(!can_see(H, target, grenade.throw_range)) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(H.get_active_held_item() != grenade) + var/obj/item/usable = inv?.draw_usable_item(grenade, AI_ITEM_GRENADE) + if(!usable) + finish_action(controller, FALSE, consumable_key, target_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + grenade = usable + grenade.arm_grenade(H) + H.throw_item(get_turf(target)) + finish_action(controller, TRUE, consumable_key, target_key) + +/datum/ai_behavior/throw_grenade/finish_action(datum/ai_controller/controller, succeeded, consumable_key, target_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + controller.clear_blackboard_key(target_key) + // If somehow we still have it (arm failed, throw failed), drop it and die I guess + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_GRENADE)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() diff --git a/code/datums/ai/subtrees/use_healing_drink.dm b/code/datums/ai/subtrees/use_healing_drink.dm index 5fae8517af2..859345d462c 100644 --- a/code/datums/ai/subtrees/use_healing_drink.dm +++ b/code/datums/ai/subtrees/use_healing_drink.dm @@ -24,7 +24,7 @@ /datum/ai_behavior/consume_healing_drink action_cooldown = 70 SECONDS - behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION /datum/ai_behavior/consume_healing_drink/perform(delta_time, datum/ai_controller/controller, consumable_key) var/obj/item/reagent_containers/glass/bottle/drink = controller.blackboard[consumable_key] diff --git a/code/datums/components/inventory_manager.dm b/code/datums/components/inventory_manager.dm index 0c960a3088d..93792b9d47e 100644 --- a/code/datums/components/inventory_manager.dm +++ b/code/datums/components/inventory_manager.dm @@ -17,20 +17,10 @@ return COMPONENT_INCOMPATIBLE _build_slot_flag_list() + inventory_map = alist() + for(var/ai_item_type as anything in GLOB.ai_item_flags) + inventory_map[ai_item_type] = list() - inventory_map = alist( - AI_ITEM_BANDAGE = list(), - AI_ITEM_HEALING_DRINK = list(), - AI_ITEM_FOOD = list(), - AI_ITEM_DRINK = list(), - AI_ITEM_POWDER = list(), - AI_ITEM_KEY = list(), - AI_ITEM_TOOL = list(), - AI_ITEM_AMMO = list(), - AI_ITEM_GRENADE = list(), - AI_ITEM_MELEE = list(), - AI_ITEM_GUN = list(), - ) container_refs = alist() RegisterSignal(parent, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equip)) @@ -98,28 +88,10 @@ /// Classify a single item into all matching categories /datum/component/ai_inventory_manager/proc/_classify_item(obj/item/it, slot_flag) RegisterSignal(it, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), override = TRUE) - if(it.flags_ai_inventory & AI_ITEM_BANDAGE) - inventory_map[AI_ITEM_BANDAGE][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_HEALING_DRINK) - inventory_map[AI_ITEM_HEALING_DRINK][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_FOOD) - inventory_map[AI_ITEM_FOOD][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_DRINK) - inventory_map[AI_ITEM_DRINK][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_POWDER) - inventory_map[AI_ITEM_POWDER][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_KEY) - inventory_map[AI_ITEM_KEY][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_TOOL) - inventory_map[AI_ITEM_TOOL][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_AMMO) - inventory_map[AI_ITEM_AMMO][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_GRENADE) - inventory_map[AI_ITEM_GRENADE][it] = slot_flag - if(it.flags_ai_inventory & AI_ITEM_MELEE) - inventory_map[AI_ITEM_MELEE][it] = slot_flag - if(isgun(it)) - inventory_map[AI_ITEM_GUN][it] = slot_flag + + for(var/ai_flag in GLOB.ai_item_flags) + if(ai_flag & it.flags_ai_inventory) + inventory_map[ai_flag][it] = slot_flag /datum/component/ai_inventory_manager/proc/on_equip(datum/source, obj/item/equipment, slot) SIGNAL_HANDLER diff --git a/code/game/objects/items/explosives/_base.dm b/code/game/objects/items/explosives/_base.dm index c9147929b30..f029434d5d6 100644 --- a/code/game/objects/items/explosives/_base.dm +++ b/code/game/objects/items/explosives/_base.dm @@ -21,6 +21,8 @@ grid_height = 64 grid_width = 32 + flags_ai_inventory = AI_ITEM_GRENADE + ///do we explode on impact? var/impact_explode = FALSE ///odds we fail on ignite diff --git a/code/modules/clothing/belt/misc.dm b/code/modules/clothing/belt/misc.dm index b4794b705ce..55c84f4d255 100644 --- a/code/modules/clothing/belt/misc.dm +++ b/code/modules/clothing/belt/misc.dm @@ -179,7 +179,6 @@ /obj/item/storage/belt/pouch/medicine populate_contents = list( /obj/item/needle, - /obj/item/reagent_containers/powder/ozium, /obj/item/natural/bundle/cloth/bandage/full, /obj/item/reagent_containers/glass/bottle/healthpot ) diff --git a/vanderlin.dme b/vanderlin.dme index f4dbf42e3cb..881498c8033 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -826,6 +826,7 @@ #include "code\datums\ai\subtrees\tip_reaction.dm" #include "code\datums\ai\subtrees\travel_to_point.dm" #include "code\datums\ai\subtrees\use_bandage.dm" +#include "code\datums\ai\subtrees\use_explosive.dm" #include "code\datums\ai\subtrees\use_healing_drink.dm" #include "code\datums\ai\subtrees\use_powders.dm" #include "code\datums\ai\targetting_datum\not_friends.dm" From e41047dbc5ecd899a71bb59432b9e0c9995a62f8 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:10:22 -0800 Subject: [PATCH 39/73] adds in a combat vocalizer component to handle noises through combat not a blackboard due to this interacting with things outside of the ai_controllers typical duties like death --- code/__DEFINES/components.dm | 5 + code/datums/ai/controllers/human_npc.dm | 1 + code/datums/components/combat_vocalizer.dm | 92 +++++++++++++++++++ .../human/npc/human_soldiers/deranged.dm | 18 ++-- vanderlin.dme | 1 + 5 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 code/datums/components/combat_vocalizer.dm diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 5cf6be3365f..fa9046fa446 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -92,6 +92,11 @@ #define COMSIG_MOB_BREAK_SNEAK "mob_break_sneak" #define COMSIG_MOB_DEATH "mob_death" //from base of mob/death(): (gibbed) +#define COMSIG_MOB_TRY_BARK "try_bark" +#define COMSIG_MOB_TRY_EMOTE "try_emote" +#define COMSIG_MOB_MODIFY_AGGRO_LINES "comsig_mob_modify_aggro_lines" +#define COMSIG_MOB_MODIFY_DEATH_LINES "comsig_mob_modify_death_lines" + #define COMSIG_MOB_CREATED_CALLOUT "mob_created_callout" #define COMSIG_MOB_CLICKON "mob_clickon" //from base of mob/clickon(): (atom/A, params) diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 965ec66b4ad..0dcf34e2ccf 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -48,6 +48,7 @@ movement_delay = living_pawn.cached_multiplicative_slowdown new_pawn.AddComponent(/datum/component/ai_inventory_manager) new_pawn.AddElement(/datum/element/interrupt_on_damage) + new_pawn.AddComponent(/datum/component/combat_vocalizer) /datum/ai_controller/human_npc/UnpossessPawn(destroy) UnregisterSignal(pawn, list( diff --git a/code/datums/components/combat_vocalizer.dm b/code/datums/components/combat_vocalizer.dm new file mode 100644 index 00000000000..5803ec5f963 --- /dev/null +++ b/code/datums/components/combat_vocalizer.dm @@ -0,0 +1,92 @@ +/datum/component/combat_vocalizer + /// List of lines to say on aggro/during combat. Can be a list of strings or a path to a file. + var/list/aggro_lines + /// List of lines to say on death. + var/list/death_lines + /// Cooldown between combat barks (say() calls during handle_combat equivalent) + var/bark_cooldown = 8 SECONDS + /// Cooldown between emotes during combat + var/emote_cooldown = 5 SECONDS + /// Chance per planning tick to bark + var/bark_chance = 3 + /// Chance per planning tick to emote + var/emote_chance = 5 + + COOLDOWN_DECLARE(last_bark) + COOLDOWN_DECLARE(last_emote) + +/datum/component/combat_vocalizer/Initialize(list/lines, list/death_lines, bark_cooldown, emote_cooldown, bark_chance, emote_chance) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.aggro_lines = lines + src.death_lines = death_lines + if(!isnull(bark_cooldown)) + src.bark_cooldown = bark_cooldown + if(!isnull(emote_cooldown)) + src.emote_cooldown = emote_cooldown + if(!isnull(bark_chance)) + src.bark_chance = bark_chance + if(!isnull(emote_chance)) + src.emote_chance = emote_chance + + RegisterSignal(parent, COMSIG_MOB_DEATH, PROC_REF(on_death)) + RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_acquired)) + RegisterSignal(parent, COMSIG_MOB_TRY_BARK, PROC_REF(try_combat_bark)) + RegisterSignal(parent, COMSIG_MOB_TRY_EMOTE, PROC_REF(try_combat_emote)) + RegisterSignal(parent, COMSIG_MOB_MODIFY_AGGRO_LINES, PROC_REF(try_modify_aggro_lines)) + RegisterSignal(parent, COMSIG_MOB_MODIFY_DEATH_LINES, PROC_REF(try_modify_death_lines)) + +/datum/component/combat_vocalizer/Destroy(force) + . = ..() + UnregisterSignal(parent, list( + COMSIG_MOB_DEATH, + COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), + )) + +/datum/component/combat_vocalizer/proc/on_target_acquired(mob/living/source, key) + if(!COOLDOWN_FINISHED(src, last_bark)) + return + if(!length(aggro_lines)) + return + var/mob/living/target = source.ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + source.say(pick(aggro_lines)) + source.pointed(target) + COOLDOWN_START(src, last_bark, bark_cooldown) + +/datum/component/combat_vocalizer/proc/try_combat_bark(mob/living/source) + if(!COOLDOWN_FINISHED(src, last_bark)) + return + if(!length(aggro_lines)) + return + if(!prob(bark_chance)) + return + if(aggro_lines) + source.say(pick(aggro_lines)) + COOLDOWN_START(src, last_bark, bark_cooldown) + +/datum/component/combat_vocalizer/proc/try_combat_emote(mob/living/source, emote_key) + if(!COOLDOWN_FINISHED(src, last_emote)) + return + if(!prob(emote_chance)) + return + source.emote(emote_key) + COOLDOWN_START(src, last_emote, emote_cooldown) + +/datum/component/combat_vocalizer/proc/on_death(mob/living/source) + if(!length(death_lines)) + return + if(death_lines) + source.say(pick(death_lines), forced = TRUE) + source.emote("painscream") + +/datum/component/combat_vocalizer/proc/try_modify_death_lines(mob/living/source, list/incoming_lines, override) + if(override) + death_lines = list() + death_lines += incoming_lines + +/datum/component/combat_vocalizer/proc/try_modify_aggro_lines(mob/living/source, list/incoming_lines, override) + if(override) + aggro_lines = list() + aggro_lines += incoming_lines diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm index 6db3e0514cf..c186f1cc1b5 100644 --- a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm @@ -34,6 +34,7 @@ GLOBAL_LIST_INIT(hedgeknight_aggro, file2list("strings/rt/hedgeknightaggrolines. var/head = get_bodypart(BODY_ZONE_HEAD) RegisterSignal(head, COMSIG_MOB_DISMEMBER, PROC_REF(handle_drop_limb)) + /mob/living/carbon/human/species/human/northern/deranged_knight/Destroy() var/head = get_bodypart(BODY_ZONE_HEAD) if(head) @@ -63,12 +64,19 @@ GLOBAL_LIST_INIT(hedgeknight_aggro, file2list("strings/rt/hedgeknightaggrolines. switch(rand(1, 4)) if(1) preset = "graggar" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.graggar_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("No more... Blood!"), TRUE) if(2) preset = "matthios" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.matthios_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("Matthios, I have failed you...", "Matthios, is this true?!"), TRUE) if(3) preset = "zizo" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.zizo_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("Zizo, forgive me!"), TRUE) if(4) preset = "hedgeknight" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.hedgeknight_aggro, TRUE) switch(preset) if("graggar") equipOutfit(new /datum/outfit/job/quest_miniboss/graggar) @@ -130,16 +138,6 @@ GLOBAL_LIST_INIT(hedgeknight_aggro, file2list("strings/rt/hedgeknightaggrolines. def_intent_change(INTENT_PARRY) /mob/living/carbon/human/species/human/northern/deranged_knight/death(gibbed, nocutscene) - if(preset == "matthios") - if(prob(95)) - say("Matthios, I have failed you...", forced = TRUE) - else - say("Matthios, is this true?!", forced = TRUE) - else if(preset == "zizo") - say("Zizo, forgive me!", forced = TRUE) - else if(preset == "graggar") - say("No more... Blood!") - emote("painscream") . = ..() if(!gibbed) dust(FALSE, FALSE, TRUE) diff --git a/vanderlin.dme b/vanderlin.dme index 881498c8033..b3dc406c4f1 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -929,6 +929,7 @@ #include "code\datums\components\chimeric_organ.dm" #include "code\datums\components\cleaner.dm" #include "code\datums\components\combat_noises.dm" +#include "code\datums\components\combat_vocalizer.dm" #include "code\datums\components\conjured_item.dm" #include "code\datums\components\connect_mob_behalf.dm" #include "code\datums\components\construction.dm" From c5800c6eeaadd08650d8666ef97c33da86a205f6 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:30:00 -0800 Subject: [PATCH 40/73] they can now use throwables and cooldowns work proper on things where they weren't working --- code/__DEFINES/ai/_ai.dm | 2 + .../hostile/find_potential_targets.dm | 1 + .../ai/behaviours/interact_with_target.dm | 1 + code/datums/ai/behaviours/move_to_cardinal.dm | 1 + code/datums/ai/behaviours/perform_emote.dm | 1 + code/datums/ai/behaviours/perform_speech.dm | 1 + code/datums/ai/behaviours/pet_use_ability.dm | 1 + .../ai/behaviours/use_targeted_ability.dm | 1 + code/datums/ai/controllers/human_npc.dm | 1 + code/datums/ai/subtrees/human_basic_attack.dm | 1 + .../datums/ai/subtrees/use_throwing_weapon.dm | 81 +++++++++++++++++++ .../objects/items/weapons/melee/knives.dm | 1 + .../living/carbon/human/npc/orc/soldiers.dm | 3 +- vanderlin.dme | 1 + 14 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 code/datums/ai/subtrees/use_throwing_weapon.dm diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index a6fa35ccfef..632ad69b729 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -337,6 +337,7 @@ #define AI_ITEM_MELEE (1<<8) #define AI_ITEM_GUN (1<<9) #define AI_ITEM_DRINK (1<<10) // generic drinkable (not necessarily healing) +#define AI_ITEM_THROWING (1<<11) GLOBAL_LIST_INIT(ai_item_flags, list( AI_ITEM_BANDAGE, @@ -350,6 +351,7 @@ GLOBAL_LIST_INIT(ai_item_flags, list( AI_ITEM_MELEE, AI_ITEM_GUN, AI_ITEM_DRINK, + AI_ITEM_THROWING, )) #define AI_INVENTORY_WATCHED_SLOTS (ITEM_SLOT_BELT | ITEM_SLOT_BACK_L | ITEM_SLOT_BACK_R | \ diff --git a/code/datums/ai/behaviours/hostile/find_potential_targets.dm b/code/datums/ai/behaviours/hostile/find_potential_targets.dm index e06d6f75860..98d29f01aa2 100644 --- a/code/datums/ai/behaviours/hostile/find_potential_targets.dm +++ b/code/datums/ai/behaviours/hostile/find_potential_targets.dm @@ -14,6 +14,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob))) return ..() /datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() var/mob/living/living_mob = controller.pawn if(living_mob.pet_passive) finish_action(controller, succeeded = FALSE) diff --git a/code/datums/ai/behaviours/interact_with_target.dm b/code/datums/ai/behaviours/interact_with_target.dm index 872dc29aea2..3f48fb51ceb 100644 --- a/code/datums/ai/behaviours/interact_with_target.dm +++ b/code/datums/ai/behaviours/interact_with_target.dm @@ -14,6 +14,7 @@ set_movement_target(controller, target) /datum/ai_behavior/interact_with_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() var/atom/target = controller.blackboard[target_key] if(QDELETED(target) || !pre_interact(controller, target)) finish_action(controller, FALSE) diff --git a/code/datums/ai/behaviours/move_to_cardinal.dm b/code/datums/ai/behaviours/move_to_cardinal.dm index 18886f4e40f..2047c0f4200 100644 --- a/code/datums/ai/behaviours/move_to_cardinal.dm +++ b/code/datums/ai/behaviours/move_to_cardinal.dm @@ -36,6 +36,7 @@ set_movement_target(controller, move_target) /datum/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() var/atom/target = controller.blackboard[target_key] if (QDELETED(target)) finish_action(controller = controller, succeeded = FALSE, target_key = target_key) diff --git a/code/datums/ai/behaviours/perform_emote.dm b/code/datums/ai/behaviours/perform_emote.dm index 72aafc53c7b..b1980da64b6 100644 --- a/code/datums/ai/behaviours/perform_emote.dm +++ b/code/datums/ai/behaviours/perform_emote.dm @@ -1,6 +1,7 @@ /datum/ai_behavior/perform_emote /datum/ai_behavior/perform_emote/perform(delta_time, datum/ai_controller/controller, emote) + . = ..() var/mob/living/living_pawn = controller.pawn if(!istype(living_pawn)) return diff --git a/code/datums/ai/behaviours/perform_speech.dm b/code/datums/ai/behaviours/perform_speech.dm index c1e87459fac..1c89729ef90 100644 --- a/code/datums/ai/behaviours/perform_speech.dm +++ b/code/datums/ai/behaviours/perform_speech.dm @@ -1,6 +1,7 @@ /datum/ai_behavior/perform_speech /datum/ai_behavior/perform_speech/perform(delta_time, datum/ai_controller/controller, speech) + . = ..() var/mob/living/living_pawn = controller.pawn if(!istype(living_pawn)) return diff --git a/code/datums/ai/behaviours/pet_use_ability.dm b/code/datums/ai/behaviours/pet_use_ability.dm index 6936ad323cf..e9fca4dd517 100644 --- a/code/datums/ai/behaviours/pet_use_ability.dm +++ b/code/datums/ai/behaviours/pet_use_ability.dm @@ -10,6 +10,7 @@ set_movement_target(controller, target) /datum/ai_behavior/pet_use_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key) + . = ..() var/datum/action/cooldown/ability = controller.blackboard[ability_key] var/mob/living/target = controller.blackboard[target_key] if (QDELETED(ability) || QDELETED(target)) diff --git a/code/datums/ai/behaviours/use_targeted_ability.dm b/code/datums/ai/behaviours/use_targeted_ability.dm index 3e0d196be7b..303d891c8ac 100644 --- a/code/datums/ai/behaviours/use_targeted_ability.dm +++ b/code/datums/ai/behaviours/use_targeted_ability.dm @@ -5,6 +5,7 @@ /datum/ai_behavior/targeted_mob_ability /datum/ai_behavior/targeted_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key) + . = ..() var/datum/action/cooldown/ability = controller.blackboard[ability_key] var/mob/living/target = controller.blackboard[target_key] if(QDELETED(ability) || QDELETED(target)) diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 0dcf34e2ccf..a44f551b057 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -23,6 +23,7 @@ /datum/ai_planning_subtree/pet_planning, /datum/ai_planning_subtree/use_powder, /datum/ai_planning_subtree/use_bandage, + /datum/ai_planning_subtree/use_throwable, /datum/ai_planning_subtree/use_healing_drink, /datum/ai_planning_subtree/throw_grenade, /datum/ai_planning_subtree/generic_wield, diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm index 3af535162e1..f21bc24f604 100644 --- a/code/datums/ai/subtrees/human_basic_attack.dm +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -43,6 +43,7 @@ _scan_for_weakpoint(controller, pawn, target) /datum/ai_behavior/basic_melee_attack/human_npc/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + controller.behavior_cooldowns[src] = world.time + get_cooldown(controller) //we don't wanna call parent tbh var/mob/living/carbon/human/pawn = controller.pawn var/atom/target = controller.blackboard[target_key] var/datum/targetting_datum/td = controller.blackboard[targetting_datum_key] diff --git a/code/datums/ai/subtrees/use_throwing_weapon.dm b/code/datums/ai/subtrees/use_throwing_weapon.dm new file mode 100644 index 00000000000..94bfa15d7c2 --- /dev/null +++ b/code/datums/ai/subtrees/use_throwing_weapon.dm @@ -0,0 +1,81 @@ +/datum/ai_planning_subtree/use_throwable + var/max_throw_dist = 7 // Only throw if within this distance + var/min_throw_dist = 2 // Don't bother throwing at point blank + +/datum/ai_planning_subtree/use_throwable/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + var/obj/item/weapon/knife/throwingknife = inv.get_item(AI_ITEM_THROWING) + if(!throwingknife) + return + + var/mob/living/pawn = controller.pawn + var/dist = get_dist(pawn, target) + + // Only throw within our sweet spot range + if(dist > max_throw_dist || dist < min_throw_dist) + return + + // Need line of sight to throw + if(!can_see(pawn, target, max_throw_dist)) + return + + + controller.set_blackboard_key(BB_HELD_CONSUMABLE, throwingknife) + controller.queue_behavior(/datum/ai_behavior/use_throwable, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/use_throwable + action_cooldown = 3 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/use_throwable/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key) + . = ..() + var/obj/item/weapon/knife/throwingknife = controller.blackboard[consumable_key] + if(!throwingknife) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/mob/living/pawn = controller.pawn + var/dist = get_dist(pawn, target) + + if(dist > throwingknife.throw_range || dist < 2) + finish_action(controller, FALSE, consumable_key, target_key) + return + + if(!can_see(pawn, target, throwingknife.throw_range)) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(pawn.get_active_held_item() != throwingknife) + var/obj/item/usable = inv?.draw_usable_item(throwingknife, AI_ITEM_THROWING) + if(!usable) + finish_action(controller, FALSE, consumable_key, target_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + throwingknife = usable + + pawn.face_atom(target) + pawn.throw_item(get_turf(target)) + finish_action(controller, TRUE, consumable_key, target_key) + +/datum/ai_behavior/use_throwable/finish_action(datum/ai_controller/controller, succeeded, consumable_key, target_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + // Restore hands to whatever they were holding before + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + inv?.restore_hands() diff --git a/code/game/objects/items/weapons/melee/knives.dm b/code/game/objects/items/weapons/melee/knives.dm index 782a284ae96..cb00cad8044 100644 --- a/code/game/objects/items/weapons/melee/knives.dm +++ b/code/game/objects/items/weapons/melee/knives.dm @@ -601,6 +601,7 @@ embedding = list("embedded_pain_multiplier" = 4, "embed_chance" = 30, "embedded_fall_chance" = 20) melt_amount = 50 sellprice = 3 + flags_ai_inventory = AI_ITEM_THROWING /obj/item/weapon/knife/throwingknife/bronze name = "bronze tossblade" diff --git a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm index f02e905ac1f..07f526f8375 100644 --- a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm +++ b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm @@ -171,8 +171,7 @@ armor = /obj/item/clothing/armor/leather/hide shirt = /obj/item/clothing/armor/gambeson/light pants = /obj/item/clothing/pants/trou/leather - belt = /obj/item/storage/belt/leather - beltl = /obj/item/storage/belt/pouch/medicine + belt = /obj/item/storage/belt/leather/knifebelt/black/steel H.base_strength = 14 // GAGGER GAGGER GAGGER H.base_speed = 10 // Fast, for an orc H.base_constitution = 12 diff --git a/vanderlin.dme b/vanderlin.dme index b3dc406c4f1..a9ff4eeb2de 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -829,6 +829,7 @@ #include "code\datums\ai\subtrees\use_explosive.dm" #include "code\datums\ai\subtrees\use_healing_drink.dm" #include "code\datums\ai\subtrees\use_powders.dm" +#include "code\datums\ai\subtrees\use_throwing_weapon.dm" #include "code\datums\ai\targetting_datum\not_friends.dm" #include "code\datums\ai\targetting_datum\not_holding_item.dm" #include "code\datums\ai\targetting_datum\simpe_targetting_datum.dm" From 78b60fe3d6330507bc7bf1da96e90b4f29fafdc1 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:56:52 -0800 Subject: [PATCH 41/73] cheese detected, cheese vaporized with atomic laser. --- code/datums/ai/controllers/human_npc.dm | 1 + code/datums/ai/subtrees/generic_restraint.dm | 28 +++++++++++++++++++ code/modules/mob/living/carbon/carbon.dm | 10 +++++-- code/modules/mob/living/carbon/human/human.dm | 6 ++-- .../mob/living/carbon/human/npc/_npc.dm | 4 +-- .../mob/living/carbon/monkey/combat.dm | 2 +- .../mob/living/carbon/spirit/combat.dm | 2 +- code/modules/mob/living/living.dm | 2 +- vanderlin.dme | 1 + 9 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 code/datums/ai/subtrees/generic_restraint.dm diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index a44f551b057..814a0326449 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -21,6 +21,7 @@ ) planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/generic_break_restraints, /datum/ai_planning_subtree/use_powder, /datum/ai_planning_subtree/use_bandage, /datum/ai_planning_subtree/use_throwable, diff --git a/code/datums/ai/subtrees/generic_restraint.dm b/code/datums/ai/subtrees/generic_restraint.dm new file mode 100644 index 00000000000..2770fba75c6 --- /dev/null +++ b/code/datums/ai/subtrees/generic_restraint.dm @@ -0,0 +1,28 @@ +/datum/ai_planning_subtree/generic_break_restraints/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/carbon/living_pawn = controller.pawn + if(!isliving(living_pawn)) + return + + if(!living_pawn.handcuffed && !living_pawn.legcuffed) + return + + if(SPT_PROB(50, seconds_per_tick)) + controller.queue_behavior(/datum/ai_behavior/break_restraints) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/break_restraints + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 30 SECONDS + +/datum/ai_behavior/break_restraints/perform(seconds_per_tick, datum/ai_controller/controller) + var/mob/living/carbon/living_pawn = controller.pawn + if(!living_pawn.handcuffed && !living_pawn.legcuffed) + finish_action(controller, FALSE) + return + + if(living_pawn.handcuffed) + living_pawn.cuff_resist(living_pawn.handcuffed, instant = TRUE) + else if(living_pawn.legcuffed) + living_pawn.cuff_resist(living_pawn.legcuffed, instant = TRUE) + + finish_action(controller, TRUE) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index b6323377577..a0c3d5ce0ea 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -350,7 +350,7 @@ ExtinguishMob(TRUE) return -/mob/living/carbon/resist_restraints() +/mob/living/carbon/resist_restraints(instant = FALSE) var/obj/item/I = null var/type = 0 if(handcuffed) @@ -366,10 +366,10 @@ if(type == 2) changeNext_move(CLICK_CD_RANGE) last_special = world.time + CLICK_CD_RANGE - cuff_resist(I) + cuff_resist(I, instant = instant) -/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0) +/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0, instant = FALSE) if(I.item_flags & BEING_REMOVED) to_chat(src, span_warning("I'm already trying to get out of \the [I]\s!")) return @@ -380,6 +380,10 @@ breakouttime = I.breakouttime if(STASTR > 15 || (mind && mind.has_antag_datum(/datum/antagonist/zombie)) ) cuff_break = INSTANT_CUFFBREAK + + if(instant) + cuff_break = INSTANT_CUFFBREAK + if(!cuff_break) to_chat(src, span_notice("I try to get out of \the [I]\s...")) if(do_after(src, breakouttime, timed_action_flags = (IGNORE_HELD_ITEM))) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index db26e4d5eef..56159db2023 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -390,7 +390,7 @@ else to_chat(C, "I feel a breath of fresh air... which is a sensation you don't recognise...") -/mob/living/carbon/human/cuff_resist(obj/item/I) +/mob/living/carbon/human/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0, instant = FALSE) if(..()) dropItemToGround(I) @@ -414,11 +414,11 @@ remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, "#000000") cut_overlay(MA) -/mob/living/carbon/human/resist_restraints() +/mob/living/carbon/human/resist_restraints(instant = FALSE) if(wear_armor && wear_armor.breakouttime) changeNext_move(CLICK_CD_BREAKOUT) last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(wear_armor) + cuff_resist(wear_armor, instant = instant) else ..() diff --git a/code/modules/mob/living/carbon/human/npc/_npc.dm b/code/modules/mob/living/carbon/human/npc/_npc.dm index 9f44d162c54..a464815ac05 100644 --- a/code/modules/mob/living/carbon/human/npc/_npc.dm +++ b/code/modules/mob/living/carbon/human/npc/_npc.dm @@ -72,7 +72,7 @@ if(A) dropItemToGround(A, TRUE) -/mob/living/carbon/human/resist_restraints() +/mob/living/carbon/human/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed @@ -81,7 +81,7 @@ if(I) changeNext_move(CLICK_CD_BREAKOUT) last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(I) + cuff_resist(I, instant = instant) // attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little /mob/living/carbon/human/proc/monkey_attack(mob/living/L) diff --git a/code/modules/mob/living/carbon/monkey/combat.dm b/code/modules/mob/living/carbon/monkey/combat.dm index 3dc6848837d..8fe9f3f8a5b 100644 --- a/code/modules/mob/living/carbon/monkey/combat.dm +++ b/code/modules/mob/living/carbon/monkey/combat.dm @@ -103,7 +103,7 @@ monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) -/mob/living/carbon/monkey/resist_restraints() +/mob/living/carbon/monkey/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed diff --git a/code/modules/mob/living/carbon/spirit/combat.dm b/code/modules/mob/living/carbon/spirit/combat.dm index 4b63b0610b2..4cc1dc78b3a 100644 --- a/code/modules/mob/living/carbon/spirit/combat.dm +++ b/code/modules/mob/living/carbon/spirit/combat.dm @@ -96,7 +96,7 @@ monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) -/mob/living/carbon/spirit/resist_restraints() +/mob/living/carbon/spirit/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 8e5addb84ad..14d8708bcff 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1659,7 +1659,7 @@ /mob/living/proc/resist_fire() return -/mob/living/proc/resist_restraints() +/mob/living/proc/resist_restraints(instant = FALSE) return /mob/living/proc/get_visible_name() diff --git a/vanderlin.dme b/vanderlin.dme index a9ff4eeb2de..702f07dc939 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -785,6 +785,7 @@ #include "code\datums\ai\subtrees\flesh_hungers.dm" #include "code\datums\ai\subtrees\flesh_melee.dm" #include "code\datums\ai\subtrees\generic_resist.dm" +#include "code\datums\ai\subtrees\generic_restraint.dm" #include "code\datums\ai\subtrees\generic_stand.dm" #include "code\datums\ai\subtrees\generic_wield.dm" #include "code\datums\ai\subtrees\glimmerwing.dm" From 8c2b93079e35b4dc81ed993b7248f59140a04fde Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:19:13 -0800 Subject: [PATCH 42/73] some tweaks and fixes for inventory access, moves bow access over to new system --- code/__DEFINES/ai/_ai.dm | 3 +- code/datums/ai/subtrees/__bow_base.dm | 42 +++++------- code/datums/ai/subtrees/bow_usage.dm | 66 ++++++++++++------- code/datums/ai/subtrees/use_bandage.dm | 7 +- code/datums/ai/subtrees/use_explosive.dm | 7 +- code/datums/ai/subtrees/use_healing_drink.dm | 7 +- code/datums/ai/subtrees/use_powders.dm | 8 +-- .../datums/ai/subtrees/use_throwing_weapon.dm | 11 ++-- code/modules/clothing/quivers/misc.dm | 3 +- .../living/carbon/human/npc/orc/soldiers.dm | 3 +- .../projectiles/guns/ballistic/launchers.dm | 1 + 11 files changed, 82 insertions(+), 76 deletions(-) diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 632ad69b729..9a2336c37d6 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -236,7 +236,6 @@ #define BB_BEGGING_FOOD_ITEM "item_beg_target" #define BB_ARCHER_NPC_TARGET_ARROW "archer_target_arrow" #define BB_ARCHER_NPC_STASHED_WEAPON "archer_stashed_weapon" -#define BB_ARCHER_NPC_CHARGE_TIMER "archer_charge_timer" #define BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY "archer_npc_equipment_cache_expiry" #define BB_ARCHER_NPC_BOW "archer_npc_bow" #define BB_ARCHER_NPC_QUIVER "archer_npc_quiver" @@ -338,6 +337,7 @@ #define AI_ITEM_GUN (1<<9) #define AI_ITEM_DRINK (1<<10) // generic drinkable (not necessarily healing) #define AI_ITEM_THROWING (1<<11) +#define AI_ITEM_QUIVER (1<<12) GLOBAL_LIST_INIT(ai_item_flags, list( AI_ITEM_BANDAGE, @@ -352,6 +352,7 @@ GLOBAL_LIST_INIT(ai_item_flags, list( AI_ITEM_GUN, AI_ITEM_DRINK, AI_ITEM_THROWING, + AI_ITEM_QUIVER, )) #define AI_INVENTORY_WATCHED_SLOTS (ITEM_SLOT_BELT | ITEM_SLOT_BACK_L | ITEM_SLOT_BACK_R | \ diff --git a/code/datums/ai/subtrees/__bow_base.dm b/code/datums/ai/subtrees/__bow_base.dm index a940b1881db..9d09d65ebf6 100644 --- a/code/datums/ai/subtrees/__bow_base.dm +++ b/code/datums/ai/subtrees/__bow_base.dm @@ -1,7 +1,4 @@ /datum/ai_planning_subtree/archer_base/proc/validate_archer_equipment(datum/ai_controller/controller) - var/mob/living/carbon/human/pawn = controller.pawn - - if(world.time < controller.blackboard[BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY]) var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/cached_bow = controller.blackboard[BB_ARCHER_NPC_BOW] var/obj/item/ammo_holder/quiver/cached_quiver = controller.blackboard[BB_ARCHER_NPC_QUIVER] @@ -12,34 +9,25 @@ _clear_equipment_cache(controller) - var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/found_bow = null - for(var/obj/item/I in pawn.get_active_held_items()) - if(istype(I, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) - found_bow = I - break - if(!found_bow) - for(var/obj/item/I in pawn.get_equipped_items()) - if(istype(I, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) - found_bow = I - break - if(!found_bow) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return FALSE + + var/mob/living/living_pawn = controller.pawn + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow = inv.get_item(AI_ITEM_GUN) + if(istype(living_pawn.get_active_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_active_held_item() + else if(istype(living_pawn.get_inactive_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_inactive_held_item() + if(!bow) return FALSE - // Find a quiver compatible with this bow's ammo - var/ammo_check = found_bow?.magazine.ammo_type - var/obj/item/ammo_holder/quiver/found_quiver = null - for(var/obj/item/ammo_holder/quiver/Q in pawn.get_equipped_items()) - for(var/accepted in Q.ammo_type) - if(ispath(ammo_check, accepted)) - found_quiver = Q - break - if(found_quiver) - break - if(!found_quiver) + var/obj/item/ammo_holder/quiver/quiver = inv.get_item(AI_ITEM_QUIVER) + if(!quiver?.ammo_list.len) return FALSE - controller.set_blackboard_key(BB_ARCHER_NPC_BOW, found_bow) - controller.set_blackboard_key(BB_ARCHER_NPC_QUIVER, found_quiver) + controller.set_blackboard_key(BB_ARCHER_NPC_BOW, bow) + controller.set_blackboard_key(BB_ARCHER_NPC_QUIVER, quiver) controller.set_blackboard_key(BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY, world.time + ARCHER_NPC_EQUIPMENT_CACHE_TIME) return TRUE diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm index 671c78bd956..b595e588b65 100644 --- a/code/datums/ai/subtrees/bow_usage.dm +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -7,13 +7,33 @@ var/mob/living/carbon/human/pawn = controller.pawn var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] if(!target || !isliving(target)) + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) return - if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) - return + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] if(!Q.ammo_list.len) return + if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) + return + controller.queue_behavior(/datum/ai_behavior/ranged_attack_bow, BB_BASIC_MOB_CURRENT_TARGET) return SUBTREE_RETURN_FINISH_PLANNING @@ -85,17 +105,13 @@ xbow.cocked = TRUE xbow.update_appearance(UPDATE_ICON_STATE) - var/chargetime = ARCHER_NPC_SIMULATED_CHARGETIME - if(pawn.used_intent && pawn.used_intent.chargetime) - chargetime = pawn.used_intent.get_chargetime() - controller.set_blackboard_key(BB_ARCHER_NPC_CHARGE_TIMER, world.time + (chargetime)) - return TRUE /datum/ai_behavior/ranged_attack_bow/perform(delta_time, datum/ai_controller/controller, target_key) var/mob/living/carbon/human/pawn = controller.pawn var/atom/target = controller.blackboard[target_key] + if(!target || (ismob(target) && target:stat == DEAD)) finish_action(controller, FALSE, target_key) return @@ -105,6 +121,9 @@ finish_action(controller, FALSE, target_key) return + if(!can_see(pawn, target, 11)) + return + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null for(var/obj/item/held in pawn.get_active_held_items()) if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) @@ -114,9 +133,12 @@ finish_action(controller, FALSE, target_key) return - // Wait for simulated charge since we have no client - if(world.time < controller.blackboard[BB_ARCHER_NPC_CHARGE_TIMER]) - pawn.face_atom(target) + var/chargetime = ARCHER_NPC_SIMULATED_CHARGETIME + if(pawn.used_intent && pawn.used_intent.chargetime) + chargetime = pawn.used_intent.get_chargetime() + + if(!do_after(pawn, min(chargetime, 0.4 SECONDS), pawn)) + finish_action(controller, FALSE, target_key) return pawn.face_atom(target) @@ -127,16 +149,16 @@ /datum/ai_behavior/ranged_attack_bow/finish_action(datum/ai_controller/controller, succeeded, target_key) . = ..() var/mob/living/carbon/human/pawn = controller.pawn + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] - controller.clear_blackboard_key(BB_ARCHER_NPC_CHARGE_TIMER) - - // Re-equip stashed melee weapon - var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] - if(stashed && !QDELETED(stashed)) - if(!pawn.get_active_held_item()) - pawn.dropItemToGround(stashed, TRUE, TRUE) - pawn.put_in_active_hand(stashed) - else if(!pawn.get_inactive_held_item()) - pawn.dropItemToGround(stashed, TRUE, TRUE) - pawn.put_in_inactive_hand(stashed) - controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) + if(!succeeded || !length(Q.ammo_list)) + // Re-equip stashed melee weapon + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) diff --git a/code/datums/ai/subtrees/use_bandage.dm b/code/datums/ai/subtrees/use_bandage.dm index 3fa887d3975..52b644fee10 100644 --- a/code/datums/ai/subtrees/use_bandage.dm +++ b/code/datums/ai/subtrees/use_bandage.dm @@ -20,15 +20,14 @@ if(!real) return - controller.set_blackboard_key(BB_HELD_CONSUMABLE, bandage) - controller.queue_behavior(/datum/ai_behavior/apply_bandage, BB_HELD_CONSUMABLE) + controller.queue_behavior(/datum/ai_behavior/apply_bandage, BB_HELD_CONSUMABLE, bandage) return SUBTREE_RETURN_FINISH_PLANNING /datum/ai_behavior/apply_bandage action_cooldown = 30 SECONDS -/datum/ai_behavior/apply_bandage/perform(delta_time, datum/ai_controller/controller, consumable_key) - var/obj/item/bandage = controller.blackboard[consumable_key] +/datum/ai_behavior/apply_bandage/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/bandage) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, bandage) if(!bandage) finish_action(controller, FALSE, consumable_key) return diff --git a/code/datums/ai/subtrees/use_explosive.dm b/code/datums/ai/subtrees/use_explosive.dm index e0bbb06dd85..1c13226b1ae 100644 --- a/code/datums/ai/subtrees/use_explosive.dm +++ b/code/datums/ai/subtrees/use_explosive.dm @@ -13,15 +13,14 @@ if(!grenade) return controller.set_blackboard_key(BB_HELD_CONSUMABLE, grenade) - controller.queue_behavior(/datum/ai_behavior/throw_grenade, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET) - return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/throw_grenade, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET, grenade) /datum/ai_behavior/throw_grenade action_cooldown = 2 MINUTES // Long cooldown - grenades are precious and dangerous also cause fuck having smoke bomb spamming horcs behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION -/datum/ai_behavior/throw_grenade/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key) - var/obj/item/explosive/grenade = controller.blackboard[consumable_key] +/datum/ai_behavior/throw_grenade/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key, obj/item/explosive/grenade) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, grenade) if(!grenade) finish_action(controller, FALSE, consumable_key, target_key) return diff --git a/code/datums/ai/subtrees/use_healing_drink.dm b/code/datums/ai/subtrees/use_healing_drink.dm index 859345d462c..4f90d2cae2e 100644 --- a/code/datums/ai/subtrees/use_healing_drink.dm +++ b/code/datums/ai/subtrees/use_healing_drink.dm @@ -19,15 +19,14 @@ if(H.getBruteLoss() < 20 && H.getFireLoss() < 20) return - controller.set_blackboard_key(BB_HELD_CONSUMABLE, drink) - controller.queue_behavior(/datum/ai_behavior/consume_healing_drink, BB_HELD_CONSUMABLE) + controller.queue_behavior(/datum/ai_behavior/consume_healing_drink, BB_HELD_CONSUMABLE, drink) /datum/ai_behavior/consume_healing_drink action_cooldown = 70 SECONDS behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION -/datum/ai_behavior/consume_healing_drink/perform(delta_time, datum/ai_controller/controller, consumable_key) - var/obj/item/reagent_containers/glass/bottle/drink = controller.blackboard[consumable_key] +/datum/ai_behavior/consume_healing_drink/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/reagent_containers/glass/bottle/drink) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, drink) var/datum/component/ai_inventory_manager/inv = controller.get_inventory() var/mob/living/carbon/human/H = controller.pawn diff --git a/code/datums/ai/subtrees/use_powders.dm b/code/datums/ai/subtrees/use_powders.dm index 3f5c01ec876..f3ce661a680 100644 --- a/code/datums/ai/subtrees/use_powders.dm +++ b/code/datums/ai/subtrees/use_powders.dm @@ -11,15 +11,13 @@ var/obj/item/powder = inv.get_item(AI_ITEM_POWDER) if(!powder) return - controller.set_blackboard_key(BB_HELD_CONSUMABLE, powder) - controller.queue_behavior(/datum/ai_behavior/use_powder, BB_HELD_CONSUMABLE) - return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/use_powder, BB_HELD_CONSUMABLE, powder) /datum/ai_behavior/use_powder action_cooldown = 3 MINUTES // Very long cooldown, this is a rare treat -/datum/ai_behavior/use_powder/perform(delta_time, datum/ai_controller/controller, consumable_key) - var/obj/item/powder = controller.blackboard[consumable_key] +/datum/ai_behavior/use_powder/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/powder) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, powder) if(!powder) finish_action(controller, FALSE, consumable_key) return diff --git a/code/datums/ai/subtrees/use_throwing_weapon.dm b/code/datums/ai/subtrees/use_throwing_weapon.dm index 94bfa15d7c2..c7a960f811a 100644 --- a/code/datums/ai/subtrees/use_throwing_weapon.dm +++ b/code/datums/ai/subtrees/use_throwing_weapon.dm @@ -28,18 +28,15 @@ if(!can_see(pawn, target, max_throw_dist)) return - - controller.set_blackboard_key(BB_HELD_CONSUMABLE, throwingknife) - controller.queue_behavior(/datum/ai_behavior/use_throwable, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET) - return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/use_throwable, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET, throwingknife) /datum/ai_behavior/use_throwable - action_cooldown = 3 SECONDS + action_cooldown = 4 SECONDS behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION -/datum/ai_behavior/use_throwable/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key) +/datum/ai_behavior/use_throwable/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key, obj/item/weapon/knife/throwingknife) . = ..() - var/obj/item/weapon/knife/throwingknife = controller.blackboard[consumable_key] + controller.set_blackboard_key(BB_HELD_CONSUMABLE, throwingknife) if(!throwingknife) finish_action(controller, FALSE, consumable_key, target_key) return diff --git a/code/modules/clothing/quivers/misc.dm b/code/modules/clothing/quivers/misc.dm index d0c4841d537..c007bf27291 100644 --- a/code/modules/clothing/quivers/misc.dm +++ b/code/modules/clothing/quivers/misc.dm @@ -5,6 +5,7 @@ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_BACK max_storage = 20 ammo_type = list (/obj/item/ammo_casing/caseless/arrow, /obj/item/ammo_casing/caseless/bolt) + flags_ai_inventory = AI_ITEM_QUIVER /obj/item/ammo_holder/quiver/arrows fill_type = /obj/item/ammo_casing/caseless/arrow @@ -48,7 +49,7 @@ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_NECK max_storage = 10 ammo_type = list(/obj/item/ammo_casing/caseless/bullet) - + /obj/item/ammo_holder/bullet/bullets fill_type = /obj/item/ammo_casing/caseless/bullet diff --git a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm index 07f526f8375..7766bac8263 100644 --- a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm +++ b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm @@ -166,12 +166,13 @@ // Heavily armored orc with complete iron protection, heavy armor, and a two hander. /datum/outfit/job/orc/npc/archer_test/pre_equip(mob/living/carbon/human/H) backr = /obj/item/gun/ballistic/revolver/grenadelauncher/bow - beltr = /obj/item/ammo_holder/quiver/arrows + backl = /obj/item/ammo_holder/quiver/arrows l_hand = /obj/item/weapon/sword/short/iron armor = /obj/item/clothing/armor/leather/hide shirt = /obj/item/clothing/armor/gambeson/light pants = /obj/item/clothing/pants/trou/leather belt = /obj/item/storage/belt/leather/knifebelt/black/steel + beltr = /obj/item/storage/belt/pouch/medicine H.base_strength = 14 // GAGGER GAGGER GAGGER H.base_speed = 10 // Fast, for an orc H.base_constitution = 12 diff --git a/code/modules/projectiles/guns/ballistic/launchers.dm b/code/modules/projectiles/guns/ballistic/launchers.dm index 0c1d0cf57a2..867df88826f 100644 --- a/code/modules/projectiles/guns/ballistic/launchers.dm +++ b/code/modules/projectiles/guns/ballistic/launchers.dm @@ -10,6 +10,7 @@ w_class = WEIGHT_CLASS_NORMAL bolt_type = BOLT_TYPE_NO_BOLT istrainable = TRUE // For the moment I'll allow these to be traineable until a proper way to level up bows and crossbows is coded. - Foxtrot + flags_ai_inventory = AI_ITEM_GUN var/damfactor = 1 // Multiplier for projectile damage. Used by bows and crossbows. /obj/item/gun/ballistic/revolver/grenadelauncher/attackby(obj/item/A, mob/user, list/modifiers) From d13b48c157d6deac8b8cb27174ea5bed32965162 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:19:28 -0800 Subject: [PATCH 43/73] Update __bow_base.dm --- code/datums/ai/subtrees/__bow_base.dm | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/datums/ai/subtrees/__bow_base.dm b/code/datums/ai/subtrees/__bow_base.dm index 9d09d65ebf6..606e9ff7eb6 100644 --- a/code/datums/ai/subtrees/__bow_base.dm +++ b/code/datums/ai/subtrees/__bow_base.dm @@ -15,10 +15,11 @@ var/mob/living/living_pawn = controller.pawn var/obj/item/gun/ballistic/revolver/grenadelauncher/bow = inv.get_item(AI_ITEM_GUN) - if(istype(living_pawn.get_active_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) - bow = living_pawn.get_active_held_item() - else if(istype(living_pawn.get_inactive_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) - bow = living_pawn.get_inactive_held_item() + if(!bow) + if(istype(living_pawn.get_active_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_active_held_item() + else if(istype(living_pawn.get_inactive_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_inactive_held_item() if(!bow) return FALSE From 94a2bf698300457b24ec0da09295af9c97ca0d69 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:07:16 -0800 Subject: [PATCH 44/73] ai tweaks for longer charge times --- code/__DEFINES/ai/_ai.dm | 2 +- code/datums/ai/subtrees/bow_usage.dm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 9a2336c37d6..fd1434ce7a3 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -248,7 +248,7 @@ #define ARCHER_NPC_EQUIPMENT_CACHE_TIME (40 SECONDS) #define ARCHER_NPC_MIN_RANGE 3 // tiles - closer than this, prefer melee #define ARCHER_NPC_ARROW_SEARCH_RANGE 9 -#define ARCHER_NPC_SIMULATED_CHARGETIME 15 // fallback charge wait in deciseconds +#define ARCHER_NPC_SIMULATED_CHARGETIME 1.5 SECONDS // fallback charge wait in deciseconds #define BB_CAT_KITTEN_TARGET "BB_cat_kitten_target" diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm index b595e588b65..5db80701b6d 100644 --- a/code/datums/ai/subtrees/bow_usage.dm +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -137,7 +137,7 @@ if(pawn.used_intent && pawn.used_intent.chargetime) chargetime = pawn.used_intent.get_chargetime() - if(!do_after(pawn, min(chargetime, 0.4 SECONDS), pawn)) + if(!do_after(pawn, min(chargetime, 1 SECONDS), pawn)) finish_action(controller, FALSE, target_key) return From 595dce4e52a454060e3be287cb63204b70ec1d0a Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:11:51 -0800 Subject: [PATCH 45/73] let human npcs loot stuff as a final thing --- code/__DEFINES/ai/_ai.dm | 3 + code/datums/ai/controllers/human_npc.dm | 1 + code/datums/ai/subtrees/bow_usage.dm | 7 +- code/datums/ai/subtrees/loot.dm | 215 ++++++++++++++++++++++++ vanderlin.dme | 1 + 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 code/datums/ai/subtrees/loot.dm diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index fd1434ce7a3..f237c601e14 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -244,6 +244,9 @@ #define BB_INVENTORY_DIRTY "inventory_dirty" // bool, triggers reappraisal #define BB_HELD_CONSUMABLE "held_consumable" // item we pulled out to use #define BB_TARGET_ZONE_OVERRIDE "bb_target_override" +#define BB_LOOT_TARGET "loot_target" +#define BB_LOOT_TARGET_ITEM "loot_target_item" +#define BB_LOOT_BLACKLIST "loot_blacklist" #define ARCHER_NPC_EQUIPMENT_CACHE_TIME (40 SECONDS) #define ARCHER_NPC_MIN_RANGE 3 // tiles - closer than this, prefer melee diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 814a0326449..e93afd06430 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -40,6 +40,7 @@ /datum/ai_planning_subtree/find_weapon, /datum/ai_planning_subtree/equip_item, /datum/ai_planning_subtree/retrieve_arrows, + /datum/ai_planning_subtree/loot, ) idle_behavior = /datum/idle_behavior/idle_random_walk diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm index 5db80701b6d..17e0cde986a 100644 --- a/code/datums/ai/subtrees/bow_usage.dm +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -38,8 +38,9 @@ return SUBTREE_RETURN_FINISH_PLANNING /datum/ai_behavior/ranged_attack_bow - behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM action_cooldown = 0.2 SECONDS + required_distance = ARCHER_NPC_MIN_RANGE + 4 /datum/ai_behavior/ranged_attack_bow/setup(datum/ai_controller/controller, target_key) . = ..() @@ -105,6 +106,8 @@ xbow.cocked = TRUE xbow.update_appearance(UPDATE_ICON_STATE) + set_movement_target(controller, target) + SEND_SIGNAL(controller.pawn, COMSIG_COMBAT_TARGET_SET, TRUE) return TRUE /datum/ai_behavior/ranged_attack_bow/perform(delta_time, datum/ai_controller/controller, target_key) @@ -122,6 +125,7 @@ return if(!can_see(pawn, target, 11)) + finish_action(controller, FALSE, target_key) return var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null @@ -152,6 +156,7 @@ var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] if(!succeeded || !length(Q.ammo_list)) + controller.clear_blackboard_key(target_key) // Re-equip stashed melee weapon var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] if(stashed && !QDELETED(stashed)) diff --git a/code/datums/ai/subtrees/loot.dm b/code/datums/ai/subtrees/loot.dm new file mode 100644 index 00000000000..08e1cea1a5f --- /dev/null +++ b/code/datums/ai/subtrees/loot.dm @@ -0,0 +1,215 @@ + +/datum/ai_planning_subtree/loot + var/scan_range = 7 + var/scan_cooldown = 15 SECONDS + var/next_scan = 0 + +/datum/ai_planning_subtree/loot/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + if(controller.blackboard[BB_BASIC_MOB_FLEEING]) + return + if(next_scan > world.time) + return + next_scan = world.time + scan_cooldown + + var/mob/living/pawn = controller.pawn + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + var/list/blacklist = controller.blackboard[BB_LOOT_BLACKLIST] + + for(var/obj/item/candidate in view(scan_range, pawn)) + if(!isturf(candidate.loc)) + continue + if(_is_blacklisted(blacklist, candidate)) + continue + if(!_item_is_wanted(inv, pawn, candidate)) + continue + controller.set_blackboard_key(BB_LOOT_TARGET, candidate) + controller.queue_behavior(/datum/ai_behavior/loot_pick_up, BB_LOOT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + for(var/mob/living/corpse in orange(scan_range, pawn)) + if(corpse == pawn) + continue + if(corpse.stat != DEAD && corpse.body_position != LYING_DOWN) + continue + var/obj/item/strip_target = _find_lootable_item_on_body(inv, pawn, corpse, blacklist) + if(!strip_target) + continue + controller.set_blackboard_key(BB_LOOT_TARGET, corpse) + controller.set_blackboard_key(BB_LOOT_TARGET_ITEM, strip_target) + controller.queue_behavior(/datum/ai_behavior/loot_strip_body, BB_LOOT_TARGET, BB_LOOT_TARGET_ITEM) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/loot/proc/_is_blacklisted(list/blacklist, obj/item/candidate) + if(!blacklist) + return FALSE + for(var/datum/weakref/ref in blacklist) + if(ref.resolve() == candidate) + return TRUE + return FALSE + +/datum/ai_planning_subtree/loot/proc/_item_is_wanted(datum/component/ai_inventory_manager/inv, mob/living/pawn, obj/item/candidate) + if(!candidate.flags_ai_inventory) + return FALSE + if(istype(candidate, /obj/item/gun)) + return FALSE + if(istype(candidate, /obj/item/weapon)) + return FALSE + if(candidate.anchored) + return FALSE + if(HAS_TRAIT(candidate, TRAIT_NODROP)) + return FALSE + return TRUE + +/datum/ai_planning_subtree/loot/proc/_find_lootable_item_on_body(datum/component/ai_inventory_manager/inv, mob/living/pawn, mob/living/corpse, list/blacklist) + for(var/obj/item/held in corpse.held_items) + if(!held) + continue + if(_is_blacklisted(blacklist, held)) + continue + if(!_item_is_wanted(inv, pawn, held)) + continue + if(!held.canStrip(corpse)) + continue + return held + return null + +/proc/ai_loot_blacklist_item(datum/ai_controller/controller, obj/item/it) + controller.add_blackboard_key_lazylist(BB_LOOT_BLACKLIST, WEAKREF(it)) + // Prune it after 5 minutes so the list doesn't grow forever + addtimer(CALLBACK(controller, TYPE_PROC_REF(/datum/ai_controller, remove_thing_from_blackboard_key), BB_LOOT_BLACKLIST, WEAKREF(it)), 5 MINUTES) + + +/datum/ai_behavior/loot_pick_up + action_cooldown = 0.5 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + var/loot_delay = 2 SECONDS + +/datum/ai_behavior/loot_pick_up/setup(datum/ai_controller/controller, target_key) + . = ..() + var/obj/item/target = controller.blackboard[target_key] + if(QDELETED(target) || !isturf(target.loc)) + return FALSE + set_movement_target(controller, target) + return TRUE + +/datum/ai_behavior/loot_pick_up/perform(delta_time, datum/ai_controller/controller, target_key) + var/obj/item/target = controller.blackboard[target_key] + if(QDELETED(target) || !isturf(target.loc)) + finish_action(controller, FALSE, target_key) + return + + var/mob/living/carbon/human/pawn = controller.pawn + if(!pawn.Adjacent(target)) + finish_action(controller, FALSE, target_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + finish_action(controller, FALSE, target_key) + return + + if(QDELETED(target) || !isturf(target.loc)) + finish_action(controller, FALSE, target_key) + return + + var/slot_flag = inv.find_space_for(target) + if(!slot_flag) + pawn.visible_message(span_notice("[pawn] looks at [target] but has no room for it.")) + ai_loot_blacklist_item(controller, target) + finish_action(controller, FALSE, target_key) + return + + var/obj/item/container = inv.container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(!STR) + finish_action(controller, FALSE, target_key) + return + + STR.handle_item_insertion(target, prevent_warning = TRUE, user = pawn) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/loot_pick_up/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + + +/datum/ai_behavior/loot_strip_body + action_cooldown = 0.5 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + var/strip_delay = 3 SECONDS + +/datum/ai_behavior/loot_strip_body/setup(datum/ai_controller/controller, body_key, item_key) + . = ..() + var/mob/living/body = controller.blackboard[body_key] + if(QDELETED(body)) + return FALSE + set_movement_target(controller, body) + return TRUE + +/datum/ai_behavior/loot_strip_body/perform(delta_time, datum/ai_controller/controller, body_key, item_key) + var/mob/living/body = controller.blackboard[body_key] + var/obj/item/target_item = controller.blackboard[item_key] + + if(QDELETED(body) || QDELETED(target_item)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(body.stat != DEAD && body.body_position != LYING_DOWN) + finish_action(controller, FALSE, body_key, item_key) + return + + var/mob/living/carbon/human/pawn = controller.pawn + if(!pawn.Adjacent(body)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(!target_item.canStrip(body)) + finish_action(controller, FALSE, body_key, item_key) + return + + pawn.visible_message(span_notice("[pawn] searches [body]'s body.")) + + if(!do_after(pawn, strip_delay, body)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(QDELETED(body) || QDELETED(target_item)) + finish_action(controller, FALSE, body_key, item_key) + return + if(body.stat != DEAD && body.body_position != LYING_DOWN) + finish_action(controller, FALSE, body_key, item_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + finish_action(controller, FALSE, body_key, item_key) + return + + var/slot_flag = inv.find_space_for(target_item) + if(!slot_flag) + ai_loot_blacklist_item(controller, target_item) + finish_action(controller, FALSE, body_key, item_key) + return + + var/obj/item/container = inv.container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(!STR) + finish_action(controller, FALSE, body_key, item_key) + return + + if(target_item.doStrip(pawn, body)) + STR.handle_item_insertion(target_item, prevent_warning = TRUE, user = pawn) + finish_action(controller, TRUE, body_key, item_key) + else + ai_loot_blacklist_item(controller, target_item) + finish_action(controller, FALSE, body_key, item_key) + +/datum/ai_behavior/loot_strip_body/finish_action(datum/ai_controller/controller, succeeded, body_key, item_key) + . = ..() + controller.clear_blackboard_key(body_key) + controller.clear_blackboard_key(item_key) diff --git a/vanderlin.dme b/vanderlin.dme index 702f07dc939..46d454cde76 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -797,6 +797,7 @@ #include "code\datums\ai\subtrees\leyline_energy.dm" #include "code\datums\ai\subtrees\leyline_teleport.dm" #include "code\datums\ai\subtrees\look_for_adult.dm" +#include "code\datums\ai\subtrees\loot.dm" #include "code\datums\ai\subtrees\make_babies.dm" #include "code\datums\ai\subtrees\minotaur_fury.dm" #include "code\datums\ai\subtrees\minotaur_melee.dm" From 339027a5f8fcb62c760334ed92fabfebe3b3fd88 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:25:34 -0800 Subject: [PATCH 46/73] as per ooks request fogbeasts https://github.com/Azure-Peak/Azure-Peak/pull/5705 https://github.com/Azure-Peak/Azure-Peak/pull/4947 --- code/__DEFINES/colors.dm | 94 +++++++ code/_onclick/item_attack.dm | 72 ++++- code/game/objects/items/barding.dm | 104 +++++++ code/game/objects/items/caparison.dm | 132 +++++++++ code/modules/crafting/anvil_recipes/armor.dm | 12 + .../crafting/quality_of_crafting/sewing.dm | 36 +++ .../mob/living/simple_animal/examine.dm | 7 + .../hostile/retaliate/game/fogbeast.dm | 254 ++++++++++++++++++ .../mob/living/simple_animal/simple_animal.dm | 98 +++++++ icons/mob/monster/fogbeast.dmi | Bin 0 -> 7058 bytes icons/roguetown/items/misc.dmi | Bin 123738 -> 148175 bytes icons/roguetown/mob/monster/saiga.dmi | Bin 19833 -> 59913 bytes vanderlin.dme | 3 + 13 files changed, 807 insertions(+), 5 deletions(-) create mode 100644 code/game/objects/items/barding.dm create mode 100644 code/game/objects/items/caparison.dm create mode 100644 code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm create mode 100644 icons/mob/monster/fogbeast.dmi diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 4d7ae879b15..4770ce100fd 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -111,6 +111,46 @@ #define CLOTHING_WHITE "#ffffff" #define CLOTHING_WET "#afafaf" +#define CLOTHING_RED "#8b2323" +#define CLOTHING_PURPLE "#8747b1" +#define CLOTHING_BLACK "#2b292e" +#define CLOTHING_GREY "#6c6c6c" +#define CLOTHING_BROWN "#61462c" +#define CLOTHING_GREEN "#428138" +#define CLOTHING_DARK_GREEN "#264d26" +#define CLOTHING_BLUE "#173266" +#define CLOTHING_YELLOW "#ffcd43" +#define CLOTHING_TEAL "#249589" +#define CLOTHING_AZURE "#007fff" +#define CLOTHING_ORANGE "#df8405" +#define CLOTHING_MAGENTA "#962e5c" + +//extended dye +#define CLOTHING_BURLAP "#a09571" +#define CLOTHING_CREAM "#fffdd0" +#define CLOTHING_DARK_GREY "#505050" +#define CLOTHING_DIRT "#7c6d5c" +#define CLOTHING_DUNKED_WATER "#bbbbbb" +#define CLOTHING_EGGPLANT "#5d4356" +#define CLOTHING_GOLD "#f9a602" +#define CLOTHING_GOLD_METALLIC "#b0955d" +#define CLOTHING_GULF_BLUE "#7bb6b0" +#define CLOTHING_LIGHT_GREY "#999999" +#define CLOTHING_MADDER "#d74c34" +#define CLOTHING_MAGE_GREY "#6c6c6c" +#define CLOTHING_MUDDY_YELLOW "#b5b004" +#define CLOTHING_OLIVE "#98bf64" +#define CLOTHING_ORCHIL "#66023c" +#define CLOTHING_PERIWINKLE_BLUE "#8f99fb" +#define CLOTHING_SCARLET "#b8252c" +#define CLOTHING_TAN "#d6a790" +#define CLOTHING_VIOLET "#5b2294" +#define CLOTHING_WOAD_BLUE "#597fb9" +#define CLOTHING_WISTERIA "#b07bb6" +#define CLOTHING_WINE_RED "#995264" +#define CLOTHING_YELLOW_WELD "#f4c430" +#define CLOTHING_YARROW "#f0cb76" + /// Deprecated macro, should be removed #define CLOTHING_COLOR_NAMES list("Ash Grey","Chalk White","Cream","White","Dark Ink","Plum Purple","Salmon","Blood Red", "Maroon","Red Ochre","Russet","Chestnut","Mustard Yellow","Yellow Ochre","Forest Green","Sky Blue","Teal", "Royal Black","Soot Black","Winestain Red","Royal Red","Royal Majenta","Fyritius Orange","Bark Brown","Peasant Brown","Mud Brown","Pear Yellow","Spring Green","Bog Green","Royal Teal","Berry Blue", "Royal Blue", "Royal Purple","Dunked in Water" ) @@ -124,6 +164,60 @@ #define CM_COLOR_LUM_MAX 0.75 +#define CLOTHING_COLOR_MAP list( \ + "Red" = CLOTHING_RED, \ + "Purple" = CLOTHING_PURPLE, \ + "Black" = CLOTHING_BLACK, \ + "Brown" = CLOTHING_BROWN, \ + "Green" = CLOTHING_GREEN, \ + "Blue" = CLOTHING_BLUE, \ + "Yellow" = CLOTHING_YELLOW, \ + "Teal" = CLOTHING_TEAL, \ + "Azure" = CLOTHING_AZURE, \ + "White" = CLOTHING_WHITE, \ + "Orange" = CLOTHING_ORANGE, \ + "Magenta" = CLOTHING_MAGENTA \ +) +/* Extended */ +#define EXTENDED_COLOR_MAP list( \ + "Burlap" = CLOTHING_BURLAP, \ + "Chalk White" = CLOTHING_CHALK_WHITE, \ + "Chestnut" = CLOTHING_CHESTNUT, \ + "Cream" = CLOTHING_CREAM, \ + "Dark Grey" = CLOTHING_DARK_GREY, \ + "Dirt" = CLOTHING_DIRT, \ + "Dunked in Water" = CLOTHING_DUNKED_WATER, \ + "Eggplant" = CLOTHING_EGGPLANT, \ + "Gold" = CLOTHING_GOLD, \ + "Gold Metallic" = CLOTHING_GOLD_METALLIC, \ + "Gulf Blue" = CLOTHING_GULF_BLUE, \ + "Light Grey" = CLOTHING_LIGHT_GREY, \ + "Madder" = CLOTHING_MADDER, \ + "Mage Blue" = CLOTHING_MAGE_BLUE, \ + "Mage Green" = CLOTHING_MAGE_GREEN, \ + "Mage Grey" = CLOTHING_MAGE_GREY, \ + "Mage Yellow" = CLOTHING_MAGE_YELLOW, \ + "Muddy Yellow" = CLOTHING_MUDDY_YELLOW, \ + "Maroon" = CLOTHING_MAROON, \ + "Olive" = CLOTHING_OLIVE, \ + "Orchil" = CLOTHING_ORCHIL, \ + "Peasant Brown" = CLOTHING_PEASANT_BROWN, \ + "Periwinkle Blue" = CLOTHING_PERIWINKLE_BLUE, \ + "Red Ochre" = CLOTHING_RED_OCHRE, \ + "Russet" = CLOTHING_RUSSET, \ + "Scarlet" = CLOTHING_SCARLET, \ + "Tan" = CLOTHING_TAN, \ + "Violet" = CLOTHING_VIOLET, \ + "Woad Blue" = CLOTHING_WOAD_BLUE, \ + "Wisteria" = CLOTHING_WISTERIA, \ + "Wine Red" = CLOTHING_WINE_RED, \ + "Yellow Ochre" = CLOTHING_YELLOW_OCHRE, \ + "Yellow Weld" = CLOTHING_YELLOW_WELD, \ + "Yarrow" = CLOTHING_YARROW \ +) + +#define COLOR_MAP (CLOTHING_COLOR_MAP + EXTENDED_COLOR_MAP) + /** * Gets a color for a name, will return the same color for a given string consistently within a round.atom * diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 489bdb620ef..d742d79ff95 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -676,11 +676,73 @@ return TRUE /mob/living/simple_animal/attacked_by(obj/item/I, mob/living/user) - if(I.force < force_threshold || I.damtype == STAMINA) - playsound(src, 'sound/blank.ogg', I.get_clamped_volume(), TRUE, -1) - else - . = ..() - I.do_special_attack_effect(user, null, null, src, null) + var/hitlim = simple_limb_hit(user.zone_selected) + I.funny_attack_effects(src, user) + var/newforce = get_complex_damage(I, user) + var/haha = user.used_intent.blade_class + var/armor = run_armor_check(null, haha, armor_penetration = I.armor_penetration, damage = newforce) + var/nodmg = FALSE + next_attack_msg.Cut() + if(armor > 0) + nodmg = TRUE + next_attack_msg += span_warning("Armor stops the damage.") + apply_damage(newforce, I.damtype, hitlim, armor) + I.remove_bintegrity(1) + if(I.damtype == BRUTE && !nodmg) + if(HAS_TRAIT(src, TRAIT_SIMPLE_WOUNDS)) + simple_woundcritroll(user.used_intent.blade_class, newforce, user, hitlim) + if(newforce > 5) + if(haha != BCLASS_BLUNT) + I.add_mob_blood(src) + var/turf/location = get_turf(src) + add_splatter_floor(location) + if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood + user.add_mob_blood(src) + if(newforce > 15) + if(haha == BCLASS_BLUNT) + I.add_mob_blood(src) + var/turf/location = get_turf(src) + add_splatter_floor(location) + if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood + user.add_mob_blood(src) + send_item_attack_message(I, user, hitlim) + next_attack_msg.Cut() + I.do_special_attack_effect(user, null, null, src, null) + + +/mob/living/simple_animal/getarmor(def_zone, type, damage, armor_penetration, blade_dulling, peeldivisor, intdamfactor = 1, used_weapon) + if(!type) + return 0 + var/armorval = 0 + if(bbarding && !bbarding.obj_broken) + armorval = bbarding.armor.getRating(type) + var/intdamage = damage + if(type != "blunt") + if((damage + armor_penetration) > armorval) + intdamage = (damage + armor_penetration) - armorval + + if(intdamfactor != 1) + intdamage *= intdamfactor + + bbarding.take_damage(intdamage, damage_flag = type, sound_effect = FALSE, armor_penetration = 100) + else + if(mind) + if(armorval > 0) + intdamage -= intdamage * ((armorval / 1.66) / 100) //Reduces it up to 60% (100 dmg -> 40 dmg at Blunt S armor (100)) + if(intdamfactor != 1) + intdamage *= intdamfactor + + bbarding.take_damage(intdamage, damage_flag = type, sound_effect = FALSE, armor_penetration = 100) + + return armorval + +/mob/living/simple_animal/damage_clothes(damage_amount, damage_type = BRUTE, damage_flag = 0, def_zone) + if(damage_type != BRUTE && damage_type != BURN) + return + if(!bbarding) + return + damage_amount *= 0.5 //0.5 multiplier for balance reason, we don't want clothes to be too easily destroyed + bbarding.take_damage(damage_amount, damage_type, damage_flag, 0) /** * Last proc in the [/obj/item/proc/melee_attack_chain] diff --git a/code/game/objects/items/barding.dm b/code/game/objects/items/barding.dm new file mode 100644 index 00000000000..ba5834d749b --- /dev/null +++ b/code/game/objects/items/barding.dm @@ -0,0 +1,104 @@ +/obj/item/clothing/barding + name = "padded barding" + desc = "A set of padded body armor for a Saiga, designed to protect your mount's vital organs." + slot_flags = null + icon = 'icons/roguetown/items/misc.dmi' + icon_state = "sewingkit" + var/barding_icon = 'icons/roguetown/mob/monster/saiga.dmi' + var/barding_state = "barding" + var/female_barding_state = "barding-f" + gender = NEUTER + var/list/valid_animal_types = list( + /mob/living/simple_animal/hostile/retaliate/saiga + ) + armor = ARMOR_PADDED_GOOD + max_integrity = ARMOR_INT_CHEST_LIGHT_MASTER + break_sound = 'sound/foley/cloth_rip.ogg' + drop_sound = 'sound/foley/dropsound/cloth_drop.ogg' + sewrepair = TRUE + salvage_result = /obj/item/natural/cloth + salvage_amount = 1 + fiber_salvage = TRUE + integrity_failure = 0.1 + +/obj/item/clothing/barding/attack(mob/living/M, mob/living/user) + if(!istype(M, /mob/living/simple_animal)) + to_chat(user, span_warning("\The [src] can only be used on animals!")) + return + if(!is_type_in_list(M, valid_animal_types)) + to_chat(user, span_warning("\The [src] cannot be used on [M]! It is only meant for specific animals.")) + return + + var/mob/living/simple_animal/animal = M + if(animal.adult_growth) + to_chat(user, span_warning("[animal] is a juvenile and cannot wear a bard!")) + return + if(animal.bbarding) + to_chat(user, span_warning("[animal] is already wearing a bard!")) + return + if(!animal.ssaddle) + to_chat(user, span_warning("[animal] needs to be saddled before you can fit a bard onto it!")) + return + + user.visible_message(span_notice("[user] is fitting a bard onto [animal]..."), span_notice("I start fitting a bard onto [animal]...")) + if(!do_after(user, 5 SECONDS, animal)) + return + + animal.bbarding = src + forceMove(animal) + animal.update_icon() + user.visible_message(span_notice("[user] fits a bard onto [animal]."), span_notice("I fit a bard onto [animal].")) + +/obj/item/clothing/barding/atom_break(damage_flag) + . = ..() + if(istype(loc, /mob/living/simple_animal)) + var/mob/living/simple_animal/A = loc + if(A.bbarding == src) + A.bbarding = null + . = ..() + +/obj/item/clothing/barding/chain + name = "chainmail barding" + desc = "A set of chainmail body armor for a Saiga, designed to protect your mount's vital organs." + icon_state = "armorkit" + barding_state = "barding_chain" + female_barding_state = "barding_chain-f" + armor = ARMOR_MAILLE + max_integrity = ARMOR_INT_CHEST_MEDIUM_STEEL + drop_sound = 'sound/foley/dropsound/chain_drop.ogg' + pickup_sound = 'sound/foley/equip/equip_armor_chain.ogg' + anvilrepair = /datum/skill/craft/armorsmithing + smeltresult = /obj/item/ingot/steel + sewrepair = FALSE + salvage_result = null + salvage_amount = 0 + fiber_salvage = FALSE + +/obj/item/clothing/barding/fogbeast + name = "padded barding" + desc = "A set of padded body armor for a Fogbeast, designed to protect your mount's vital organs." + icon_state = "sewingkit" + barding_icon = 'icons/mob/monster/fogbeast.dmi' + barding_state = "barding" + female_barding_state = "barding" + valid_animal_types = list( + /mob/living/simple_animal/hostile/retaliate/fogbeast + ) + +/obj/item/clothing/barding/fogbeast/chain + name = "chainmail barding" + desc = "A set of chainmail body armor for a Fogbeast, designed to protect your mount's vital organs." + icon_state = "armorkit" + barding_state = "barding_chain" + female_barding_state = "barding_chain" + armor = ARMOR_MAILLE + max_integrity = ARMOR_INT_CHEST_MEDIUM_STEEL + drop_sound = 'sound/foley/dropsound/chain_drop.ogg' + pickup_sound = 'sound/foley/equip/equip_armor_chain.ogg' + anvilrepair = /datum/skill/craft/armorsmithing + melting_material = /datum/material/steel + melt_amount = 80 + sewrepair = FALSE + salvage_result = null + salvage_amount = 0 + fiber_salvage = FALSE diff --git a/code/game/objects/items/caparison.dm b/code/game/objects/items/caparison.dm new file mode 100644 index 00000000000..b4f81505d4d --- /dev/null +++ b/code/game/objects/items/caparison.dm @@ -0,0 +1,132 @@ +/obj/item/caparison + name = "caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Saiga." + icon = 'icons/roguetown/items/misc.dmi' + icon_state = "caparison" + var/caparison_icon = 'icons/roguetown/mob/monster/saiga.dmi' + var/caparison_state = "caparison" + var/detail_state + var/list/detail_types + var/list/symbol_types + var/female_caparison_state = "caparison-f" + gender = NEUTER + var/list/valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/saiga) + +/obj/item/caparison/attack(mob/living/M, mob/living/user) + if(!istype(M, /mob/living/simple_animal)) + to_chat(user, span_warning("\The [src] can only be used on animals!")) + return + if(!is_type_in_list(M, valid_animal_types)) + to_chat(user, span_warning("\The [src] cannot be used on [M]! It is only meant for specific animals.")) + return + + var/mob/living/simple_animal/animal = M + if(animal.adult_growth) + to_chat(user, span_warning("[animal] is a juvenile and cannot wear a caparison!")) + return + if(animal.ccaparison) + to_chat(user, span_warning("[animal] is already wearing a caparison!")) + return + if(!animal.ssaddle) + to_chat(user, span_warning("[animal] needs to be saddled before you can fit a caparison onto it!")) + return + + user.visible_message(span_notice("[user] is fitting a caparison onto [animal]..."), span_notice("I start fitting a caparison onto [animal]...")) + if(!do_after(user, 5 SECONDS, animal)) + return + + animal.ccaparison = src + forceMove(animal) + animal.update_icon() + user.visible_message(span_notice("[user] fits a caparison onto [animal]."), span_notice("I fit a caparison onto [animal].")) + + +/obj/item/caparison/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(!length(detail_types)) + return + + var/list/possible_detail_types = list("None" = null) + detail_types.Copy() + if(length(symbol_types)) + possible_detail_types += list("Symbol" = null) + + var/chosen_design = input(user, "Select a design.", "Caparison Design") as null|anything in possible_detail_types + if(!chosen_design) + return + + if(chosen_design == "Symbol") + var/chosen_symbol = input(user, "Select a symbol.", "Caparison Design") as null|anything in symbol_types + if(!chosen_symbol) + return + detail_state = symbol_types[chosen_symbol] + else + detail_state = detail_types[chosen_design] + + var/list/colors_to_pick = list() + if(GLOB.lordprimary) + colors_to_pick["Primary Keep Color"] = GLOB.lordprimary + if(GLOB.lordsecondary) + colors_to_pick["Secondary Keep Color"] = GLOB.lordsecondary + var/list/color_map_list = COLOR_MAP + colors_to_pick += color_map_list.Copy() + + var/primary_color = input(user, "Select a primary color.", "Caparison Design") as null|anything in colors_to_pick + if(!primary_color) + return + color = colors_to_pick[primary_color] + + if(chosen_design != "None") + if(chosen_design != "Symbol") + var/secondary_color = input(user, "Select a secondary color.", "Caparison Design") as null|anything in colors_to_pick + if(!secondary_color) + return + detail_color = colors_to_pick[secondary_color] + else + detail_color = COLOR_WHITE + +////////////////////// +// SUBTYPES - SAIGA // +////////////////////// + +/obj/item/caparison/psy + name = "psydonite caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Psycrosses. This one fits on a Saiga." + caparison_state = "psy_caparison" + female_caparison_state = "psy_caparison-f" + +/obj/item/caparison/astrata + name = "astratan caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Astratan crosses. This one fits on a Saiga." + caparison_state = "astra_caparison" + female_caparison_state = "astra_caparison-f" + +/obj/item/caparison/eora + name = "eoran caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Eoran hearts. This one fits on a Saiga." + caparison_state = "eora_caparison" + female_caparison_state = "eora_caparison-f" + +/obj/item/caparison/azure + name = "azurean caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with ducal colours. This one fits on a Saiga." + caparison_state = "azure_caparison" + female_caparison_state = "azure_caparison-f" + +/obj/item/caparison/heartfelt + name = "Heartfelt caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with the colours of Heartfelt. This one fits on a Saiga." + caparison_state = "heartfelt_caparison" + female_caparison_state = "heartfelt_caparison-f" + +///////////////////////// +// SUBTYPES - FOGBEAST // +///////////////////////// + +/obj/item/caparison/fogbeast + name = "caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Fogbeast." + caparison_icon = 'icons/mob/monster/fogbeast.dmi' + valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/fogbeast) + color = COLOR_WHITE + detail_types = list("Quad" = "quad") + symbol_types = list("Psycross" = "psycross", "Astrata" = "astrata") diff --git a/code/modules/crafting/anvil_recipes/armor.dm b/code/modules/crafting/anvil_recipes/armor.dm index 8ef6310d529..6090e253206 100644 --- a/code/modules/crafting/anvil_recipes/armor.dm +++ b/code/modules/crafting/anvil_recipes/armor.dm @@ -756,6 +756,18 @@ created_item = (/obj/item/clothing/head/helmet/visored/hounskull) craftdiff = 4 +/datum/anvil_recipe/armor/steel/barding + name = "Saiga Barding, Chainmail (+1 Steel)" + req_bar = /obj/item/ingot/steel + additional_items = list(/obj/item/ingot/steel) + created_item = /obj/item/clothing/barding/chain + +/datum/anvil_recipe/armor/steel/barding/fogbeast + name = "Fogbeast Barding, Chainmail (+1 Steel)" + req_bar = /obj/item/ingot/steel + additional_items = list(/obj/item/ingot/steel) + created_item = /obj/item/clothing/barding/fogbeast/chain + /* /datum/anvil_recipe/armor/steel/warden_helm name = "Warden Helmet (+Bar)" diff --git a/code/modules/crafting/quality_of_crafting/sewing.dm b/code/modules/crafting/quality_of_crafting/sewing.dm index 2aff5cadb27..623ec22c6be 100644 --- a/code/modules/crafting/quality_of_crafting/sewing.dm +++ b/code/modules/crafting/quality_of_crafting/sewing.dm @@ -1731,3 +1731,39 @@ ) craftdiff = 2 category = "Gloves" + +/datum/repeatable_crafting_recipe/sewing/barding + name = "padded barding (saiga)" + category = "Armor" + output = /obj/item/clothing/barding + requirements = list(/obj/item/natural/cloth = 4, + /obj/item/natural/fibers = 1) + craftdiff = 3 + +/datum/repeatable_crafting_recipe/sewing/barding/fogbeast + name = "padded barding (fogbeast)" + output = /obj/item/clothing/barding/fogbeast + +/datum/repeatable_crafting_recipe/sewing/caparison + name = "caparison" + category = "Armor" + output =/obj/item/caparison + requirements = list(/obj/item/natural/cloth = 4, + /obj/item/natural/fibers = 2) + craftdiff = 2 + +/datum/repeatable_crafting_recipe/sewing/caparison/psy + name = "psydonite caparison" + output =/obj/item/caparison/psy + +/datum/repeatable_crafting_recipe/sewing/caparison/astrata + name = "astratan caparison" + output =/obj/item/caparison/astrata + +/datum/repeatable_crafting_recipe/sewing/caparison/eora + name = "eoran caparison" + output =/obj/item/caparison/eora + +/datum/repeatable_crafting_recipe/sewing/caparison/fogbeast + name = "fogbeast caparison" + output =/obj/item/caparison/fogbeast diff --git a/code/modules/mob/living/simple_animal/examine.dm b/code/modules/mob/living/simple_animal/examine.dm index 4d180405fb7..7b03bb0e0d2 100644 --- a/code/modules/mob/living/simple_animal/examine.dm +++ b/code/modules/mob/living/simple_animal/examine.dm @@ -108,5 +108,12 @@ if(desc) . += desc + if(ssaddle) + . += span_notice("This animal is saddled: ([ssaddle.name]).") + if(ccaparison) + . += span_notice("This animal is wearing a caparison: ([ccaparison.name]).") + if(bbarding) + . += span_notice("This animal is wearing a bard: ([bbarding.name]).") + . += "ᛉ ------------ ᛉ" SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm new file mode 100644 index 00000000000..a837cb14b32 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm @@ -0,0 +1,254 @@ +GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_GRAY, "Black" = COLOR_ALMOST_BLACK, "Brown" = COLOR_DARK_BROWN, "Chestnut" = COLOR_DARK_ORANGE)) + +/mob/living/simple_animal/hostile/retaliate/fogbeast + name = "fogbeast mare" + desc = "A distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." + icon = 'icons/mob/monster/fogbeast.dmi' + icon_state = "fogbeast" + icon_living = "fogbeast" + icon_dead = "fogbeast_dead" + icon_gib = "saiga_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_see = list("looks around.", "chews some leaves.", "neighs") + speak_chance = 1 + see_in_dark = 6 + move_to_delay = 8 + butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/reagent_containers/food/snacks/fat = 2, + /obj/item/natural/hide = 4, + /obj/item/natural/bundle/bone/full = 1 + ) + base_intents = list(/datum/intent/simple/fogbeast) + animal_species = /mob/living/simple_animal/hostile/retaliate/fogbeast/male + health = 380 + maxHealth = 380 + food_type = list( + /obj/item/reagent_containers/food/snacks/produce/grain/wheat, + /obj/item/reagent_containers/food/snacks/produce/grain/oat, + /obj/item/reagent_containers/food/snacks/produce/fruit/apple + ) + tame_chance = 15 + bonus_tame_chance = 15 + footstep_type = FOOTSTEP_MOB_SHOE + pooptype = /obj/item/natural/poo/horse + faction = list("horse") + attack_verb_continuous = "tramples" + attack_verb_simple = "kicks" + melee_damage_lower = 50 + melee_damage_upper = 70 + retreat_distance = 0 + minimum_distance = 10 + base_speed = 15 + base_constitution = 8 + base_strength = 12 + base_endurance = 15 + pixel_x = -8 + attack_sound = list('sound/vo/mobs/saiga/attack (1).ogg','sound/vo/mobs/saiga/attack (2).ogg') + can_buckle = TRUE + buckle_lying = 0 + can_saddle = TRUE + aggressive = TRUE + remains_type = /obj/effect/decal/remains/saiga + ai_controller = /datum/ai_controller/saiga + var/fogbeast_color + var/can_breed = TRUE + +/mob/living/simple_animal/hostile/retaliate/fogbeast/Initialize(mapload, var/set_fogbeast_color) + . = ..() + fogbeast_color = set_fogbeast_color + if(!fogbeast_color) + fogbeast_color = pick(GLOB.valid_fogbeast_colors) + color = GLOB.valid_fogbeast_colors[fogbeast_color] + + if(can_breed) + AddComponent(\ + /datum/component/breed,\ + list(/mob/living/simple_animal/hostile/retaliate/fogbeast),\ + 3 MINUTES,\ + list(/mob/living/simple_animal/hostile/retaliate/fogbeast/kid = 70, /mob/living/simple_animal/hostile/retaliate/fogbeast/kid/male = 30),\ + CALLBACK(src, PROC_REF(after_birth)),\ + ) + +/mob/living/simple_animal/hostile/retaliate/fogbeast/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) + return + +/mob/living/simple_animal/hostile/retaliate/fogbeast/tame + tame = TRUE + +/mob/living/simple_animal/hostile/retaliate/fogbeast/tame/saddled/Initialize() + . = ..() + var/obj/item/natural/saddle/S = new(src) + ssaddle = S + update_icon() + +// BEHAVIORS +/mob/living/simple_animal/hostile/retaliate/fogbeast/update_icon() + cut_overlays() + ..() + if(stat != DEAD) + if(ssaddle) + var/mutable_appearance/saddlet = mutable_appearance(icon, "saddle-above", 4.3) + saddlet.appearance_flags = RESET_ALPHA|RESET_COLOR + add_overlay(saddlet) + saddlet = mutable_appearance(icon, "saddle") + saddlet.appearance_flags = RESET_ALPHA|RESET_COLOR + add_overlay(saddlet) + if(has_buckled_mobs()) + var/mutable_appearance/mounted = mutable_appearance(icon, "[icon_state]_mounted", 4.3) + add_overlay(mounted) + + +/mob/living/simple_animal/hostile/retaliate/fogbeast/get_sound(input) + switch(input) + if("aggro") + return pick('sound/vo/mobs/saiga/attack (1).ogg','sound/vo/mobs/saiga/attack (2).ogg') + if("pain") + return pick('sound/vo/mobs/saiga/pain (1).ogg','sound/vo/mobs/saiga/pain (2).ogg','sound/vo/mobs/saiga/pain (3).ogg') + if("death") + return pick('sound/vo/mobs/saiga/death (1).ogg','sound/vo/mobs/saiga/death (2).ogg') + if("idle") + return pick('sound/vo/mobs/saiga/idle (1).ogg','sound/vo/mobs/saiga/idle (2).ogg','sound/vo/mobs/saiga/idle (3).ogg','sound/vo/mobs/saiga/idle (4).ogg','sound/vo/mobs/saiga/idle (5).ogg','sound/vo/mobs/saiga/idle (6).ogg','sound/vo/mobs/saiga/idle (7).ogg') + + +/mob/living/simple_animal/hostile/retaliate/fogbeast/tamed() + ..() + deaggroprob = 20 + if(can_buckle) + AddComponent(/datum/component/riding/saiga) + +/mob/living/simple_animal/hostile/retaliate/fogbeast/death() + unbuckle_all_mobs() + return ..() + +/mob/living/simple_animal/hostile/retaliate/fogbeast/simple_limb_hit(zone) + if(!zone) + return "" + switch(zone) + if(BODY_ZONE_PRECISE_R_EYE) + return "head" + if(BODY_ZONE_PRECISE_L_EYE) + return "head" + if(BODY_ZONE_PRECISE_NOSE) + return "snout" + if(BODY_ZONE_PRECISE_MOUTH) + return "snout" + if(BODY_ZONE_PRECISE_SKULL) + return "head" + if(BODY_ZONE_PRECISE_EARS) + return "head" + if(BODY_ZONE_PRECISE_NECK) + return "neck" + if(BODY_ZONE_PRECISE_L_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_R_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_L_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_R_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_STOMACH) + return "stomach" + if(BODY_ZONE_HEAD) + return "head" + if(BODY_ZONE_R_LEG) + return "leg" + if(BODY_ZONE_L_LEG) + return "leg" + if(BODY_ZONE_R_ARM) + return "foreleg" + if(BODY_ZONE_L_ARM) + return "foreleg" + return ..() + +/// If we're a mount and are hit while sprinting, throw our rider off +/// Also called if the rider is hit +/mob/living/simple_animal/hostile/retaliate/fogbeast/proc/check_sprint_dismount() + SIGNAL_HANDLER + for(var/mob/living/carbon/human/rider in buckled_mobs) + if(rider.m_intent == MOVE_INTENT_RUN) + var/rider_skill = rider.get_skill_level(/datum/skill/misc/riding) + if(rider_skill < SKILL_LEVEL_MASTER) + violent_dismount(rider) + +/mob/living/simple_animal/hostile/retaliate/fogbeast/post_buckle_mob(mob/living/M) + . = ..() + RegisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + if(!has_buckled_mobs()) + RegisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + +/mob/living/simple_animal/hostile/retaliate/fogbeast/post_unbuckle_mob(mob/living/M) + . = ..() + UnregisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + if(!has_buckled_mobs()) + UnregisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + +/obj/effect/decal/remains/fogbeast + name = "remains" + desc = "The remains of a once-proud fogbeast. Perhaps it was killed for food, or slain in battle with a valiant knight atop?" + gender = PLURAL + icon_state = "skele" + icon = 'icons/mob/monster/fogbeast.dmi' + +/mob/living/simple_animal/hostile/retaliate/fogbeast/male + name = "fogbeast stallion" + gender = MALE + +/mob/living/simple_animal/hostile/retaliate/fogbeast/male/tame + tame = TRUE + +/mob/living/simple_animal/hostile/retaliate/fogbeast/male/tame/saddled/Initialize() + . = ..() + var/obj/item/natural/saddle/S = new(src) + ssaddle = S + update_icon() + +// FOAL +/mob/living/simple_animal/hostile/retaliate/fogbeast/kid + name = "fogbeast filly" + desc = "A young fogbeast, likely to be running around with its mother. Fogbeasts are a distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." + icon = 'icons/mob/monster/fogbeast.dmi' + icon_state = "foggie" + icon_living = "foggie" + icon_dead = "foggie_dead" + icon_gib = "foggie_dead" + animal_species = null + emote_see = list("looks around.", "chews some leaves.", "neighs", "hops about playfully") + animal_species = null + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, /obj/item/alch/bone = 3) + health = 20 + maxHealth = 20 + pass_flags = PASSTABLE | PASSMOB + mob_size = MOB_SIZE_SMALL + melee_damage_lower = 1 + melee_damage_upper = 6 + base_constitution = 5 + base_strength = 5 + base_speed = 5 + adult_growth = /mob/living/simple_animal/hostile/retaliate/fogbeast + tame = TRUE + can_buckle = FALSE + can_saddle = FALSE + aggressive = TRUE + ai_controller = /datum/ai_controller/saiga_kid + can_breed = FALSE + +/mob/living/simple_animal/hostile/retaliate/fogbeast/kid/male + name = "fogbeast colt" + adult_growth = /mob/living/simple_animal/hostile/retaliate/fogbeast/male + +// INTENT +/datum/intent/simple/fogbeast + name = "horse" + icon_state = "instrike" + attack_verb = list("tramples", "rams", "kicks") + animname = "blank22" + blade_class = BCLASS_BLUNT + hitsound = "punch_hard" + chargetime = 0 + penfactor = 10 + swingdelay = 0 + candodge = TRUE + canparry = TRUE + item_damage_type = "blunt" + clickcd = CLICK_CD_MELEE * 1.1 diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index c543b438066..696815a3371 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -170,6 +170,10 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) "best_friend" = 100 ) + var/obj/item/caparison/ccaparison + var/obj/item/clothing/barding/bbarding + var/caparison_over_barding = FALSE + /mob/living/simple_animal/Initialize() . = ..() if(gender == PLURAL) @@ -200,8 +204,73 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(ssaddle) QDEL_NULL(ssaddle) + if(ccaparison) + QDEL_NULL(ccaparison) + ccaparison = null + return ..() + +/mob/living/simple_animal/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(ccaparison) + user.visible_message(span_notice("[user] is removing the caparison from [src]..."), span_notice("I start removing the caparison from [src]...")) + if(!do_after(user, 10 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the caparison from [src]."), span_notice("I remove the caparison from [src].")) + var/obj/item/caparison/C = ccaparison + ccaparison = null + C.forceMove(get_turf(src)) + user.put_in_hands(C) + update_appearance() + return + else if(bbarding) + user.visible_message(span_notice("[user] is removing the bard from [src]..."), span_notice("I start removing the bard from [src]...")) + if(!do_after(user, 10 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the bard from [src]."), span_notice("I remove the bard from [src].")) + var/obj/item/clothing/barding/B = bbarding + bbarding = null + B.forceMove(get_turf(src)) + user.put_in_hands(B) + update_appearance() + return + else if(ssaddle) + user.visible_message(span_notice("[user] is removing the saddle from [src]..."), span_notice("I start removing the saddle from [src]...")) + if(!do_after(user, 5 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the saddle from [src]."), span_notice("I remove the saddle from [src].")) + var/obj/item/natural/saddle/S = ssaddle + ssaddle = null + S.forceMove(get_turf(src)) + user.put_in_hands(S) + update_appearance() + return + return ..() + +/mob/living/simple_animal/update_overlays() + . = ..() + var/barding_layer = 6 + var/caparison_layer = 5 + if(caparison_over_barding) + caparison_layer = 6 + barding_layer = 5 + if(ccaparison && stat == CONSCIOUS && !resting) + var/caparison_overlay = ccaparison.female_caparison_state && gender == FEMALE ? ccaparison.female_caparison_state : ccaparison.caparison_state + var/mutable_appearance/caparison_base_overlay = mutable_appearance(ccaparison.caparison_icon, caparison_overlay, caparison_layer) + var/mutable_appearance/caparison_above_overlay = mutable_appearance(ccaparison.caparison_icon, caparison_overlay + "-above", caparison_layer - 0.69) + . += caparison_base_overlay + . += caparison_above_overlay + if(bbarding && stat == CONSCIOUS && !resting) + var/barding_overlay = bbarding.female_barding_state && gender == FEMALE ? bbarding.female_barding_state : bbarding.barding_state + var/mutable_appearance/barding_base_overlay = mutable_appearance(bbarding.barding_icon, barding_overlay, barding_layer) + var/mutable_appearance/barding_above_overlay = mutable_appearance(bbarding.barding_icon, barding_overlay + "-above", barding_layer - 0.69) + . += barding_base_overlay + . += barding_above_overlay + /mob/living/simple_animal/attackby(obj/item/O, mob/user, list/modifiers) if(!is_type_in_list(O, food_type)) return ..() @@ -265,6 +334,7 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(user) owner = user + update_appearance() //mob/living/simple_animal/examine(mob/user) // . = ..() @@ -390,6 +460,25 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) playsound(src, 'sound/foley/gross.ogg', 100, FALSE) if(do_after(user, 3 SECONDS, src)) butcher(user) + else if (stat != DEAD && istype(ssaddle, /obj/item/natural/saddle) && bbarding && ccaparison) + var/pick = browser_alert(user, "What would you like to do?", "[src.name]", list("Adjust caparison", "Look through the saddle bags")) + if(!pick) + pick = "Look through the saddle bags" + switch(pick) + if("Adjust caparison") + caparison_over_barding = !caparison_over_barding + to_chat(user, span_info("I [caparison_over_barding ? "adjust [ccaparison] to cover [bbarding]" : "adjust [ccaparison] to be under [bbarding]"].")) + update_appearance() + if("Look through the saddle bags") + var/datum/component/storage/saddle_storage = ssaddle.GetComponent(/datum/component/storage) + var/access_time = (user in buckled_mobs) ? 10 : 30 + if (do_after(user, access_time, target = src)) + saddle_storage.show_to(user) + else if(bbarding && ccaparison) + caparison_over_barding = !caparison_over_barding + to_chat(user, span_info("I [caparison_over_barding ? "adjust [ccaparison] to cover [bbarding]" : "adjust [ccaparison] to be under [bbarding]"].")) + update_appearance() + ..() /mob/living/simple_animal/proc/butcher(mob/living/user) @@ -886,6 +975,15 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) playsound(L, 'sound/foley/zfall.ogg', 100, FALSE) L.visible_message(span_danger("[L] falls off [src]!")) +/mob/living/simple_animal/proc/violent_dismount(mob/living/user) + if(isliving(user)) + var/mob/living/L = user + unbuckle_mob(L) + L.Paralyze(5 SECONDS) + L.Stun(5 SECONDS) + playsound(L.loc, 'sound/foley/zfall.ogg', 100, FALSE) + L.visible_message(span_danger("[L] falls off [src]!")) + /mob/living/simple_animal/buckle_mob(mob/living/buckled_mob, force = 0, check_loc = 1) . = ..() LoadComponent(/datum/component/riding) diff --git a/icons/mob/monster/fogbeast.dmi b/icons/mob/monster/fogbeast.dmi new file mode 100644 index 0000000000000000000000000000000000000000..492f17630c70466c1cf2fb58a82825be068935be GIT binary patch literal 7058 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 literal 123738 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ξ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

VVRRT$-Cv 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 Gj{0x-IQ+~2 literal 59913 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 z zXa#AB_a)^Yp+9%+Qi z5O8>*J||qf{2@Am6yx^7HkfjN<&qVa8y%snJT6}lI=9A#s4TH#9%|C;Qc;6I@#O@i z8;b!s#6nsgB(C2mB;@nobziNi;U3z;WVn;4BD_ z9&FeGjT#{ zv+zb+lK@lRqx#Ynl!vqVQXB5X-<$ivd6pQ^xqV{02|cc(nKl1lZ3NSn3KXp?5Ol(Z z=0ktjYKP84Nt1%qOwF$#y|}B+Wrb}sQ}q)M^Mo!Q0+55IdPS8xN|emdxs*jO=?3L_ ztYkhsSa_?iZ_6h85b7gypr@Oa3F^zBnFf|nOQAHEV9sxMJJ$+F(26iKFe&^yRk7Q@6#&)E z7YM1z8%f@=`=sqDQa+Mia|(=sppaO})a)wdr6dI7ymA3s_sDdY=9}JT>iW8eccr*R(v@JWhr&1pfI7Z6old(bP2=;3=?2k^z5mf^+f;tnz}F4;^*)$M!yFsgZRSD2F(+d*`HsI9)x{*Pm3Jq*q>L zyu9X4bow-aE)?#XdcUbdMnt$WC_qSfUpT^*0sL^9ou1R`R{ zI>4N$Qip_43vaCZQ7nS$O)MIbB`DdRj#$|(m%n<-H+Bgc2C7S+*=kn5R=|VlyM925 zm!wXE=A(9>?}W2u9*J zdG}%c#Z9WDRPB|Gc5$Zay-3vPzvugCsV~Mm!i$`#_hBSef7AQa3RGcj)6=wO%k6i| zM}7@ATqYC4Zhwkt4k5Spml>56N6aYv6TxD|flpiI#g<$g80{`UZRq^Qg#){2mcU7;Ug8Ni|T}%wSNL{aKSAA diff --git a/vanderlin.dme b/vanderlin.dme index 2c1c1dc5570..963867d66d0 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -27,6 +27,7 @@ #include "code\__DEFINES\achievements.dm" #include "code\__DEFINES\actions.dm" #include "code\__DEFINES\admin.dm" +#include "code\__DEFINES\animal_gene.dm" #include "code\__DEFINES\animals.dm" #include "code\__DEFINES\antagonists.dm" #include "code\__DEFINES\area.dm" @@ -3358,6 +3359,27 @@ #include "code\modules\mob\living\simple_animal\topic.dm" #include "code\modules\mob\living\simple_animal\friendly\cat.dm" #include "code\modules\mob\living\simple_animal\friendly\pet.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genetics.dm" +#include "code\modules\mob\living\simple_animal\genetics\genetic_types.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\_animal_gene.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\aggressive.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\barren.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\coat.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\diet.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\docile.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\dominant_lineage.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\fat.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\fecundity.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\frail.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\glowing_undercoat.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\hardy.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\hide.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\lean.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\productive.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\prolific.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\slugish.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\swift.dm" +#include "code\modules\mob\living\simple_animal\genetics\animal_genes\undercoat.dm" #include "code\modules\mob\living\simple_animal\hostile\crow.dm" #include "code\modules\mob\living\simple_animal\hostile\gnome.dm" #include "code\modules\mob\living\simple_animal\hostile\haunt.dm" From e619d84b6dcbe7f98b4e08bb4d890e8ab199c8d3 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:31:24 -0800 Subject: [PATCH 49/73] Update definitions.dm --- code/__DEFINES/traits/definitions.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index 5dbc79a93b9..d6cf70ecb9a 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -227,6 +227,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BLUEPRINT_VISION "blueprint_vision" /// Used to limit healing to putrid flesh mobs #define TRAIT_PUTRID "Putrid" +#define TRAIT_STUCKITEMS "stuck_items" // Prevents removing items except for hand slots /// Confessed under torture, to force sign #define TRAIT_HAS_CONFESSED "has_confessed" /// Confessed for specific type of antag @@ -431,7 +432,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_SATE "SATE" #define TRAIT_NODE_EXTRACTED "Humors Extracted" #define TRAIT_NO_EXPERIENCE "unlearning" -#define TRAIT_STUCKITEMS "stuck_items" // Prevents removing items except for hand slots /// This mob should never close UI even if it doesn't have a client #define TRAIT_PRESERVE_UI_WITHOUT_CLIENT "preserve_ui_without_client" From b0e73989188ab43b92b7c01d6072960b97737ff6 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:32:26 -0800 Subject: [PATCH 50/73] Update simple_animal.dm --- .../mob/living/simple_animal/simple_animal.dm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 7cbf19f0e0d..778bf7df122 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -1000,6 +1000,12 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) playsound(L.loc, 'sound/foley/zfall.ogg', 100, FALSE) L.visible_message(span_danger("[L] falls off [src]!")) +/mob/living/simple_animal/proc/apply_gene(datum/animal_gene/G) + G.apply_to(src) + +/mob/living/simple_animal/proc/remove_gene(datum/animal_gene/G) + G.remove_from(src) + /mob/living/simple_animal/buckle_mob(mob/living/buckled_mob, force = 0, check_loc = 1) . = ..() LoadComponent(/datum/component/riding) @@ -1037,9 +1043,3 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) /mob/living/simple_animal/proc/eat_food_after(obj/item/reagent_containers/food/snacks/eaten) qdel(eaten) - -/mob/living/simple_animal/proc/apply_gene(datum/animal_gene/G) - G.apply_to(src) - -/mob/living/simple_animal/proc/remove_gene(datum/animal_gene/G) - G.remove_from(src) From 834fc6994f92bc23d918fbac021b449313abf4cd Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:46:24 -0800 Subject: [PATCH 51/73] Update simple_animal.dm --- code/modules/mob/living/simple_animal/simple_animal.dm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 778bf7df122..137ac19166d 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -218,7 +218,8 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) QDEL_NULL(ccaparison) ccaparison = null - QDEL_NULL(genetics) + if(!ispath(genetics)) + QDEL_NULL(genetics) return ..() From cbdd06027b81c4e604c4cc618042d1e8a3e9448f Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:57:55 -0800 Subject: [PATCH 52/73] fixes --- .../mob/living/simple_animal/genetics/animal_genes/slugish.dm | 4 ++-- .../mob/living/simple_animal/genetics/animal_genes/swift.dm | 4 ++-- .../mob/living/simple_animal/genetics/animal_genetics.dm | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm index 2c8353b2085..8e78e11342b 100644 --- a/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm @@ -12,7 +12,7 @@ var/penalty = round(intensity) target.genetic_speed_delta += penalty if(target.ai_controller) - target.ai_controller.movement_cooldown += penalty + target.ai_controller.movement_delay += penalty target.move_to_delay += penalty /datum/animal_gene/sluggish/remove_from(mob/living/simple_animal/hostile/target) @@ -21,5 +21,5 @@ var/penalty = round(intensity) target.genetic_speed_delta -= penalty if(target.ai_controller) - target.ai_controller.movement_cooldown -= penalty + target.ai_controller.movement_delay -= penalty target.move_to_delay -= penalty diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm index 126fdbe6d4f..0ad59385263 100644 --- a/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm @@ -12,7 +12,7 @@ var/reduction = round(intensity) target.genetic_speed_delta -= reduction if(target.ai_controller) - target.ai_controller.movement_cooldown = max(1, target.ai_controller.movement_cooldown - reduction) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay - reduction) target.move_to_delay = max(1, target.move_to_delay - reduction) @@ -22,5 +22,5 @@ var/reduction = round(intensity) target.genetic_speed_delta += reduction if(target.ai_controller) - target.ai_controller.movement_cooldown += reduction + target.ai_controller.movement_delay += reduction target.move_to_delay += reduction diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm index 61a8f911e83..f0a8f24dc4c 100644 --- a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm +++ b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm @@ -359,7 +359,7 @@ GLOBAL_LIST_INIT(all_animal_genes_weighted, generate_animaL_genes()) genetics.refresh() /mob/living/simple_animal/proc/debug_apply_max_genetics() - if(!genetics) + if(!genetics || ispath(genetics)) genetics = new /datum/animal_genetics(src) var/datum/animal_gene/swift/swift = new() From ba8ab05fd6fe63c692bfa95b6c79e6204db7f0cd Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:01:43 -0800 Subject: [PATCH 53/73] farm animal genetics --- .../mob/living/simple_animal/genetics/animal_genetics.dm | 2 +- .../mob/living/simple_animal/hostile/retaliate/farm/chicken.dm | 2 ++ .../mob/living/simple_animal/hostile/retaliate/farm/cow.dm | 3 +-- .../mob/living/simple_animal/hostile/retaliate/farm/goat.dm | 1 + .../living/simple_animal/hostile/retaliate/farm/trufflepig.dm | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm index f0a8f24dc4c..fd06a265be1 100644 --- a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm +++ b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm @@ -334,7 +334,7 @@ GLOBAL_LIST_INIT(all_animal_genes_weighted, generate_animaL_genes()) return !length(remaining) /mob/living/simple_animal/proc/roll_initial_genetics(max_genes = 2, intensity_bound_cap = 0.4, recessive_bias = 30) - if(!genetics) + if(!genetics || ispath(genetics)) genetics = new /datum/animal_genetics(src) var/attempts = 0 diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm index 3068aab3890..c9e2b0573c2 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm @@ -48,6 +48,8 @@ pooptype = /obj/item/natural/poo/horse happy_funtime_mob = TRUE + generate_genetics = TRUE + var/eggsFertile = TRUE var/body_color var/icon_prefix = "chicken" diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm index f0539fbefb9..9df07d7f27b 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm @@ -48,8 +48,7 @@ base_strength = 4 remains_type = /obj/effect/decal/remains/cow happy_funtime_mob = TRUE - - + generate_genetics = TRUE ai_controller = /datum/ai_controller/basic_controller/cow var/can_breed = TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm index cae0feb3a34..b908a7488a5 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm @@ -56,6 +56,7 @@ buckle_lying = FALSE can_buckle = TRUE remains_type = /obj/effect/decal/remains/cow + generate_genetics = TRUE ai_controller = /datum/ai_controller/gote happy_funtime_mob = TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm index e282e9c333b..4e4f7561c53 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm @@ -172,6 +172,7 @@ ) happy_funtime_mob = TRUE + generate_genetics = TRUE var/hangry_meter = 0 var/random_gender = TRUE var/can_breed = TRUE From 034e5588becbebbaa82da366e4412d948add0bda Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:16:25 -0800 Subject: [PATCH 54/73] Update saiga.dm --- .../mob/living/simple_animal/hostile/retaliate/game/saiga.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm index e1dad9eb47b..eea0697d140 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm @@ -93,7 +93,7 @@ /mob/living/simple_animal/hostile/retaliate/saiga/update_overlays() . = ..() - if(genetics) + if(istype(genetics)) var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) @@ -251,7 +251,7 @@ /mob/living/simple_animal/hostile/retaliate/saigabuck/update_overlays() . = ..() - if(genetics) + if(istype(genetics)) var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) From 98e4b61a93b636d4a92815d53865b8d6ecdfaf36 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:31:18 -0800 Subject: [PATCH 55/73] whoops --- .../mob/living/simple_animal/hostile/retaliate/farm/chicken.dm | 1 + .../mob/living/simple_animal/hostile/retaliate/farm/cow.dm | 2 ++ .../mob/living/simple_animal/hostile/retaliate/farm/goat.dm | 2 ++ .../living/simple_animal/hostile/retaliate/farm/trufflepig.dm | 1 + 4 files changed, 6 insertions(+) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm index c9e2b0573c2..acf8044decb 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm @@ -190,6 +190,7 @@ ai_controller = /datum/ai_controller/basic_controller/chicken/baby chicken_init = FALSE + generate_genetics = FALSE /obj/structure/fluff/nest name = "nest" diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm index 9df07d7f27b..701313ec907 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm @@ -173,6 +173,7 @@ base_speed = 2 remains_type = /obj/effect/decal/remains/cow happy_funtime_mob = TRUE + generate_genetics = TRUE ai_controller = /datum/ai_controller/basic_controller/cow /mob/living/simple_animal/hostile/retaliate/bull/Initialize() @@ -237,6 +238,7 @@ ai_controller = /datum/ai_controller/basic_controller/cow/baby can_breed = FALSE can_tip = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/cow/cowlet/udder_component() return diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm index b908a7488a5..1545e698c47 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm @@ -192,6 +192,7 @@ ai_controller = /datum/ai_controller/gote happy_funtime_mob = TRUE + generate_genetics = TRUE /mob/living/simple_animal/hostile/retaliate/goatmale/Initialize() . = ..() @@ -298,6 +299,7 @@ adult_growth = /mob/living/simple_animal/hostile/retaliate/goat can_buckle = FALSE can_breed = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/goat/goatlet/udder_component() return diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm index 4e4f7561c53..0e917568704 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm @@ -303,6 +303,7 @@ name = "truffle piglet" adult_growth = /mob/living/simple_animal/hostile/retaliate/trufflepig/female can_breed = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/trufflepig/piglet/Initialize() . = ..() From 8ea04a3e2c07bd1f258c14d3dd45a69af1512372 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:51:22 -0800 Subject: [PATCH 56/73] adds in a low pop vote system --- code/controllers/subsystem/ticker.dm | 11 +++++++++-- code/controllers/subsystem/vote.dm | 15 +++++++++++++++ code/datums/antag_retainer.dm | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index f9303a057bd..f6e2a151469 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -75,7 +75,9 @@ SUBSYSTEM_DEF(ticker) var/mob/living/carbon/human/rulermob = null /// The appointed regent mob var/mob/living/carbon/human/regent_mob = null - var/failedstarts = 0 + var/vote_started = FALSE + var/voting = FALSE + var/pre_vote = 0 var/list/manualmodes = list() var/end_party = FALSE @@ -305,9 +307,14 @@ SUBSYSTEM_DEF(ticker) continue readied_jobs.Add(V) - if(CONFIG_GET(flag/ruler_required)) + if(CONFIG_GET(flag/ruler_required) && !vote_started) + if(pre_vote > 10 && !voting) + voting = TRUE + SSvote.initiate_vote("norulervote", "The Gods") if(!(("Monarch" in readied_jobs) || (start_immediately == TRUE))) //start_immediately triggers when the world is doing a test run or an admin hits start now, we don't need to check for king to_chat(world, span_purple("[pick(no_ruler_lines)]")) + if(!voting) + pre_vote++ return FALSE job_change_locked = TRUE diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index eb62fa09dbc..2f3f67057e4 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -190,6 +190,14 @@ SUBSYSTEM_DEF(vote) if("storyteller") SSgamemode.storyteller_vote_result(.) + if("norulervote") + switch(.) + if("Start Anyway") + // Set a flag or call whatever starts the round ignoring ruler check + SSticker.vote_started = FALSE + if("Wait for Ruler") + SSticker.vote_started = FALSE + SSticker.pre_vote = 0 if(restart) var/active_admins = 0 for(var/client/C in GLOB.admins) @@ -280,6 +288,8 @@ SUBSYSTEM_DEF(vote) choices.Add("Continue Playing","End Round") if("storyteller") choices.Add(SSgamemode.storyteller_vote_choices()) + if("norulervote") + choices.Add("Start Anyway", "Wait for Ruler") else return 0 mode = vote_type @@ -305,6 +315,11 @@ SUBSYSTEM_DEF(vote) return 1 return 0 +/datum/controller/subsystem/vote/proc/initiate_norulervote() + if(mode) // Already a vote in progress + return 0 + return initiate_vote("norulervote", "The Gods") + /datum/controller/subsystem/vote/proc/interface(client/C) if(!C) return diff --git a/code/datums/antag_retainer.dm b/code/datums/antag_retainer.dm index 07a7e482b72..5feb17c7eaf 100644 --- a/code/datums/antag_retainer.dm +++ b/code/datums/antag_retainer.dm @@ -73,7 +73,8 @@ SSticker.missing_lord_time = world.time if(world.time > SSticker.missing_lord_time + 10 MINUTES) SSticker.missing_lord_time = world.time - addomen(OMEN_NOLORD) + if(!SSticker.vote_started) + addomen(OMEN_NOLORD) return FALSE else return TRUE From 04a5f84b01260ad3457a37e40104890b3e0dcb56 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:55:30 -0800 Subject: [PATCH 57/73] Update ambush.dm --- code/modules/mob/living/ambush.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index fa78fc9fc0d..8ccac6a5773 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -175,7 +175,7 @@ GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each ind /proc/get_adjacent_ambush_turfs(turf/T) var/list/adjacent = list() - for(var/turf/AT in get_adjacent_ambush_turfs(T)) + for(var/turf/AT in get_adjacent_open_turfs(T)) if(AT.density || T.LinkBlockedWithAccess(AT, null)) continue adjacent += AT From 74eae91fda2107ca67582c1e7d7360418a363f1b Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:08:52 -0800 Subject: [PATCH 58/73] Update vote.dm --- code/controllers/subsystem/vote.dm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 2f3f67057e4..60887d19355 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -193,8 +193,7 @@ SUBSYSTEM_DEF(vote) if("norulervote") switch(.) if("Start Anyway") - // Set a flag or call whatever starts the round ignoring ruler check - SSticker.vote_started = FALSE + SSticker.vote_started = TRUE if("Wait for Ruler") SSticker.vote_started = FALSE SSticker.pre_vote = 0 From e155a4fbdec004d4ee231929558f5044f64d774d Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:10:34 -0800 Subject: [PATCH 59/73] Update ticker.dm --- code/controllers/subsystem/ticker.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index f6e2a151469..1eb65bbbf7f 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -308,7 +308,7 @@ SUBSYSTEM_DEF(ticker) readied_jobs.Add(V) if(CONFIG_GET(flag/ruler_required) && !vote_started) - if(pre_vote > 10 && !voting) + if(pre_vote > 4 && !voting) voting = TRUE SSvote.initiate_vote("norulervote", "The Gods") if(!(("Monarch" in readied_jobs) || (start_immediately == TRUE))) //start_immediately triggers when the world is doing a test run or an admin hits start now, we don't need to check for king From bfe800c97ae5d5a316f341d2a5d88a3fd9c7a4b5 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:17:46 -0800 Subject: [PATCH 60/73] map and z-web stuff --- _maps/map_files/vanderlin/vanderlin.dmm | 6 +- .../map_files/vanderlin/vanderlin_forest.dmm | 154 +++++++++++------- code/modules/questing/items/quest_scroll.dm | 10 +- 3 files changed, 105 insertions(+), 65 deletions(-) diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm index 0d4b9c7fdc2..07e5874e6fb 100644 --- a/_maps/map_files/vanderlin/vanderlin.dmm +++ b/_maps/map_files/vanderlin/vanderlin.dmm @@ -45346,6 +45346,10 @@ /mob/living/carbon/human/species/rousman/npc, /turf/open/floor/cobble, /area/indoors/dungeon) +"vJt" = ( +/obj/structure/fake_machine/contractledger, +/turf/open/floor/ruinedwood/darker, +/area/indoors/town/tavern) "vJB" = ( /obj/structure/table/wood/plain_alt, /obj/item/reagent_containers/glass/bowl{ @@ -114547,7 +114551,7 @@ wrm wrm wrm bqc -cfr +vJt cfr kse itp diff --git a/_maps/map_files/vanderlin/vanderlin_forest.dmm b/_maps/map_files/vanderlin/vanderlin_forest.dmm index 9a2ec849428..5d7d13154b2 100644 --- a/_maps/map_files/vanderlin/vanderlin_forest.dmm +++ b/_maps/map_files/vanderlin/vanderlin_forest.dmm @@ -204,6 +204,10 @@ /obj/item/reagent_containers/glass/cup/wooden, /turf/open/floor/tile/kitchen, /area/indoors/wilderness/tavern) +"dq" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/grass, +/area/outdoors/wilderness) "dr" = ( /obj/structure/door/stone, /obj/structure/trap/shock, @@ -306,6 +310,10 @@ /obj/structure/bed/hay, /turf/open/floor/cobble, /area/indoors/wilderness/garrison) +"eC" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/grass, +/area/outdoors/wilderness) "eD" = ( /obj/effect/decal/cleanable/sigil/S, /turf/open/floor/grass, @@ -1043,6 +1051,10 @@ /obj/machinery/light/fueled/wallfire/candle/weak, /turf/open/floor/dirt, /area/outdoors/wilderness) +"lZ" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/dirt/road, +/area/outdoors/wilderness) "md" = ( /mob/living/simple_animal/hostile/retaliate/elemental/behemoth{ color = "#FFC000" @@ -1057,6 +1069,10 @@ "mk" = ( /turf/open/floor/tile/masonic/spiral, /area/indoors/dungeon) +"ml" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "mn" = ( /turf/closed/basic, /area/outdoors/wilderness) @@ -1543,6 +1559,10 @@ "sd" = ( /turf/closed/wall/mineral/wooddark, /area/outdoors/wilderness) +"sh" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "sk" = ( /obj/structure/fluff/walldeco/vinez, /turf/closed/wall/mineral/decowood, @@ -2043,6 +2063,10 @@ "yg" = ( /turf/closed/wall/mineral/stone/moss, /area/outdoors/wilderness) +"yj" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/grass, +/area/outdoors/wilderness) "yl" = ( /obj/structure/fluff/walldeco/painting/skull, /turf/closed/wall/mineral/craftstone, @@ -4077,6 +4101,10 @@ "Uj" = ( /turf/closed/mineral/random/med, /area/indoors/cave) +"Un" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "Uo" = ( /obj/structure/door/stone, /obj/effect/mapping_helpers/access/locker, @@ -4199,6 +4227,10 @@ /obj/structure/closet/crate/drawer/inn, /turf/open/floor/twig, /area/indoors/wilderness/tavern) +"VC" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/dirt/road, +/area/outdoors/wilderness) "VJ" = ( /obj/structure/window/openclose{ dir = 4 @@ -8084,7 +8116,7 @@ ZV Nf ZV ZV -ZV +dq tk ZV tk @@ -8869,7 +8901,7 @@ Uj xa ZV gr -gr +Un ZV gr ZV @@ -8937,7 +8969,7 @@ tk gr gr ZV -gr +Un gr ZV ZV @@ -9364,7 +9396,7 @@ Nf Nf ZV gr -ZV +dq gr ZV Nf @@ -9588,7 +9620,7 @@ tk tk ZV ZV -ZV +eC tk ZV gr @@ -9887,7 +9919,7 @@ ZV gr ZV ZV -ZV +eC gr ZV ZV @@ -10282,7 +10314,7 @@ gr ZV gr ZV -gr +sh ZV ZV ZV @@ -10515,7 +10547,7 @@ ZV Nf gr ZV -gr +sh ZV gr IK @@ -11345,7 +11377,7 @@ ZV gr ZV ZV -ZV +eC ZV ZV Nf @@ -12774,7 +12806,7 @@ ZV tk ZV ZV -ZV +yj ZV ZV Nf @@ -13028,7 +13060,7 @@ gr Nf Nf ZV -gr +Un gr gr ZV @@ -14322,7 +14354,7 @@ ZV gr Nf Nf -ZV +yj ZV ZV tk @@ -14495,7 +14527,7 @@ gr ZV ZV ZV -gr +sh gr ZV tk @@ -14984,7 +15016,7 @@ gr ZV Nf ZV -ZV +yj ZV ZV ZV @@ -15705,7 +15737,7 @@ ZV ZV Nf ZV -gr +Un tk ZV RO @@ -15754,7 +15786,7 @@ gr gr ZV gr -ZV +yj ZV ZV tk @@ -15979,7 +16011,7 @@ ZV Nf Nf ZV -ZV +yj gr gr tk @@ -15988,7 +16020,7 @@ gr Nf tk ZV -ZV +eC IK gr gr @@ -16224,7 +16256,7 @@ ZV ZV gr ZV -ZV +yj ZV gr gr @@ -16884,7 +16916,7 @@ gr TW gr gr -ZV +yj ZV Nf tk @@ -17110,7 +17142,7 @@ ZV Fw ZV Nf -ZV +yj iQ Iz iQ @@ -17966,7 +17998,7 @@ ZV ZV ZV ZV -ZV +eC gr gr tk @@ -18330,7 +18362,7 @@ Nf Nf gr ZV -gr +ml ZV RO gr @@ -18479,7 +18511,7 @@ ZV ZV gr gr -ZV +yj tk ZV tk @@ -18582,7 +18614,7 @@ Nf ZV ZV gr -gr +ml ZV gr tk @@ -19297,7 +19329,7 @@ Iz iQ Iz gr -gr +ml gr Nf gr @@ -19454,7 +19486,7 @@ gr gr zd zd -ZV +yj gr ZV ZV @@ -19572,7 +19604,7 @@ gr ZV Nf gr -gr +Un gr gr Nf @@ -19835,7 +19867,7 @@ gr ZV ZV ZV -ZV +eC gr gr ZV @@ -20009,7 +20041,7 @@ gr gr ZV tk -ZV +yj gr Nf tk @@ -20279,7 +20311,7 @@ gr ZV ZV ZV -ZV +yj ZV ZV zd @@ -20672,7 +20704,7 @@ ZV yV ZV ZV -ZV +yj zd gr gr @@ -21667,7 +21699,7 @@ Nf Nf ZV ZV -ZV +eC gr ZV ZV @@ -21763,7 +21795,7 @@ ZV ZV Nf gr -gr +sh ZV ZV Nf @@ -22698,7 +22730,7 @@ gr Nf tk ZV -gr +sh gr ZV gr @@ -22740,7 +22772,7 @@ iQ HC HC ZV -gr +ml ZV Fw Fw @@ -24149,7 +24181,7 @@ Fw gr ZV ZV -gr +ml gr Nf tk @@ -27014,7 +27046,7 @@ Nf ZV ZV ZV -gr +Un tk gr gr @@ -27043,7 +27075,7 @@ ZV gr gr gr -gr +sh gr gr ZV @@ -27588,7 +27620,7 @@ tk ZV ZV gr -gr +ml gr gr HC @@ -28565,7 +28597,7 @@ Nf Nf Nf ZV -gr +sh gr gr gr @@ -29041,7 +29073,7 @@ gr ZV ZV ZV -tk +sh gr ZV gr @@ -29464,7 +29496,7 @@ Nf gr Nf Iz -ZV +dq gr gr ZV @@ -29517,7 +29549,7 @@ ZV ZV Iz Iz -Iz +VC ZV gr ZV @@ -29689,7 +29721,7 @@ tk ZV ZV Nf -ZV +eC ZV gr Nf @@ -30641,7 +30673,7 @@ iQ Nf ZV ZV -gr +ml ZV gr gr @@ -32322,7 +32354,7 @@ Nf ZV ZV gr -gr +Un ZV tk ZV @@ -32460,7 +32492,7 @@ gr gr gr ZV -gr +ml tk tk tk @@ -32847,7 +32879,7 @@ gr ZV Nf gr -ZV +yj gr Nf Nf @@ -33313,7 +33345,7 @@ hx tk ZV Nf -ZV +dq gr gr Nf @@ -33554,7 +33586,7 @@ ZV ZV ZV ZV -gr +Un ZV Nf Nf @@ -35437,7 +35469,7 @@ gr gr ZV gr -ZV +eC gr Nf Nf @@ -35473,7 +35505,7 @@ Iz Iz ZV gr -ZV +yj gr gr gr @@ -36509,7 +36541,7 @@ Jo gr ZV ZV -ZV +eC gr ZV gr @@ -38188,7 +38220,7 @@ Nf gr gr ZV -ZV +dq yC Iz ZV @@ -39909,7 +39941,7 @@ ZV gr tk gr -ZV +yj ZV ZV Nf @@ -41063,7 +41095,7 @@ ZV gr ZV ZV -ZV +dq ZV tk yC @@ -41236,7 +41268,7 @@ gr gr Iz Iz -Iz +lZ Iz gr Nf @@ -41277,7 +41309,7 @@ yC ZV ZV gr -Iz +VC gr gr Nf @@ -41498,7 +41530,7 @@ Iz ZV ZV ZV -ZV +yj ZV ZV ZV @@ -43446,7 +43478,7 @@ ZV gr ZV ZV -ZV +dq gr Nf Nf diff --git a/code/modules/questing/items/quest_scroll.dm b/code/modules/questing/items/quest_scroll.dm index 103325d84d7..12dbf55c050 100644 --- a/code/modules/questing/items/quest_scroll.dm +++ b/code/modules/questing/items/quest_scroll.dm @@ -297,9 +297,13 @@ GLOBAL_LIST_EMPTY(quest_scrolls) // Especially cuz of how many transitions exist if(target_turf.z != user_turf.z) var/z_diff = abs(target_turf.z - user_turf.z) - last_z_level_hint = target_turf.z > user_turf.z ? \ - "[z_diff] level\s above you" : \ - "[z_diff] level\s below you" + if(!is_in_zweb(target_turf.z, user_turf.z)) + var/area/area = get_area(target_turf) + last_z_level_hint = "in [area.name]" + else + last_z_level_hint = target_turf.z > user_turf.z ? \ + "[z_diff] level\s above you" : \ + "[z_diff] level\s below you" // Calculate direction from user to target var/dx = target_turf.x - user_turf.x // EAST direction From fff43af2080ea9c5afc3b876c4313de846c86928 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:22:08 -0800 Subject: [PATCH 61/73] quickie --- code/datums/mana/leylines/_leyline.dm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/datums/mana/leylines/_leyline.dm b/code/datums/mana/leylines/_leyline.dm index 75a38bffee7..55d0dbdf6fd 100644 --- a/code/datums/mana/leylines/_leyline.dm +++ b/code/datums/mana/leylines/_leyline.dm @@ -166,6 +166,7 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) ending, icon_state = "blood", time = INFINITY, + beam_type = /obj/effect/ebeam/leyline, max_distance = world.maxx, beam_color = theme?.beam_color, beam_layer = UPPER_LEYLINE_LAYER, @@ -173,3 +174,6 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) invisibility = INVISIBILITY_LEYLINES, mana_pool = src, ) + +/obj/effect/ebeam/leyline + vis_flags = VIS_HIDE From f81b4cb1033ed3509c785910353a47ec48531e98 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:52:06 -0800 Subject: [PATCH 62/73] Update eggs.dm --- code/modules/farming/items/eggs.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/farming/items/eggs.dm b/code/modules/farming/items/eggs.dm index ca77a99e4fc..74ce72c25ef 100644 --- a/code/modules/farming/items/eggs.dm +++ b/code/modules/farming/items/eggs.dm @@ -37,4 +37,4 @@ SEND_SIGNAL(parent, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, new_chick) SEND_SIGNAL(parent, COMSIG_HAPPINESS_PASS_HAPPINESS, new_chick) if(parent.genetics && !ispath(parent.genetics)) - parent.genetics.inherit_to(src, father) + parent.genetics.inherit_to(new_chick, father) From 37d80f0994bb64f64c356de6e19f17eea0dca7bb Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:09:33 -0800 Subject: [PATCH 63/73] Update animal_genetics.dm --- .../mob/living/simple_animal/genetics/animal_genetics.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm index fd06a265be1..835ff96bdd1 100644 --- a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm +++ b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm @@ -220,7 +220,7 @@ GLOBAL_LIST_INIT(all_animal_genes_weighted, generate_animaL_genes()) return father_match /datum/animal_genetics/proc/inherit_to(mob/living/simple_animal/baby, mob/living/simple_animal/father) - if(!baby.genetics) + if(!istype(baby.genetics)) baby.genetics = new /datum/animal_genetics(baby) var/list/mother_pool = _build_allele_pool() var/list/father_pool = istype(father?.genetics) ? father.genetics._build_allele_pool() : list() From dab08d187d035a5ea07045754aa27cc40e96f49e Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:32:19 -0800 Subject: [PATCH 64/73] Update _kill_base.dm --- code/modules/questing/quests/kill/_kill_base.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/modules/questing/quests/kill/_kill_base.dm b/code/modules/questing/quests/kill/_kill_base.dm index 75828ca6b64..6e23a125a04 100644 --- a/code/modules/questing/quests/kill/_kill_base.dm +++ b/code/modules/questing/quests/kill/_kill_base.dm @@ -17,6 +17,7 @@ var/obj/effect/quest_spawn/spawn_effect = new /obj/effect/quest_spawn(spawn_turf) var/mob/living/new_mob = new target_mob_type(spawn_effect) + ADD_TRAIT(new_mob, TRAIT_STUCKITEMS, TRAIT_GENERIC) new_mob.faction |= "quest" new_mob.AddComponent(/datum/component/quest_object/kill, src) ADD_TRAIT(new_mob, TRAIT_FRESHSPAWN, "[type]") From 9c53744df9470356981a7d9fb6ca0ebbf861f9ea Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:38:02 -0800 Subject: [PATCH 65/73] high_value strip prevention --- code/__DEFINES/obj_flags.dm | 1 + code/__DEFINES/traits/definitions.dm | 1 + code/game/objects/items.dm | 6 ++++++ code/modules/questing/quests/kill/_kill_base.dm | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm index 29f482a51b5..e797a85ef38 100644 --- a/code/__DEFINES/obj_flags.dm +++ b/code/__DEFINES/obj_flags.dm @@ -33,6 +33,7 @@ #define SURGICAL_TOOL (1<<12) //Tool commonly used for surgery: won't attack targets in an active surgical operation on help intent (in case of mistakes) #define SHRINK_ENCHANT (1<<13) #define ITEM_ONLY_BREAK (1<<14) +#define HIGH_VALUE (1<<15) // Flags for the clothing_flags var on /obj/item/clothing diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index d6cf70ecb9a..e209d898cd8 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -228,6 +228,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Used to limit healing to putrid flesh mobs #define TRAIT_PUTRID "Putrid" #define TRAIT_STUCKITEMS "stuck_items" // Prevents removing items except for hand slots +#define TRAIT_HIGHVALUE_STUCK "highvalue_stuck" //Prevents removing items except for hand slots if it is consdiered to strong /// Confessed under torture, to force sign #define TRAIT_HAS_CONFESSED "has_confessed" /// Confessed for specific type of antag diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index d474c1afe86..da2ea7abdab 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1346,6 +1346,12 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e /obj/item/proc/canStrip(mob/stripper, mob/owner) if(HAS_TRAIT(loc, TRAIT_STUCKITEMS)) return FALSE + if(HAS_TRAIT(loc, TRAIT_HIGHVALUE_STUCK)) + if(melting_material == /datum/material/steel) + return FALSE + if(item_flags & HIGH_VALUE) + return FALSE + return !HAS_TRAIT(src, TRAIT_NODROP) /obj/item/proc/doStrip(mob/stripper, mob/owner) diff --git a/code/modules/questing/quests/kill/_kill_base.dm b/code/modules/questing/quests/kill/_kill_base.dm index 6e23a125a04..101bf99982b 100644 --- a/code/modules/questing/quests/kill/_kill_base.dm +++ b/code/modules/questing/quests/kill/_kill_base.dm @@ -17,7 +17,7 @@ var/obj/effect/quest_spawn/spawn_effect = new /obj/effect/quest_spawn(spawn_turf) var/mob/living/new_mob = new target_mob_type(spawn_effect) - ADD_TRAIT(new_mob, TRAIT_STUCKITEMS, TRAIT_GENERIC) + ADD_TRAIT(new_mob, TRAIT_HIGHVALUE_STUCK, TRAIT_GENERIC) new_mob.faction |= "quest" new_mob.AddComponent(/datum/component/quest_object/kill, src) ADD_TRAIT(new_mob, TRAIT_FRESHSPAWN, "[type]") From 5d6079b5a2e7d34e7ae3d25abe3c80ae0c9c4245 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:50:44 -0800 Subject: [PATCH 66/73] compass overhaul to use portal locations aswell --- code/game/objects/structures/traveltile.dm | 16 +++++ code/modules/questing/items/quest_scroll.dm | 70 ++++++++++++++++----- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/code/game/objects/structures/traveltile.dm b/code/game/objects/structures/traveltile.dm index b9ba41357e9..d52ea8ad838 100644 --- a/code/game/objects/structures/traveltile.dm +++ b/code/game/objects/structures/traveltile.dm @@ -49,11 +49,27 @@ var/can_gain_by_walking = FALSE var/check_other_side = FALSE var/list/revealed_to = list() + var/area/cached_destination_area /obj/structure/fluff/traveltile/Initialize() GLOB.traveltiles += src hide_if_needed() . = ..() + return INITIALIZE_HINT_LATELOAD + + +/obj/structure/fluff/traveltile/LateInitialize() + . = ..() + // Find our paired portal and cache what area it's in + resolve_destination_area() + +/obj/structure/fluff/traveltile/proc/resolve_destination_area() + if(!aportalgoesto) + return + for(var/obj/structure/fluff/traveltile/other in GLOB.traveltiles) // or however you iterate portals + if(other.aportalid == aportalgoesto) + cached_destination_area = get_area(other) + return /obj/structure/fluff/traveltile/Destroy() GLOB.traveltiles -= src diff --git a/code/modules/questing/items/quest_scroll.dm b/code/modules/questing/items/quest_scroll.dm index 12dbf55c050..af8bd558673 100644 --- a/code/modules/questing/items/quest_scroll.dm +++ b/code/modules/questing/items/quest_scroll.dm @@ -177,16 +177,32 @@ GLOBAL_LIST_EMPTY(quest_scrolls) if(!user_turf) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + var/turf/compass_target = target_turf + var/portal_hint + if(target_turf.z != user_turf.z) - to_chat(user, span_info("The scroll pulses faintly - the target is on a different level.")) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + if(!is_in_zweb(target_turf.z, user_turf.z)) + var/area/target_area = get_area(target_turf) + var/obj/structure/fluff/traveltile/portal = find_portal_to_area(target_area, user_turf) + if(portal) + compass_target = get_turf(portal) + portal_hint = "traverse to [target_area.name]" + else + to_chat(user, span_info("The scroll pulses faintly - the target is in [target_area.name], but you sense no path from here.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + else + to_chat(user, span_info("The scroll pulses faintly - the target is on a different level.")) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - var/dir = get_dir(user_turf, target_turf) + var/dir = get_dir(user_turf, compass_target) if(!dir) - to_chat(user, span_info("The scroll pulses warmly - you are nearby.")) + if(portal_hint) + to_chat(user, span_info("The scroll pulses warmly - the [portal_hint] is nearby.")) + else + to_chat(user, span_info("The scroll pulses warmly - you are nearby.")) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - var/distance = get_dist(user_turf, target_turf) + var/distance = get_dist(user_turf, compass_target) var/arrow_color switch(distance) if(1 to 15) @@ -210,6 +226,9 @@ GLOBAL_LIST_EMPTY(quest_scrolls) user_hud.infodisplay += arrow user_hud.show_hud(user_hud.hud_version) + if(portal_hint) + to_chat(user, span_info("The scroll points toward the [portal_hint].")) + QDEL_IN(arrow, 1.5 SECONDS) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN @@ -285,29 +304,32 @@ GLOBAL_LIST_EMPTY(quest_scrolls) last_compass_direction = "Searching for target..." last_z_level_hint = "" - // Get target location from quest datum var/turf/target_turf = assigned_quest.get_target_location() if(!target_turf) last_compass_direction = "location unknown" last_z_level_hint = "" return - // We want the target to know z level differences but verticality exists - // We don't want to frustrate player by forcing them to track on the same z level - // Especially cuz of how many transitions exist + var/turf/compass_target = target_turf // may be overridden to a portal + if(target_turf.z != user_turf.z) - var/z_diff = abs(target_turf.z - user_turf.z) if(!is_in_zweb(target_turf.z, user_turf.z)) - var/area/area = get_area(target_turf) - last_z_level_hint = "in [area.name]" + var/area/target_area = get_area(target_turf) + var/obj/structure/fluff/traveltile/portal = find_portal_to_area(target_area, user_turf) + if(portal) + compass_target = get_turf(portal) + last_z_level_hint = "traverse to [target_area.name]" + else + last_z_level_hint = "in [target_area.name]" else + var/z_diff = abs(target_turf.z - user_turf.z) last_z_level_hint = target_turf.z > user_turf.z ? \ "[z_diff] level\s above you" : \ "[z_diff] level\s below you" - // Calculate direction from user to target - var/dx = target_turf.x - user_turf.x // EAST direction - var/dy = target_turf.y - user_turf.y // NORTH direction + // Use compass_target (portal or actual target) for all direction math below + var/dx = compass_target.x - user_turf.x + var/dy = compass_target.y - user_turf.y var/distance = sqrt(dx*dx + dy*dy) // If very close, don't show direction @@ -317,7 +339,7 @@ GLOBAL_LIST_EMPTY(quest_scrolls) return // Get precise direction text - var/direction_text = get_precise_direction_between(user_turf, target_turf) + var/direction_text = get_precise_direction_between(user_turf, compass_target) if(!direction_text) direction_text = "unknown direction" @@ -338,3 +360,19 @@ GLOBAL_LIST_EMPTY(quest_scrolls) last_z_level_hint = "on this level" #undef WHISPER_COOLDOWN + +/obj/item/paper/scroll/quest/proc/find_portal_to_area(area/target_area, turf/from_turf) + var/obj/structure/fluff/traveltile/best + var/best_dist = INFINITY + + for(var/obj/structure/fluff/traveltile/tile in GLOB.traveltiles) + var/turf/get_turf = get_turf(tile) + if(!is_in_zweb(get_turf.z, from_turf.z)) + continue + if(tile.cached_destination_area == target_area) + var/d = get_dist(from_turf, get_turf(tile)) + if(d < best_dist) + best_dist = d + best = tile + + return best From d07f5244f508dbc636946645059f7ae4e7ec1657 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:58:14 -0800 Subject: [PATCH 67/73] aggro board setting and maneaters don't target clientless --- code/datums/components/aggro_board.dm | 3 +++ code/game/objects/structures/maneater.dm | 2 ++ 2 files changed, 5 insertions(+) diff --git a/code/datums/components/aggro_board.dm b/code/datums/components/aggro_board.dm index d206568be3b..ff3f7caed8a 100644 --- a/code/datums/components/aggro_board.dm +++ b/code/datums/components/aggro_board.dm @@ -116,6 +116,9 @@ // Update the aggro table victim.ai_controller.blackboard[BB_MOB_AGGRO_TABLE] = aggro_table + if(!victim.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + victim.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, attacker) + // Update highest threat mob update_highest_threat(victim) diff --git a/code/game/objects/structures/maneater.dm b/code/game/objects/structures/maneater.dm index 60c5b6c9f2c..cf0f48a3076 100644 --- a/code/game/objects/structures/maneater.dm +++ b/code/game/objects/structures/maneater.dm @@ -138,6 +138,8 @@ return if(L.buckling) return // Something else is buckling them, maybe another maneater even + if(!L.client) + return buckle_mob(L, TRUE, check_loc = FALSE) START_PROCESSING(SSobj, src) if(!HAS_TRAIT(L, TRAIT_NOPAIN)) From c13b584f9e107128892fc2cf34220633dfecfa00 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:19:28 -0800 Subject: [PATCH 68/73] Update ambush.dm --- code/modules/mob/living/ambush.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index 8ccac6a5773..b34ec5aea6c 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -137,6 +137,7 @@ GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each ind H.del_on_deaggro = 44 SECONDS H.last_aggro_loss = world.time H.faction += "ambush" + ADD_TRAIT(H, TRAIT_HIGHVALUE_STUCK, TRAIT_GENERIC) mustype = 2 if(mustype == 1) playsound_local(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) From 846d677250ea1e42758606b8f8fd83b361eb8458 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:36:36 -0800 Subject: [PATCH 69/73] replaces high_value strip with an element so you can still salvage shit --- code/datums/elements/locked_item.dm | 42 +++++++++++++++++++ code/modules/mob/living/ambush.dm | 5 +++ .../questing/quests/kill/_kill_base.dm | 2 +- vanderlin.dme | 1 + 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 code/datums/elements/locked_item.dm diff --git a/code/datums/elements/locked_item.dm b/code/datums/elements/locked_item.dm new file mode 100644 index 00000000000..85c0491307b --- /dev/null +++ b/code/datums/elements/locked_item.dm @@ -0,0 +1,42 @@ +/// Applied to items. Prevents mobs from equipping this item to any slot except hands +/// unless they are in the ambush or quest faction. +/datum/element/faction_restricted_equip + element_flags = ELEMENT_BESPOKE + + /// List of factions that are allowed to equip this item freely. Defaults to ambush + quest. + var/list/allowed_factions + +/datum/element/faction_restricted_equip/Attach(datum/target, list/factions) + . = ..() + if(!isitem(target)) + return ELEMENT_INCOMPATIBLE + + allowed_factions = factions || list("ambush", "quest") + RegisterSignal(target, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped)) + RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + +/datum/element/faction_restricted_equip/Detach(datum/target) + . = ..() + UnregisterSignal(target, COMSIG_ITEM_EQUIPPED) + UnregisterSignal(target, COMSIG_PARENT_EXAMINE) + +/datum/element/faction_restricted_equip/proc/on_examine(datum/source, mob/user, list/examine_text) + examine_text += span_danger("This item has engraved runes preventing it from being worn.") + +/// Checks if the mob equipping is in an allowed faction, blocks non-hand slots if not. +/datum/element/faction_restricted_equip/proc/on_equipped(obj/item/source, mob/living/user, slot) + SIGNAL_HANDLER + + // Always allow hand slots + if(slot == ITEM_SLOT_HANDS) + return + + for(var/faction in allowed_factions) + if(faction in user.faction) + return + + user.temporarilyRemoveItemFromInventory(source) + if(!user.put_in_hands(source)) + source.forceMove(get_turf(user)) + + to_chat(user, span_warning("The enscribed runes in [source] prevent it from fitting on you.")) diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index b34ec5aea6c..4d21166e895 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -137,6 +137,7 @@ GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each ind H.del_on_deaggro = 44 SECONDS H.last_aggro_loss = world.time H.faction += "ambush" + addtimer(CALLBACK(H, PROC_REF(setup_equip_block)), 3 SECONDS) ADD_TRAIT(H, TRAIT_HIGHVALUE_STUCK, TRAIT_GENERIC) mustype = 2 if(mustype == 1) @@ -145,6 +146,10 @@ GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each ind playsound_local(src, pick('sound/misc/jumphumans (1).ogg','sound/misc/jumphumans (2).ogg','sound/misc/jumphumans (3).ogg'), 100) shake_camera(src, 2, 2) +/mob/living/proc/setup_equip_block() + for(var/obj/item/clothing/clothing in contents) + clothing.AddElement(/datum/element/faction_restricted_equip) + // Return whether a mob is blocked from being ambushed /mob/living/proc/get_will_block_ambush() if(!ambushable()) diff --git a/code/modules/questing/quests/kill/_kill_base.dm b/code/modules/questing/quests/kill/_kill_base.dm index 101bf99982b..a36171de47c 100644 --- a/code/modules/questing/quests/kill/_kill_base.dm +++ b/code/modules/questing/quests/kill/_kill_base.dm @@ -17,11 +17,11 @@ var/obj/effect/quest_spawn/spawn_effect = new /obj/effect/quest_spawn(spawn_turf) var/mob/living/new_mob = new target_mob_type(spawn_effect) - ADD_TRAIT(new_mob, TRAIT_HIGHVALUE_STUCK, TRAIT_GENERIC) new_mob.faction |= "quest" new_mob.AddComponent(/datum/component/quest_object/kill, src) ADD_TRAIT(new_mob, TRAIT_FRESHSPAWN, "[type]") addtimer(TRAIT_CALLBACK_REMOVE(new_mob, TRAIT_FRESHSPAWN, "[type]"), 60 SECONDS) + addtimer(CALLBACK(new_mob, TYPE_PROC_REF(/mob/living, setup_equip_block)), 3 SECONDS) spawn_effect.contained_atom = new_mob spawn_effect.AddComponent(/datum/component/quest_object/mob_spawner, src) add_tracked_atom(new_mob) diff --git a/vanderlin.dme b/vanderlin.dme index 963867d66d0..d983adb5c0e 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -1117,6 +1117,7 @@ #include "code\datums\elements\hat_wearer.dm" #include "code\datums\elements\holy_weakness.dm" #include "code\datums\elements\interrupt_on_damage.dm" +#include "code\datums\elements\locked_item.dm" #include "code\datums\elements\mob_overlay_effect.dm" #include "code\datums\elements\movetype_handler.dm" #include "code\datums\elements\no_mouse_drop.dm" From ddb50fef4568d67be26a12957d24290084ff2d7a Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:27:57 -0800 Subject: [PATCH 70/73] whoops --- code/modules/mob/living/ambush.dm | 1 - code/modules/questing/quests/courier.dm | 3 --- 2 files changed, 4 deletions(-) diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index 4d21166e895..263ad90834f 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -138,7 +138,6 @@ GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each ind H.last_aggro_loss = world.time H.faction += "ambush" addtimer(CALLBACK(H, PROC_REF(setup_equip_block)), 3 SECONDS) - ADD_TRAIT(H, TRAIT_HIGHVALUE_STUCK, TRAIT_GENERIC) mustype = 2 if(mustype == 1) playsound_local(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) diff --git a/code/modules/questing/quests/courier.dm b/code/modules/questing/quests/courier.dm index 3b2e2368e69..750f85df4d7 100644 --- a/code/modules/questing/quests/courier.dm +++ b/code/modules/questing/quests/courier.dm @@ -76,9 +76,6 @@ /obj/item/gem/yellow, /obj/item/reagent_containers/glass/bottle/manapot, ), - /area/indoors/town = list( - /obj/item/ration, - ) ) var/list/possible_items = area_delivery_items[delivery_area] || list( From bd1287482c0e81551db564b94ecf084e6dbc1217 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:29:47 -0800 Subject: [PATCH 71/73] Update loot.dm --- code/datums/ai/subtrees/loot.dm | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/code/datums/ai/subtrees/loot.dm b/code/datums/ai/subtrees/loot.dm index 08e1cea1a5f..4517e18893f 100644 --- a/code/datums/ai/subtrees/loot.dm +++ b/code/datums/ai/subtrees/loot.dm @@ -47,9 +47,8 @@ /datum/ai_planning_subtree/loot/proc/_is_blacklisted(list/blacklist, obj/item/candidate) if(!blacklist) return FALSE - for(var/datum/weakref/ref in blacklist) - if(ref.resolve() == candidate) - return TRUE + if(candidate in blacklist) + return TRUE return FALSE /datum/ai_planning_subtree/loot/proc/_item_is_wanted(datum/component/ai_inventory_manager/inv, mob/living/pawn, obj/item/candidate) @@ -79,9 +78,9 @@ return null /proc/ai_loot_blacklist_item(datum/ai_controller/controller, obj/item/it) - controller.add_blackboard_key_lazylist(BB_LOOT_BLACKLIST, WEAKREF(it)) + controller.add_blackboard_key_lazylist(BB_LOOT_BLACKLIST, it) // Prune it after 5 minutes so the list doesn't grow forever - addtimer(CALLBACK(controller, TYPE_PROC_REF(/datum/ai_controller, remove_thing_from_blackboard_key), BB_LOOT_BLACKLIST, WEAKREF(it)), 5 MINUTES) + addtimer(CALLBACK(controller, TYPE_PROC_REF(/datum/ai_controller, remove_thing_from_blackboard_key), BB_LOOT_BLACKLIST, it), 5 MINUTES) /datum/ai_behavior/loot_pick_up From 8dc8136a80f4c66f3287de4ca9640322cf53d29c Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:15:15 -0800 Subject: [PATCH 72/73] this should make it feel more responsive --- code/datums/ai/behaviours/find_and_set.dm | 2 +- .../ai/behaviours/hostile/find_highest_aggro.dm | 6 +++--- .../ai/behaviours/hostile/find_potential_targets.dm | 2 +- .../proximity_monitor/fields/ai_aggro_targetting.dm | 11 ++++++++--- code/datums/proximity_monitor/fields/ai_targetting.dm | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/code/datums/ai/behaviours/find_and_set.dm b/code/datums/ai/behaviours/find_and_set.dm index 8b9f561dea9..7e2eddff5db 100644 --- a/code/datums/ai/behaviours/find_and_set.dm +++ b/code/datums/ai/behaviours/find_and_set.dm @@ -598,7 +598,7 @@ GLOBAL_LIST_INIT(find_and_set_interested_atoms, typecacheof(list(/obj/item, /mob var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() - controller.modify_cooldown(src, get_cooldown(controller)) + controller.modify_cooldown(src, world.time + get_cooldown(controller)) /** * Proximity monitor for find_and_set tracking diff --git a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm index 2de56e94a2a..a3014f71c56 100644 --- a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm +++ b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm @@ -191,14 +191,12 @@ if(!accepted_targets.len) return - // Add threat to all accepted targets, then see if any become our new highest threat var/datum/component/ai_aggro_system/aggro_comp = pawn.GetComponent(/datum/component/ai_aggro_system) if(aggro_comp) for(var/mob/living/target in accepted_targets) aggro_comp.add_threat_to_mob_capped(target, 15, 15) aggro_comp.add_threat_to_mob(target, 3) - // Check if we now have a highest threat target var/mob/highest_threat = controller.blackboard[BB_HIGHEST_THREAT_MOB] if(highest_threat) controller.set_blackboard_key(target_key, highest_threat) @@ -208,6 +206,8 @@ controller.set_blackboard_key(hiding_location_key, potential_hiding_location) finish_action(controller, succeeded = TRUE) + else + controller.modify_cooldown(src, world.time) /// Helper proc to find if a mob is hiding in something /datum/ai_behavior/find_aggro_targets/proc/find_hiding_location(mob/living/source, mob/living/target) @@ -222,7 +222,7 @@ var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() // Cancel any further queued actions so they setup again with new target - controller.modify_cooldown(controller, get_cooldown(controller)) + controller.modify_cooldown(controller, world.time + get_cooldown(controller)) /datum/ai_behavior/find_aggro_targets/bum/finish_action(datum/ai_controller/controller, succeeded, ...) . = ..() diff --git a/code/datums/ai/behaviours/hostile/find_potential_targets.dm b/code/datums/ai/behaviours/hostile/find_potential_targets.dm index 98d29f01aa2..8f2c80e5a1c 100644 --- a/code/datums/ai/behaviours/hostile/find_potential_targets.dm +++ b/code/datums/ai/behaviours/hostile/find_potential_targets.dm @@ -152,7 +152,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob))) var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() // On retarget cancel any further queued actions so that they will setup again with new target - controller.modify_cooldown(controller, get_cooldown(controller)) + controller.modify_cooldown(controller, world.time + get_cooldown(controller)) /// Returns the desired final target from the filtered list of targets /datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets) diff --git a/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm b/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm index 83044506682..75373b454d7 100644 --- a/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm +++ b/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm @@ -38,10 +38,9 @@ /datum/proximity_monitor/advanced/ai_aggro_tracking/Destroy() . = ..() if(!QDELETED(controller) && owning_behavior) - controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + controller.modify_cooldown(owning_behavior, world.time + owning_behavior.get_cooldown(controller)) owning_behavior = null controller = null - target_key = null targeting_strategy_key = null hiding_location_key = null filter = null @@ -54,7 +53,13 @@ . = ..() if(first_build) return - owning_behavior.new_turf_found(target, controller, filter) + var/list/present = list() + for(var/atom/movable/AM in target) + present += AM + if(length(present)) + owning_behavior.new_atoms_found(present, controller, target_key, filter, hiding_location_key) + else + owning_behavior.new_turf_found(target, controller, filter) /datum/proximity_monitor/advanced/ai_aggro_tracking/field_turf_crossed(atom/movable/movable, turf/location, turf/old_location) if(!owning_behavior.atom_allowed(movable, filter, controller.pawn)) diff --git a/code/datums/proximity_monitor/fields/ai_targetting.dm b/code/datums/proximity_monitor/fields/ai_targetting.dm index cc4c7685c98..846c31ffffd 100644 --- a/code/datums/proximity_monitor/fields/ai_targetting.dm +++ b/code/datums/proximity_monitor/fields/ai_targetting.dm @@ -38,7 +38,7 @@ /datum/proximity_monitor/advanced/ai_target_tracking/Destroy() . = ..() if(!QDELETED(controller) && owning_behavior) - controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + controller.modify_cooldown(owning_behavior, world.time + owning_behavior.get_cooldown(controller)) owning_behavior = null controller = null target_key = null From 3cf619b869b19b8421c77a6937c3b6ef48db5f34 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:44:52 -0800 Subject: [PATCH 73/73] Update mob_helpers.dm --- code/modules/mob/mob_helpers.dm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index ffb2d942a1e..c37d81c3a95 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -995,7 +995,9 @@ used_title = return_our_apprentice_name() else if(job) var/datum/job/job_datum = SSjob.GetJob(job) - var/datum/job/used_job = job_datum.parent_job ? job_datum.parent_job : job_datum + if(!job_datum) + return job + var/datum/job/used_job = job_datum?.parent_job ? job_datum.parent_job : job_datum if(!used_job) return job if(steward_check && (used_job.department_flag == OUTSIDERS))

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 z zXa#AB_a)^Yp+9%+Qi z5O8>*J||qf{2@Am6yx^7HkfjN<&qVa8y%snJT6}lI=9A#s4TH#9%|C;Qc;6I@#O@i z8;b!s#6nsgB(C2mB;@nobziNi;U3z;WVn;4BD_ z9&FeGjT#{ zv+zb+lK@lRqx#Ynl!vqVQXB5X-<$ivd6pQ^xqV{02|cc(nKl1lZ3NSn3KXp?5Ol(Z z=0ktjYKP84Nt1%qOwF$#y|}B+Wrb}sQ}q)M^Mo!Q0+55IdPS8xN|emdxs*jO=?3L_ ztYkhsSa_?iZ_6h85b7gypr@Oa3F^zBnFf|nOQAHEV9sxMJJ$+F(26iKFe&^yRk7Q@6#&)E z7YM1z8%f@=`=sqDQa+Mia|(=sppaO})a)wdr6dI7ymA3s_sDdY=9}JT>iW8eccr*R(v@JWhr&1pfI7Z6old(bP2=;3=?2k^z5mf^+f;tnz}F4;^*)$M!yFsgZRSD2F(+d*`HsI9)x{*Pm3Jq*q>L zyu9X4bow-aE)?#XdcUbdMnt$WC_qSfUpT^*0sL^9ou1R`R{ zI>4N$Qip_43vaCZQ7nS$O)MIbB`DdRj#$|(m%n<-H+Bgc2C7S+*=kn5R=|VlyM925 zm!wXE=A(9>?}W2u9*J zdG}%c#Z9WDRPB|Gc5$Zay-3vPzvugCsV~Mm!i$`#_hBSef7AQa3RGcj)6=wO%k6i| zM}7@ATqYC4Zhwkt4k5Spml>56N6aYv6TxD|flpiI#g<$g80{`UZRq^Qg#){2mcU7;Ug8Ni|T}%wSNL{aKSAA literal 19833 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 diff --git a/vanderlin.dme b/vanderlin.dme index 46d454cde76..bc7dc1d829f 100644 --- a/vanderlin.dme +++ b/vanderlin.dme @@ -1696,6 +1696,7 @@ #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\barding.dm" #include "code\game\objects\items\beartraps.dm" #include "code\game\objects\items\bedsheets.dm" #include "code\game\objects\items\bells.dm" @@ -1705,6 +1706,7 @@ #include "code\game\objects\items\breaching_charge.dm" #include "code\game\objects\items\broom.dm" #include "code\game\objects\items\candle.dm" +#include "code\game\objects\items\caparison.dm" #include "code\game\objects\items\cigs_lighters.dm" #include "code\game\objects\items\coins.dm" #include "code\game\objects\items\contraption.dm" @@ -3403,6 +3405,7 @@ #include "code\modules\mob\living\simple_animal\hostile\retaliate\farm\cow.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\farm\goat.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\farm\trufflepig.dm" +#include "code\modules\mob\living\simple_animal\hostile\retaliate\game\fogbeast.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\game\saiga.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\summons\elemental\_base.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\summons\elemental\behemoth.dm" From 2e3edabdc7c601bd3ad58096591fc60d47c086f5 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:36:47 -0800 Subject: [PATCH 47/73] fogbeast > honse --- code/game/objects/items/barding.dm | 10 +-- code/game/objects/items/caparison.dm | 8 +- code/modules/crafting/anvil_recipes/armor.dm | 6 +- .../crafting/quality_of_crafting/sewing.dm | 12 +-- .../hostile/retaliate/game/fogbeast.dm | 76 +++++++++---------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/code/game/objects/items/barding.dm b/code/game/objects/items/barding.dm index ba5834d749b..0312415ca19 100644 --- a/code/game/objects/items/barding.dm +++ b/code/game/objects/items/barding.dm @@ -74,20 +74,20 @@ salvage_amount = 0 fiber_salvage = FALSE -/obj/item/clothing/barding/fogbeast +/obj/item/clothing/barding/honse name = "padded barding" - desc = "A set of padded body armor for a Fogbeast, designed to protect your mount's vital organs." + desc = "A set of padded body armor for a Honse, designed to protect your mount's vital organs." icon_state = "sewingkit" barding_icon = 'icons/mob/monster/fogbeast.dmi' barding_state = "barding" female_barding_state = "barding" valid_animal_types = list( - /mob/living/simple_animal/hostile/retaliate/fogbeast + /mob/living/simple_animal/hostile/retaliate/honse ) -/obj/item/clothing/barding/fogbeast/chain +/obj/item/clothing/barding/honse/chain name = "chainmail barding" - desc = "A set of chainmail body armor for a Fogbeast, designed to protect your mount's vital organs." + desc = "A set of chainmail body armor for a Honse, designed to protect your mount's vital organs." icon_state = "armorkit" barding_state = "barding_chain" female_barding_state = "barding_chain" diff --git a/code/game/objects/items/caparison.dm b/code/game/objects/items/caparison.dm index b4f81505d4d..186f1abbcdb 100644 --- a/code/game/objects/items/caparison.dm +++ b/code/game/objects/items/caparison.dm @@ -119,14 +119,14 @@ female_caparison_state = "heartfelt_caparison-f" ///////////////////////// -// SUBTYPES - FOGBEAST // +// SUBTYPES - HONSE // ///////////////////////// -/obj/item/caparison/fogbeast +/obj/item/caparison/honse name = "caparison" - desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Fogbeast." + desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Honse." caparison_icon = 'icons/mob/monster/fogbeast.dmi' - valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/fogbeast) + valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/honse) color = COLOR_WHITE detail_types = list("Quad" = "quad") symbol_types = list("Psycross" = "psycross", "Astrata" = "astrata") diff --git a/code/modules/crafting/anvil_recipes/armor.dm b/code/modules/crafting/anvil_recipes/armor.dm index 6090e253206..75804e2c539 100644 --- a/code/modules/crafting/anvil_recipes/armor.dm +++ b/code/modules/crafting/anvil_recipes/armor.dm @@ -762,11 +762,11 @@ additional_items = list(/obj/item/ingot/steel) created_item = /obj/item/clothing/barding/chain -/datum/anvil_recipe/armor/steel/barding/fogbeast - name = "Fogbeast Barding, Chainmail (+1 Steel)" +/datum/anvil_recipe/armor/steel/barding/honse + name = "Honse Barding, Chainmail (+1 Steel)" req_bar = /obj/item/ingot/steel additional_items = list(/obj/item/ingot/steel) - created_item = /obj/item/clothing/barding/fogbeast/chain + created_item = /obj/item/clothing/barding/honse/chain /* /datum/anvil_recipe/armor/steel/warden_helm diff --git a/code/modules/crafting/quality_of_crafting/sewing.dm b/code/modules/crafting/quality_of_crafting/sewing.dm index c467a0f28ee..ce844b0686c 100644 --- a/code/modules/crafting/quality_of_crafting/sewing.dm +++ b/code/modules/crafting/quality_of_crafting/sewing.dm @@ -1740,9 +1740,9 @@ /obj/item/natural/fibers = 1) craftdiff = 3 -/datum/repeatable_crafting_recipe/sewing/barding/fogbeast - name = "padded barding (fogbeast)" - output = /obj/item/clothing/barding/fogbeast +/datum/repeatable_crafting_recipe/sewing/barding/honse + name = "padded barding (honse)" + output = /obj/item/clothing/barding/honse /datum/repeatable_crafting_recipe/sewing/caparison name = "caparison" @@ -1764,6 +1764,6 @@ name = "eoran caparison" output =/obj/item/caparison/eora -/datum/repeatable_crafting_recipe/sewing/caparison/fogbeast - name = "fogbeast caparison" - output =/obj/item/caparison/fogbeast +/datum/repeatable_crafting_recipe/sewing/caparison/honse + name = "honse caparison" + output =/obj/item/caparison/honse diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm index a837cb14b32..7d1b15abf7f 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm @@ -1,7 +1,7 @@ -GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_GRAY, "Black" = COLOR_ALMOST_BLACK, "Brown" = COLOR_DARK_BROWN, "Chestnut" = COLOR_DARK_ORANGE)) +GLOBAL_LIST_INIT(valid_honse_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_GRAY, "Black" = COLOR_ALMOST_BLACK, "Brown" = COLOR_DARK_BROWN, "Chestnut" = COLOR_DARK_ORANGE)) -/mob/living/simple_animal/hostile/retaliate/fogbeast - name = "fogbeast mare" +/mob/living/simple_animal/hostile/retaliate/honse + name = "honse mare" desc = "A distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." icon = 'icons/mob/monster/fogbeast.dmi' icon_state = "fogbeast" @@ -19,8 +19,8 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL /obj/item/natural/hide = 4, /obj/item/natural/bundle/bone/full = 1 ) - base_intents = list(/datum/intent/simple/fogbeast) - animal_species = /mob/living/simple_animal/hostile/retaliate/fogbeast/male + base_intents = list(/datum/intent/simple/honse) + animal_species = /mob/living/simple_animal/hostile/retaliate/honse/male health = 380 maxHealth = 380 food_type = list( @@ -51,39 +51,39 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL aggressive = TRUE remains_type = /obj/effect/decal/remains/saiga ai_controller = /datum/ai_controller/saiga - var/fogbeast_color + var/honse_color var/can_breed = TRUE -/mob/living/simple_animal/hostile/retaliate/fogbeast/Initialize(mapload, var/set_fogbeast_color) +/mob/living/simple_animal/hostile/retaliate/honse/Initialize(mapload, set_honse_color) . = ..() - fogbeast_color = set_fogbeast_color - if(!fogbeast_color) - fogbeast_color = pick(GLOB.valid_fogbeast_colors) - color = GLOB.valid_fogbeast_colors[fogbeast_color] + honse_color = set_honse_color + if(!honse_color) + honse_color = pick(GLOB.valid_honse_colors) + color = GLOB.valid_honse_colors[honse_color] if(can_breed) AddComponent(\ /datum/component/breed,\ - list(/mob/living/simple_animal/hostile/retaliate/fogbeast),\ + list(/mob/living/simple_animal/hostile/retaliate/honse),\ 3 MINUTES,\ - list(/mob/living/simple_animal/hostile/retaliate/fogbeast/kid = 70, /mob/living/simple_animal/hostile/retaliate/fogbeast/kid/male = 30),\ + list(/mob/living/simple_animal/hostile/retaliate/honse/kid = 70, /mob/living/simple_animal/hostile/retaliate/honse/kid/male = 30),\ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/fogbeast/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) +/mob/living/simple_animal/hostile/retaliate/honse/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) return -/mob/living/simple_animal/hostile/retaliate/fogbeast/tame +/mob/living/simple_animal/hostile/retaliate/honse/tame tame = TRUE -/mob/living/simple_animal/hostile/retaliate/fogbeast/tame/saddled/Initialize() +/mob/living/simple_animal/hostile/retaliate/honse/tame/saddled/Initialize() . = ..() var/obj/item/natural/saddle/S = new(src) ssaddle = S update_icon() // BEHAVIORS -/mob/living/simple_animal/hostile/retaliate/fogbeast/update_icon() +/mob/living/simple_animal/hostile/retaliate/honse/update_icon() cut_overlays() ..() if(stat != DEAD) @@ -99,7 +99,7 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL add_overlay(mounted) -/mob/living/simple_animal/hostile/retaliate/fogbeast/get_sound(input) +/mob/living/simple_animal/hostile/retaliate/honse/get_sound(input) switch(input) if("aggro") return pick('sound/vo/mobs/saiga/attack (1).ogg','sound/vo/mobs/saiga/attack (2).ogg') @@ -111,17 +111,17 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL return pick('sound/vo/mobs/saiga/idle (1).ogg','sound/vo/mobs/saiga/idle (2).ogg','sound/vo/mobs/saiga/idle (3).ogg','sound/vo/mobs/saiga/idle (4).ogg','sound/vo/mobs/saiga/idle (5).ogg','sound/vo/mobs/saiga/idle (6).ogg','sound/vo/mobs/saiga/idle (7).ogg') -/mob/living/simple_animal/hostile/retaliate/fogbeast/tamed() +/mob/living/simple_animal/hostile/retaliate/honse/tamed() ..() deaggroprob = 20 if(can_buckle) AddComponent(/datum/component/riding/saiga) -/mob/living/simple_animal/hostile/retaliate/fogbeast/death() +/mob/living/simple_animal/hostile/retaliate/honse/death() unbuckle_all_mobs() return ..() -/mob/living/simple_animal/hostile/retaliate/fogbeast/simple_limb_hit(zone) +/mob/living/simple_animal/hostile/retaliate/honse/simple_limb_hit(zone) if(!zone) return "" switch(zone) @@ -163,7 +163,7 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL /// If we're a mount and are hit while sprinting, throw our rider off /// Also called if the rider is hit -/mob/living/simple_animal/hostile/retaliate/fogbeast/proc/check_sprint_dismount() +/mob/living/simple_animal/hostile/retaliate/honse/proc/check_sprint_dismount() SIGNAL_HANDLER for(var/mob/living/carbon/human/rider in buckled_mobs) if(rider.m_intent == MOVE_INTENT_RUN) @@ -171,42 +171,42 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL if(rider_skill < SKILL_LEVEL_MASTER) violent_dismount(rider) -/mob/living/simple_animal/hostile/retaliate/fogbeast/post_buckle_mob(mob/living/M) +/mob/living/simple_animal/hostile/retaliate/honse/post_buckle_mob(mob/living/M) . = ..() RegisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) if(!has_buckled_mobs()) RegisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) -/mob/living/simple_animal/hostile/retaliate/fogbeast/post_unbuckle_mob(mob/living/M) +/mob/living/simple_animal/hostile/retaliate/honse/post_unbuckle_mob(mob/living/M) . = ..() UnregisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) if(!has_buckled_mobs()) UnregisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) -/obj/effect/decal/remains/fogbeast +/obj/effect/decal/remains/honse name = "remains" - desc = "The remains of a once-proud fogbeast. Perhaps it was killed for food, or slain in battle with a valiant knight atop?" + desc = "The remains of a once-proud honse. Perhaps it was killed for food, or slain in battle with a valiant knight atop?" gender = PLURAL icon_state = "skele" icon = 'icons/mob/monster/fogbeast.dmi' -/mob/living/simple_animal/hostile/retaliate/fogbeast/male - name = "fogbeast stallion" +/mob/living/simple_animal/hostile/retaliate/honse/male + name = "honse stallion" gender = MALE -/mob/living/simple_animal/hostile/retaliate/fogbeast/male/tame +/mob/living/simple_animal/hostile/retaliate/honse/male/tame tame = TRUE -/mob/living/simple_animal/hostile/retaliate/fogbeast/male/tame/saddled/Initialize() +/mob/living/simple_animal/hostile/retaliate/honse/male/tame/saddled/Initialize() . = ..() var/obj/item/natural/saddle/S = new(src) ssaddle = S update_icon() // FOAL -/mob/living/simple_animal/hostile/retaliate/fogbeast/kid - name = "fogbeast filly" - desc = "A young fogbeast, likely to be running around with its mother. Fogbeasts are a distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." +/mob/living/simple_animal/hostile/retaliate/honse/kid + name = "honse filly" + desc = "A young honse, likely to be running around with its mother. Honses are a distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." icon = 'icons/mob/monster/fogbeast.dmi' icon_state = "foggie" icon_living = "foggie" @@ -225,7 +225,7 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL base_constitution = 5 base_strength = 5 base_speed = 5 - adult_growth = /mob/living/simple_animal/hostile/retaliate/fogbeast + adult_growth = /mob/living/simple_animal/hostile/retaliate/honse tame = TRUE can_buckle = FALSE can_saddle = FALSE @@ -233,12 +233,12 @@ GLOBAL_LIST_INIT(valid_fogbeast_colors, list("White" = COLOR_WHITE, "Gray" = COL ai_controller = /datum/ai_controller/saiga_kid can_breed = FALSE -/mob/living/simple_animal/hostile/retaliate/fogbeast/kid/male - name = "fogbeast colt" - adult_growth = /mob/living/simple_animal/hostile/retaliate/fogbeast/male +/mob/living/simple_animal/hostile/retaliate/honse/kid/male + name = "honse colt" + adult_growth = /mob/living/simple_animal/hostile/retaliate/honse/male // INTENT -/datum/intent/simple/fogbeast +/datum/intent/simple/honse name = "horse" icon_state = "instrike" attack_verb = list("tramples", "rams", "kicks") From 57a93314d82be5a5e130ef9e5478003ae48ee717 Mon Sep 17 00:00:00 2001 From: dwasint <82520990+dwasint@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:51:18 -0800 Subject: [PATCH 48/73] start of the animal eugenics Update animal_genetics.dm --- code/__DEFINES/animal_gene.dm | 37 ++ code/__DEFINES/traits/definitions.dm | 4 + code/_onclick/item_attack.dm | 5 + code/datums/ai/_ai_controller.dm | 39 ++ code/datums/components/udder.dm | 5 +- code/modules/farming/items/eggs.dm | 4 +- .../mob/living/simple_animal/examine.dm | 3 + .../mob/living/simple_animal/friendly/cat.dm | 3 - .../genetics/animal_genes/_animal_gene.dm | 66 +++ .../genetics/animal_genes/aggressive.dm | 36 ++ .../genetics/animal_genes/barren.dm | 29 ++ .../genetics/animal_genes/coat.dm | 71 ++++ .../genetics/animal_genes/diet.dm | 66 +++ .../genetics/animal_genes/docile.dm | 26 ++ .../genetics/animal_genes/dominant_lineage.dm | 20 + .../genetics/animal_genes/fat.dm | 33 ++ .../genetics/animal_genes/fecundity.dm | 31 ++ .../genetics/animal_genes/frail.dm | 21 + .../animal_genes/glowing_undercoat.dm | 13 + .../genetics/animal_genes/hardy.dm | 22 + .../genetics/animal_genes/hide.dm | 30 ++ .../genetics/animal_genes/lean.dm | 33 ++ .../genetics/animal_genes/productive.dm | 14 + .../genetics/animal_genes/prolific.dm | 70 ++++ .../genetics/animal_genes/slugish.dm | 25 ++ .../genetics/animal_genes/swift.dm | 26 ++ .../genetics/animal_genes/undercoat.dm | 59 +++ .../simple_animal/genetics/animal_genetics.dm | 395 ++++++++++++++++++ .../simple_animal/genetics/genetic_types.dm | 5 + .../hostile/retaliate/farm/chicken.dm | 13 +- .../hostile/retaliate/farm/cow.dm | 3 - .../hostile/retaliate/farm/goat.dm | 3 - .../hostile/retaliate/farm/trufflepig.dm | 3 - .../hostile/retaliate/game/fogbeast.dm | 12 +- .../hostile/retaliate/game/saiga.dm | 50 ++- .../hostile/retaliate/retaliate.dm | 2 + .../mob/living/simple_animal/simple_animal.dm | 27 +- icons/roguetown/mob/monster/saiga.dmi | Bin 59913 -> 22290 bytes vanderlin.dme | 22 + 39 files changed, 1297 insertions(+), 29 deletions(-) create mode 100644 code/__DEFINES/animal_gene.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/animal_genetics.dm create mode 100644 code/modules/mob/living/simple_animal/genetics/genetic_types.dm diff --git a/code/__DEFINES/animal_gene.dm b/code/__DEFINES/animal_gene.dm new file mode 100644 index 00000000000..9fe5a9cde99 --- /dev/null +++ b/code/__DEFINES/animal_gene.dm @@ -0,0 +1,37 @@ +#define GENE_GROUP_BODY_SIZE "body_size" +#define GENE_GROUP_SPEED "speed" +#define GENE_GROUP_CONSTITUTION "constitution" +#define GENE_GROUP_TEMPERAMENT "temperament" +#define GENE_GROUP_DIET "diet" +#define GENE_GROUP_HIDE "hide" +#define GENE_GROUP_COAT_COLOR "coat_color" +#define GENE_GROUP_UNDERCOAT "undercoat" +#define GENE_GROUP_BREEDING "breeding" +#define GENE_GROUP_PROGENY "progeny" +#define GENE_GROUP_EMISSIVE "emissive" + +#define GENETICS_TRAIT "genetics" +#define GENETICS_MUTATION_CHANCE 15 +#define GENETICS_EMERGENCE_CHANCE 40 +#define GENETICS_MAX_GENES 4 + +// How much random noise (+/-) is applied when averaging two parent intensities +#define GENETICS_INTENSITY_NOISE 1 +// Minimum fraction of intensity_min a bred gene can ever fall to +#define GENETICS_INTENSITY_FLOOR 9 +// Pass chance for dominant genes from a single parent +#define GENETICS_DOMINANT_PASS_CHANCE 70 +// Pass chance for recessive genes from a single parent, lower, needs both sides contributing to reliably transmit +#define GENETICS_RECESSIVE_PASS_CHANCE 40 + +#define RECESSIVE_NONE 0 +#define RECESSIVE_CARRIED 1 // one parent had it - silent carrier +#define RECESSIVE_EXPRESSED 2 // both parents had it - expresses + +#define GENE_FLAG_INTRINSIC (1<<0) // Always passed, doesn't count toward gene cap +#define GENE_FLAG_UNCOUNTED (1<<1) // This gene never gets counted towards the gene cap +#define GENE_FLAG_EXLUDE_WILD (1<<2) // This gene can never appear in the wild +#define GENE_FLAG_EMERGENCE (1<<3) // This gene will only appear through genetic mutation outside of the wild + +#define LINEAGE_MOTHER "mother" +#define LINEAGE_FATHER "father" diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index 1907d2838c6..5dbc79a93b9 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -528,3 +528,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// This object has sound debugging tools attached to it #define TRAIT_SOUND_DEBUGGED "sound_debugged" + +// genetic traits +#define TRAIT_ANIMAL_NATURAL_ARMOR "natural_armor" +#define TRAIT_ANIMAL_PRODUCTIVE "trait_productive" diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index d742d79ff95..30c304d5608 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -714,6 +714,11 @@ if(!type) return 0 var/armorval = 0 + if(HAS_TRAIT(src, TRAIT_ANIMAL_NATURAL_ARMOR) && genetics) + var/natural = genetics.get_natural_armor_for_type(type) + if(natural) + armorval += max(0, natural - armor_penetration) + if(bbarding && !bbarding.obj_broken) armorval = bbarding.armor.getRating(type) var/intdamage = damage diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 7765b366b00..5addf3879e5 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -105,6 +105,45 @@ have ways of interacting with a specific atom and control it. They posses a blac if(new_movement) change_ai_movement_type(new_movement) + +/** + * Removes a subtree from planning_subtrees by typepath. + * Safe to call whether planning_subtrees holds instances or typepaths. + */ +/datum/ai_controller/proc/remove_subtree(datum/ai_planning_subtree/subtree_type) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.type == subtree_type) + planning_subtrees -= subtree + return + +/** + * Adds a subtree at a given position (1-indexed) by typepath, resolving the singleton instance. + * If the subtree is already present it will not be added again. + * Position is clamped so 1 = top, length+1 (or any value beyond the list) = bottom. + */ +/datum/ai_controller/proc/add_subtree_at(datum/ai_planning_subtree/subtree_type, index = 1) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.type == subtree_type) + return // already present, do nothing + + var/datum/ai_planning_subtree/subtree_instance = GLOB.ai_subtrees[subtree_type] + if(!subtree_instance) + CRASH("add_subtree_at: subtree type [subtree_type] not found in GLOB.ai_subtrees") + + LAZYINITLIST(planning_subtrees) + index = clamp(index, 1, length(planning_subtrees) + 1) + planning_subtrees.Insert(index, subtree_instance) + +/** + * Returns the index of a subtree in planning_subtrees by typepath, or 0 if not found. + */ +/datum/ai_controller/proc/get_subtree_index(datum/ai_planning_subtree/subtree_type) + for(var/i in 1 to length(planning_subtrees)) + var/datum/ai_planning_subtree/subtree = planning_subtrees[i] + if(subtree.type == subtree_type) + return i + return 0 + ///Overrides the current ai_movement of this controller with a new one /datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement) ai_movement = SSai_movement.movement_types[new_movement] diff --git a/code/datums/components/udder.dm b/code/datums/components/udder.dm index d1b46377d6f..d036c3a560a 100644 --- a/code/datums/components/udder.dm +++ b/code/datums/components/udder.dm @@ -105,7 +105,10 @@ return COMPONENT_NO_AFTERATTACK /obj/item/udder/proc/handle_consumption(atom/movable/food, mob/user) - COOLDOWN_START(src, require_consume_cooldown, require_consume_timer) + var/cooldown_time = require_consume_timer + if(HAS_TRAIT(udder_mob, TRAIT_ANIMAL_PRODUCTIVE)) + cooldown_time *= 0.25 + COOLDOWN_START(src, require_consume_cooldown, cooldown_time) /obj/item/udder/Destroy() . = ..() diff --git a/code/modules/farming/items/eggs.dm b/code/modules/farming/items/eggs.dm index 06c8a668119..ca77a99e4fc 100644 --- a/code/modules/farming/items/eggs.dm +++ b/code/modules/farming/items/eggs.dm @@ -31,8 +31,10 @@ visible_message("[H] crushes [src] underfoot.") qdel(src) -/obj/item/reagent_containers/food/snacks/egg/proc/hatch(mob/living/simple_animal/hostile/retaliate/chicken/parent) +/obj/item/reagent_containers/food/snacks/egg/proc/hatch(mob/living/simple_animal/hostile/retaliate/chicken/parent, mob/living/simple_animal/hostile/retaliate/chicken/father) record_round_statistic(STATS_ANIMALS_BRED) var/mob/living/simple_animal/hostile/retaliate/chicken/chick/new_chick = new /mob/living/simple_animal/hostile/retaliate/chicken/chick(get_turf(parent)) SEND_SIGNAL(parent, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, new_chick) SEND_SIGNAL(parent, COMSIG_HAPPINESS_PASS_HAPPINESS, new_chick) + if(parent.genetics && !ispath(parent.genetics)) + parent.genetics.inherit_to(src, father) diff --git a/code/modules/mob/living/simple_animal/examine.dm b/code/modules/mob/living/simple_animal/examine.dm index 7b03bb0e0d2..b476159ddc0 100644 --- a/code/modules/mob/living/simple_animal/examine.dm +++ b/code/modules/mob/living/simple_animal/examine.dm @@ -115,5 +115,8 @@ if(bbarding) . += span_notice("This animal is wearing a bard: ([bbarding.name]).") + if(genetics && length(genetics.genes)) + . += span_notice("Genetic traits: [english_list(genetics.get_gene_names())].") + . += "ᛉ ------------ ᛉ" SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .) diff --git a/code/modules/mob/living/simple_animal/friendly/cat.dm b/code/modules/mob/living/simple_animal/friendly/cat.dm index 21844051181..c511ea1a613 100644 --- a/code/modules/mob/living/simple_animal/friendly/cat.dm +++ b/code/modules/mob/living/simple_animal/friendly/cat.dm @@ -157,9 +157,6 @@ matrix.Scale(0.5, 0.5) transform = matrix -/mob/living/simple_animal/pet/cat/proc/after_birth(mob/living/simple_animal/pet/cat/kitten/baby, mob/living/partner) - return - /mob/living/simple_animal/pet/cat/proc/wuv(change, mob/M) if(change) if(change > 0) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm new file mode 100644 index 00000000000..d288105cb03 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm @@ -0,0 +1,66 @@ +/datum/animal_gene + var/name = "Unknown Gene" + var/desc = "An unknown genetic trait." + var/rarity = 10 + var/exclusion_group = null + var/dominant = TRUE + var/recessive_state = RECESSIVE_NONE + ///if set this is the odds of emergence actually working (useful for non requirement emergence parts) + var/emergence_chance + var/intensity = 1.0 + var/intensity_min = 10 + var/intensity_max = 10 + var/applied = FALSE + var/gene_flags = NONE + var/list/type_whitelist = null + /// List of gene types, each parent must contribute at least one matching gene + /// for this gene to be eligible during breeding. Only checked when GENE_FLAG_EMERGENCE is set. + var/list/required_parent_genes = null + +/datum/animal_gene/New() + if(intensity_min != intensity_max) + intensity = rand(intensity_min, intensity_max) * 0.1 + else + intensity = intensity_min + +/datum/animal_gene/proc/allowed_for(mob/living/simple_animal/hostile/target) + if(!type_whitelist) + return TRUE + return is_type_in_list(target, type_whitelist) + +/datum/animal_gene/proc/apply_to(mob/living/simple_animal/hostile/target) + SHOULD_CALL_PARENT(TRUE) + var/old_apply = applied + applied = TRUE + return old_apply + +/datum/animal_gene/proc/remove_from(mob/living/simple_animal/hostile/target) + SHOULD_CALL_PARENT(TRUE) + var/old_apply = applied + applied = FALSE + return old_apply + +/// Produce an offspring gene from this gene and an optional matching gene from the other parent. +/// If other is null (unpaired), intensity is preserved with minor noise. +/// If paired, intensity trends toward the higher of the two, selective breeding accumulates. +/datum/animal_gene/proc/breed_with(datum/animal_gene/other) + var/datum/animal_gene/offspring = new type() + offspring.dominant = dominant + if(!offspring.dominant) + // Expression state is determined by _breed_pools based on whether both parents contributed + // Always start as CARRIED here; _breed_pools will upgrade to EXPRESSED if warranted + offspring.recessive_state = RECESSIVE_CARRIED + if(!other) + // Unpaired: pass with minor noise, floored at GENETICS_INTENSITY_FLOOR of intensity_min + var/noise = (rand(-GENETICS_INTENSITY_NOISE, GENETICS_INTENSITY_NOISE) * 0.1) * intensity + offspring.intensity = clamp(intensity + noise, intensity_min * GENETICS_INTENSITY_FLOOR, intensity_max) + return offspring + // Paired: average biased toward the higher value (stronger gene wins more often), + // plus noise. This means two fat parents consistently produce fatter offspring. + var/lo = min(intensity, other.intensity) + var/hi = max(intensity, other.intensity) + // Bias: pick in the upper 60% of the lo-hi range, then add noise + var/base = lo + (rand(4, 10) * 0.1) * (hi - lo) + var/noise = (rand(-GENETICS_INTENSITY_NOISE, GENETICS_INTENSITY_NOISE) * 0.1) * hi + offspring.intensity = clamp(base + noise, (intensity_min * GENETICS_INTENSITY_FLOOR) * 0.1, intensity_max * 0.1) + return offspring diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm new file mode 100644 index 00000000000..4096becc862 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm @@ -0,0 +1,36 @@ +/datum/animal_gene/aggressive + name = "Aggressive" + desc = "Territorial. Will attack nearby creatures unprovoked." + rarity = 3 + dominant = TRUE + exclusion_group = GENE_GROUP_TEMPERAMENT + + +/datum/animal_gene/aggressive/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + if(!target.ai_controller) + return + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/flee_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/look_for_adult) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/aggro_find_target, 1) + var/aggro_index = target.ai_controller.get_subtree_index(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/basic_melee_attack_subtree, aggro_index + 1) + if(!target.GetComponent(/datum/component/ai_aggro_system)) + target.AddComponent(/datum/component/ai_aggro_system) + target.melee_damage_lower = max(target.melee_damage_lower, 3) + target.melee_damage_upper = max(target.melee_damage_upper, 6) + +/datum/animal_gene/aggressive/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + if(!target.ai_controller) + return + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/basic_melee_attack_subtree) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/flee_target) + qdel(target.GetComponent(/datum/component/ai_aggro_system)) + target.melee_damage_lower = initial(target.melee_damage_lower) + target.melee_damage_upper = initial(target.melee_damage_upper) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm new file mode 100644 index 00000000000..7a14a93e726 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm @@ -0,0 +1,29 @@ +/datum/animal_gene/barren + name = "Barren" + desc = "Dulls reproductive drive, significantly extending the cooldown between breeding cycles." + rarity = 6 + exclusion_group = GENE_GROUP_BREEDING + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/barren/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // At max intensity, timer is doubled. At min, barely changed. + // Multiplier lands between ~1.05 (min) and 2.0 (max) + var/multiplier = 1.0 + (intensity * 1.0) + breed_component.breed_timer = breed_component.breed_timer * multiplier + +/datum/animal_gene/barren/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + var/multiplier = 1.0 + (intensity * 1.0) + breed_component.breed_timer = breed_component.breed_timer / multiplier diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm new file mode 100644 index 00000000000..2a77c4fc1f6 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm @@ -0,0 +1,71 @@ +/datum/animal_gene/coat_color + name = "Coat Color" + exclusion_group = GENE_GROUP_COAT_COLOR + dominant = TRUE + rarity = 10 + abstract_type = /datum/animal_gene/coat_color + gene_flags = GENE_FLAG_INTRINSIC + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/honse, + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) + +/datum/animal_gene/coat_color/breed_with(datum/animal_gene/other) + if(!other) + return new type() + // 50/50 which parent's coat the foal inherits + if(prob(50)) + return new type() + return new other.type() + +/datum/animal_gene/coat_color/proc/get_color() + return COLOR_WHITE + +/datum/animal_gene/coat_color/apply_to(mob/living/simple_animal/target) + . = ..() + if(istype(target, /mob/living/simple_animal/hostile/retaliate/saiga) || istype(target, /mob/living/simple_animal/hostile/retaliate/saigabuck)) + return + target.color = get_color() + +/datum/animal_gene/coat_color/remove_from(mob/living/simple_animal/target) + . = ..() + target.color = null + +/datum/animal_gene/coat_color/white + name = "White Coat" +/datum/animal_gene/coat_color/white/get_color() + return COLOR_WHITE + +/datum/animal_gene/coat_color/gray + name = "Gray Coat" +/datum/animal_gene/coat_color/gray/get_color() + return COLOR_GRAY + +/datum/animal_gene/coat_color/black + name = "Black Coat" +/datum/animal_gene/coat_color/black/get_color() + return COLOR_ALMOST_BLACK + +/datum/animal_gene/coat_color/brown + name = "Brown Coat" +/datum/animal_gene/coat_color/brown/get_color() + return COLOR_DARK_BROWN + +/datum/animal_gene/coat_color/chestnut + name = "Chestnut Coat" +/datum/animal_gene/coat_color/chestnut/get_color() + return COLOR_DARK_ORANGE + +/datum/animal_gene/coat_color/silver_dapple + name = "Silver Dapple Coat" + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + required_parent_genes = list( + /datum/animal_gene/coat_color/black, + /datum/animal_gene/coat_color/chestnut + ) + +/datum/animal_gene/coat_color/silver_dapple/get_color() + return COLOR_SILVER diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm new file mode 100644 index 00000000000..cb9389f3741 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm @@ -0,0 +1,66 @@ +/datum/animal_gene/diet + abstract_type = /datum/animal_gene/diet + exclusion_group = GENE_GROUP_DIET + var/list/added_foods = list() + var/list/removed_foods = list() + +/datum/animal_gene/diet/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + if(!target.food_type) + target.food_type = list() + for(var/F in removed_foods) + target.food_type -= F + for(var/F in added_foods) + if(!(F in target.food_type)) + target.food_type += F + if(target.ai_controller) + target.ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(target.food_type)) + +/datum/animal_gene/diet/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + if(!target.food_type) + return + for(var/F in added_foods) + target.food_type -= F + for(var/F in removed_foods) + if(!(F in target.food_type)) + target.food_type += F + if(target.ai_controller) + target.ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(target.food_type)) + +/datum/animal_gene/diet/strict_herbivore + name = "Strict Herbivore" + desc = "Will only eat plant matter. Refuses meat entirely." + rarity = 5 + removed_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + +/datum/animal_gene/diet/omnivore + name = "Opportunistic Omnivore" + desc = "Will eat almost anything, including meat and fish." + rarity = 4 + added_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + +/datum/animal_gene/diet/carnivore_instinct + name = "Carnivore Instinct" + desc = "A rare genetic throwback. This animal craves meat above all else." + rarity = 2 + dominant = TRUE + added_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + removed_foods = list( + /obj/item/reagent_containers/food/snacks/produce, + ) + type_whitelist = list(/mob/living/simple_animal/hostile/retaliate/cow) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm new file mode 100644 index 00000000000..7244920bef0 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm @@ -0,0 +1,26 @@ +/datum/animal_gene/docile + name = "Docile" + desc = "Exceptionally calm. Much easier to tame; won't flee when struck." + rarity = 4 + exclusion_group = GENE_GROUP_TEMPERAMENT + +/datum/animal_gene/docile/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.tame_chance = min(95, target.tame_chance + 30) + target.bonus_tame_chance += 10 + if(target.ai_controller) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/flee_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/basic_melee_attack_subtree) + +/datum/animal_gene/docile/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.tame_chance = max(0, target.tame_chance - 30) + target.bonus_tame_chance -= 10 + if(target.ai_controller) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/flee_target) + diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm new file mode 100644 index 00000000000..a1600abf26d --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm @@ -0,0 +1,20 @@ +/datum/animal_gene/dominant_lineage + name = "Dominant Lineage" + desc = "A powerful hereditary force that pulls offspring traits strongly toward one bloodline." + rarity = 3 // quite rare, strong selective effect + exclusion_group = GENE_GROUP_PROGENY + intensity_min = 1 + intensity_max = 10 + /// which parent this gene favors, set at creation, persists through inheritance + var/favored_side = LINEAGE_MOTHER + +/datum/animal_gene/dominant_lineage/New() + ..() + favored_side = pick(LINEAGE_MOTHER, LINEAGE_FATHER) + +/datum/animal_gene/dominant_lineage/breed_with(datum/animal_gene/other) + . = ..() + // Preserve the favored side through inheritance rather than re-rolling it + var/datum/animal_gene/dominant_lineage/offspring = . + offspring.favored_side = favored_side + return offspring diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm new file mode 100644 index 00000000000..57729e85dff --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm @@ -0,0 +1,33 @@ +/datum/animal_gene/fat + name = "Fat" + desc = "Heavyset build. More meat when butchered, but slower." + rarity = 8 + exclusion_group = GENE_GROUP_BODY_SIZE + intensity_min = 11 + intensity_max = 18 + +/datum/animal_gene/fat/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.genetic_butcher_scale = intensity + var/hp_bonus = round(initial(target.maxHealth) * (intensity - 1.0) * 0.5) + target.maxHealth += hp_bonus + target.health = min(target.health + hp_bonus, target.maxHealth) + var/delay_penalty = round((intensity - 1.0) * 5) + target.genetic_speed_delta += delay_penalty + if(target.ai_controller) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay + delay_penalty) + target.move_to_delay = max(1, target.move_to_delay + delay_penalty) + +/datum/animal_gene/fat/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.genetic_butcher_scale = initial(target.genetic_butcher_scale) + var/hp_bonus = round(initial(target.maxHealth) * (intensity - 1.0) * 0.5) + target.maxHealth -= hp_bonus + target.health = min(target.health, target.maxHealth) + var/delay_penalty = round((intensity - 1.0) * 5) + target.genetic_speed_delta -= delay_penalty + if(target.ai_controller) + target.ai_controller.movement_delay -= delay_penalty + target.move_to_delay = max(1, target.move_to_delay - delay_penalty) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm new file mode 100644 index 00000000000..46bf90924da --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm @@ -0,0 +1,31 @@ +/datum/animal_gene/fecundity + name = "Fecundity" + desc = "Affects reproductive drive, altering the cooldown between breeding cycles." + rarity = 6 + exclusion_group = GENE_GROUP_BREEDING + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/fecundity/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Intensity ranges 0.1–1.0; at max intensity, timer is halved. At min, barely changed. + // Multiplier lands between 0.5 (max) and ~0.95 (min) + var/multiplier = 1.0 - (intensity * 0.5) + breed_component.breed_timer = max(breed_component.breed_timer * multiplier, 10 SECONDS) + +/datum/animal_gene/fecundity/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Reverse the multiplier to restore the original timer + var/multiplier = 1.0 - (intensity * 0.5) + if(multiplier > 0) + breed_component.breed_timer = breed_component.breed_timer / multiplier diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm new file mode 100644 index 00000000000..885106959bc --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm @@ -0,0 +1,21 @@ +/datum/animal_gene/frail + name = "Frail" + desc = "Delicate constitution. Lower max health." + rarity = 6 + exclusion_group = GENE_GROUP_CONSTITUTION + intensity_min = 1 + intensity_max = 5 + +/datum/animal_gene/frail/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/loss = round(initial(target.maxHealth) * intensity) + target.maxHealth = max(5, target.maxHealth - loss) + target.health = min(target.health, target.maxHealth) + +/datum/animal_gene/frail/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/loss = round(initial(target.maxHealth) * intensity) + target.maxHealth += loss + target.health = min(target.health, target.maxHealth) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm new file mode 100644 index 00000000000..20b270557fa --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm @@ -0,0 +1,13 @@ +/datum/animal_gene/glowing_undercoat + name = "Glowing Undercoat" + exclusion_group = GENE_GROUP_EMISSIVE + dominant = TRUE + emergence_chance = 5 + rarity = 1 + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm new file mode 100644 index 00000000000..7471b409cb3 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm @@ -0,0 +1,22 @@ +/datum/animal_gene/hardy + name = "Hardy" + desc = "Robust constitution. Significantly higher max health." + rarity = 6 + + exclusion_group = GENE_GROUP_CONSTITUTION + intensity_min = 2 + intensity_max = 6 + +/datum/animal_gene/hardy/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/bonus = round(initial(target.maxHealth) * intensity) + target.maxHealth += bonus + target.health = min(target.health + bonus, target.maxHealth) + +/datum/animal_gene/hardy/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/bonus = round(initial(target.maxHealth) * intensity) + target.maxHealth -= bonus + target.health = min(target.health, target.maxHealth) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm new file mode 100644 index 00000000000..faca6d6c179 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm @@ -0,0 +1,30 @@ +/datum/animal_gene/hide + abstract_type = /datum/animal_gene/hide + exclusion_group = GENE_GROUP_HIDE + var/list/armor_covered = list() + +/datum/animal_gene/hide/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + ADD_TRAIT(target, TRAIT_ANIMAL_NATURAL_ARMOR, GENETICS_TRAIT) + +/datum/animal_gene/hide/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + REMOVE_TRAIT(target, TRAIT_ANIMAL_NATURAL_ARMOR, GENETICS_TRAIT) + +/datum/animal_gene/hide/thick_hide + name = "Thick Hide" + desc = "Dense skin. Absorbs melee and slashing damage." + rarity = 4 + intensity_min = 50 + intensity_max = 200 + armor_covered = list("stab", "slash") + +/datum/animal_gene/hide/ironhide + name = "Ironhide" + desc = "Unnaturally dense flesh. Resists most physical damage types." + rarity = 2 + intensity_min = 150 + intensity_max = 400 + armor_covered = list("stab", "slash", "piercing") diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm new file mode 100644 index 00000000000..9a6c5cb21de --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm @@ -0,0 +1,33 @@ +/datum/animal_gene/lean + name = "Lean" + desc = "Slender build. Less meat when butchered, but slightly quicker." + rarity = 8 + exclusion_group = GENE_GROUP_BODY_SIZE + intensity_min = 3 + intensity_max = 8 + +/datum/animal_gene/lean/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.genetic_butcher_scale = intensity + var/hp_loss = round(initial(target.maxHealth) * (1.0 - intensity) * 0.4) + target.maxHealth = max(5, target.maxHealth - hp_loss) + target.health = min(target.health, target.maxHealth) + var/delay_bonus = round((1.0 - intensity) * 3) + target.genetic_speed_delta -= delay_bonus + if(target.ai_controller) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay - delay_bonus) + target.move_to_delay = max(1, target.move_to_delay - delay_bonus) + +/datum/animal_gene/lean/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.genetic_butcher_scale = initial(target.genetic_butcher_scale) + var/hp_loss = round(initial(target.maxHealth) * (1.0 - intensity) * 0.4) + target.maxHealth += hp_loss + target.health = min(target.health, target.maxHealth) + var/delay_bonus = round((1.0 - intensity) * 3) + target.genetic_speed_delta += delay_bonus + if(target.ai_controller) + target.ai_controller.movement_delay += delay_bonus + target.move_to_delay += delay_bonus diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm new file mode 100644 index 00000000000..e4e7c51ad04 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm @@ -0,0 +1,14 @@ +/datum/animal_gene/productive + name = "Productive" + desc = "High-yield. Produces milk, wool, or eggs more frequently." + rarity = 4 + +/datum/animal_gene/productive/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + ADD_TRAIT(target, TRAIT_ANIMAL_PRODUCTIVE, GENETICS_TRAIT) + +/datum/animal_gene/productive/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + REMOVE_TRAIT(target, TRAIT_ANIMAL_PRODUCTIVE, GENETICS_TRAIT) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm new file mode 100644 index 00000000000..426491ee099 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm @@ -0,0 +1,70 @@ +/datum/animal_gene/prolific + name = "Prolific" + desc = "Grants a chance to birth multiple offspring in a single litter." + rarity = 4 + exclusion_group = GENE_GROUP_PROGENY + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/prolific/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Override the birth callback to inject our multi-birth logic + var/datum/callback/old_override = breed_component.override_baby + breed_component.override_baby = CALLBACK(src, PROC_REF(prolific_birth), target, breed_component, old_override) + +/datum/animal_gene/prolific/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Clear our override; if there was a prior override we stored it, but restoring + // arbitrary callback chains cleanly is complex, nulling is safest here. + // If your codebase commonly stacks overrides, consider a callback list instead. + breed_component.override_baby = null + +/// Handles the actual multi-birth. Spawns between 1 and max_litter babies, +/// weighted toward smaller litters, then fires post_birth for each. +/datum/animal_gene/prolific/proc/prolific_birth(mob/living/simple_animal/mother, datum/component/breed/breed_comp, datum/callback/old_override) + // At intensity 1.0 (max): ~60% chance of extra kids, up to 4 total + // At intensity 0.1 (min): ~6% chance of even one extra, max 2 total + var/extra_chance = intensity * 60 // 6%–60% + var/max_litter = max(1, round(intensity * 4)) // 1–4 extras on top of the base baby + + var/extras = 0 + for(var/i in 1 to max_litter) + if(prob(extra_chance)) + extras++ + else + break + + var/total = 1 + extras + var/turf/loc = get_turf(mother) + + if(old_override) + // Respect any existing override for the base baby + old_override.Invoke() + else + // Spawn the base baby normally + var/picked = pickweight(breed_comp.baby_path) + var/mob/living/base_baby = new picked(loc) + SEND_SIGNAL(mother, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, base_baby) + SEND_SIGNAL(mother, COMSIG_HAPPINESS_PASS_HAPPINESS, base_baby) + breed_comp.post_birth?.Invoke(base_baby, null) + + // Spawn any extras + for(var/i in 1 to extras) + var/picked = pickweight(breed_comp.baby_path) + var/mob/living/extra_baby = new picked(loc) + SEND_SIGNAL(mother, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, extra_baby) + SEND_SIGNAL(mother, COMSIG_HAPPINESS_PASS_HAPPINESS, extra_baby) + breed_comp.post_birth?.Invoke(extra_baby, null) + + if(total > 1) + mother.visible_message(span_notice("[mother] gives birth to a litter of [total]!")) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm new file mode 100644 index 00000000000..2c8353b2085 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm @@ -0,0 +1,25 @@ +/datum/animal_gene/sluggish + name = "Sluggish" + desc = "Slower than average. Easy to herd." + rarity = 8 + exclusion_group = GENE_GROUP_SPEED + intensity_min = 10 + intensity_max = 50 + +/datum/animal_gene/sluggish/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/penalty = round(intensity) + target.genetic_speed_delta += penalty + if(target.ai_controller) + target.ai_controller.movement_cooldown += penalty + target.move_to_delay += penalty + +/datum/animal_gene/sluggish/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/penalty = round(intensity) + target.genetic_speed_delta -= penalty + if(target.ai_controller) + target.ai_controller.movement_cooldown -= penalty + target.move_to_delay -= penalty diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm new file mode 100644 index 00000000000..126fdbe6d4f --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm @@ -0,0 +1,26 @@ +/datum/animal_gene/swift + name = "Swift" + desc = "Faster than average. Harder to herd or catch." + rarity = 6 + exclusion_group = GENE_GROUP_SPEED + intensity_min = 5 + intensity_max = 30 + +/datum/animal_gene/swift/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/reduction = round(intensity) + target.genetic_speed_delta -= reduction + if(target.ai_controller) + target.ai_controller.movement_cooldown = max(1, target.ai_controller.movement_cooldown - reduction) + + target.move_to_delay = max(1, target.move_to_delay - reduction) + +/datum/animal_gene/swift/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/reduction = round(intensity) + target.genetic_speed_delta += reduction + if(target.ai_controller) + target.ai_controller.movement_cooldown += reduction + target.move_to_delay += reduction diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm new file mode 100644 index 00000000000..3cbe779d42b --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm @@ -0,0 +1,59 @@ +/datum/animal_gene/undercoat + name = "Undercoat" + exclusion_group = GENE_GROUP_UNDERCOAT + dominant = TRUE + rarity = 10 + abstract_type = /datum/animal_gene/undercoat + gene_flags = GENE_FLAG_INTRINSIC + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) + +/datum/animal_gene/undercoat/breed_with(datum/animal_gene/other) + if(!other) + return new type() + if(prob(50)) + return new type() + return new other.type() + +/datum/animal_gene/undercoat/proc/get_color() + return COLOR_WHITE + +/datum/animal_gene/undercoat/white + name = "White Undercoat" +/datum/animal_gene/undercoat/white/get_color() + return COLOR_WHITE + +/datum/animal_gene/undercoat/gray + name = "Gray Undercoat" +/datum/animal_gene/undercoat/gray/get_color() + return COLOR_GRAY + +/datum/animal_gene/undercoat/black + name = "Black Undercoat" +/datum/animal_gene/undercoat/black/get_color() + return COLOR_ALMOST_BLACK + +/datum/animal_gene/undercoat/brown + name = "Brown Undercoat" +/datum/animal_gene/undercoat/brown/get_color() + return COLOR_DARK_BROWN + +/datum/animal_gene/undercoat/chestnut + name = "Chestnut Undercoat" +/datum/animal_gene/undercoat/chestnut/get_color() + return COLOR_DARK_ORANGE + +/datum/animal_gene/undercoat/silver_dapple + name = "Silver Dapple Undercoat" + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + required_parent_genes = list( + /datum/animal_gene/undercoat/black, + /datum/animal_gene/undercoat/chestnut + ) + +/datum/animal_gene/undercoat/silver_dapple/get_color() + return COLOR_SILVER diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm new file mode 100644 index 00000000000..61a8f911e83 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm @@ -0,0 +1,395 @@ +GLOBAL_LIST_INIT(all_animal_genes_weighted, generate_animaL_genes()) +/proc/generate_animaL_genes() + . = list() + for(var/datum/animal_gene/gene as anything in subtypesof(/datum/animal_gene)) + if(IS_ABSTRACT(gene)) + continue + .[gene] = initial(gene.rarity) + return . + +/datum/animal_genetics + var/datum/weakref/owner_ref = null + var/list/datum/animal_gene/genes = list() + var/list/guaranteed_genes = list() // list of gene TYPES that always roll on creation, e.g. list(/datum/animal_gene/coat_color/brown) + +/datum/animal_genetics/New(mob/living/simple_animal/owner) + owner_ref = WEAKREF(owner) + +/datum/animal_genetics/Destroy() + for(var/datum/animal_gene/G in genes) + qdel(G) + genes = list() + owner_ref = null + return ..() + +/datum/animal_genetics/proc/roll_guaranteed_genes() + var/list/true_genes = list() + for(var/datum/animal_gene/gene_type as anything in guaranteed_genes) + if(IS_ABSTRACT(gene_type)) + var/list/picked = list() + for(var/datum/animal_gene/gene as anything in subtypesof(gene_type)) + if((gene.gene_flags & GENE_FLAG_EXLUDE_WILD) || (gene.gene_flags & GENE_FLAG_EMERGENCE)) + continue + picked[gene] = initial(gene.rarity) + true_genes |= pickweight(picked) + continue + true_genes |= gene_type + + for(var/gene_type in true_genes) + var/datum/animal_gene/G = new gene_type() + if(!_insert_gene(G)) + qdel(G) + refresh() + +/datum/animal_genetics/proc/should_express(datum/animal_gene/G) + var/mob/living/simple_animal/owner = owner_ref?.resolve() + if(!owner || !G.allowed_for(owner)) + return FALSE + if(G.dominant) + return TRUE + return G.recessive_state == RECESSIVE_EXPRESSED + +/datum/animal_genetics/proc/_insert_gene(datum/animal_gene/new_gene) + if(!((new_gene.gene_flags & GENE_FLAG_INTRINSIC) || (new_gene.gene_flags & GENE_FLAG_UNCOUNTED))) + if(get_gene_count() >= GENETICS_MAX_GENES) + return FALSE + if(new_gene.exclusion_group) + for(var/datum/animal_gene/existing in genes) + if(existing.exclusion_group == new_gene.exclusion_group) + genes -= existing + qdel(existing) + break + genes += new_gene + return TRUE + +/datum/animal_genetics/proc/refresh() + var/mob/living/simple_animal/owner = owner_ref?.resolve() + if(!owner) + return + for(var/datum/animal_gene/G in genes) + if(should_express(G)) + owner.remove_gene(G) + owner.apply_gene(G) + +/datum/animal_genetics/proc/add_gene(datum/animal_gene/new_gene) + if(!_insert_gene(new_gene)) + return FALSE + refresh() + return TRUE + +/datum/animal_genetics/proc/remove_gene(datum/animal_gene/target_gene) + if(!(target_gene in genes)) + return FALSE + var/mob/living/simple_animal/owner = owner_ref?.resolve() + genes -= target_gene + owner.remove_gene(target_gene) + qdel(target_gene) + refresh() + return TRUE + +/datum/animal_genetics/proc/get_natural_armor_for_type(type_str) + var/total = 0 + for(var/datum/animal_gene/hide/H in genes) + if(should_express(H) && (type_str in H.armor_covered)) + total += round(H.intensity) + return total + +/datum/animal_genetics/proc/get_gene_count() + var/count = 0 + for(var/datum/animal_gene/G in genes) + if(!((G.gene_flags & GENE_FLAG_INTRINSIC) || (G.gene_flags & GENE_FLAG_UNCOUNTED))) + count++ + return count + +/datum/animal_genetics/proc/get_gene_names() + var/list/names = list() + for(var/datum/animal_gene/G in genes) + names += should_express(G) ? G.name : "[G.name] (recessive)" + return names + +/datum/animal_genetics/proc/get_gene_by_exclusion_group(group) + for(var/datum/animal_gene/G in genes) + if(G.exclusion_group == group) + return G + return null + +/// Build this parent's contribution for an inheritance pass. +/// Returns an associative list of exclusion_group (or type path for ungrouped) -> gene datum. +/// Whether a gene is offered at all depends on dominant/recessive pass chances. +/// Dominant genes are more reliably passed; recessives need selection pressure from both sides. +/datum/animal_genetics/proc/_build_allele_pool() + var/list/pool = list() + for(var/datum/animal_gene/G in genes) + if((G.gene_flags & GENE_FLAG_INTRINSIC)) + var/key = G.exclusion_group ? G.exclusion_group : "[G.type]" + pool[key] = G + continue + var/pass_chance = G.dominant ? GENETICS_DOMINANT_PASS_CHANCE : GENETICS_RECESSIVE_PASS_CHANCE + if(!prob(pass_chance)) + continue + var/key = G.exclusion_group ? G.exclusion_group : "[G.type]" + if(pool[key]) + var/datum/animal_gene/existing = pool[key] + if(G.intensity > existing.intensity) + pool[key] = G + else + pool[key] = G + return pool + +/// Merge two allele pools (mother and father) into bred offspring genes. +/// Where both parents offer the same trait, breed_with() averages intensities biased upward. +/// Where only one parent offers a trait, it passes through unpaired with minor noise. +/datum/animal_genetics/proc/_breed_pools(list/mother_pool, list/father_pool, mob/living/simple_animal/mother, mob/living/simple_animal/father) + // Check if either parent carries an expressed dominant lineage gene + var/lineage_weight = 0.5 // default: equal chance from either side + var/datum/animal_gene/dominant_lineage/lineage_gene = null + + for(var/datum/animal_gene/dominant_lineage/G in genes) // mother's genes + if(should_express(G)) + lineage_gene = G + break + if(!lineage_gene && father?.genetics) + for(var/datum/animal_gene/dominant_lineage/G in father.genetics.genes) + if(father.genetics.should_express(G)) + lineage_gene = G + break + + if(lineage_gene) + // intensity 0.1–1.0 maps to a skew of 0.05–0.5 away from center + // at max intensity: 100% from favored side (weight = 0 or 1) + // at min intensity: 55/45 split + var/skew = lineage_gene.intensity * 0.5 + lineage_weight = (lineage_gene.favored_side == LINEAGE_MOTHER) ? (0.5 + skew) : (0.5 - skew) + + var/list/datum/animal_gene/result = list() + var/list/all_keys = list() + for(var/key in mother_pool) + all_keys |= key + for(var/key in father_pool) + all_keys |= key + + for(var/key in all_keys) + var/datum/animal_gene/MG = mother_pool[key] + var/datum/animal_gene/FG = father_pool[key] + var/datum/animal_gene/offspring + + if(MG && FG) + // Both parents have this trait, use lineage weight to pick whose + // intensity anchors the breed_with call, rather than always averaging evenly + if(prob(lineage_weight * 100)) + offspring = MG.breed_with(FG) + else + offspring = FG.breed_with(MG) + if(!offspring.dominant) + offspring.recessive_state = RECESSIVE_EXPRESSED + else + var/datum/animal_gene/source = MG ? MG : FG + // If the unpaired gene is from the non-favored side, it has a chance to be dropped + // entirely, making the favored lineage even more dominant at high intensity + if(lineage_gene) + var/is_mothers = MG != null + var/favors_mother = lineage_gene.favored_side == LINEAGE_MOTHER + var/from_favored = (is_mothers == favors_mother) + if(!from_favored && !prob(lineage_weight * 100)) + continue // discard this gene entirely, the favored bloodline crowds it out + offspring = source.breed_with(null) + if(!offspring.dominant) + offspring.recessive_state = RECESSIVE_CARRIED + result += offspring + return result + +/datum/animal_genetics/proc/_check_emergence_requirements(datum/animal_gene/candidate, list/mother_pool, list/father_pool) + if(!candidate.required_parent_genes || !length(candidate.required_parent_genes)) + return TRUE + // Check mother's pool for at least one match + var/mother_match = FALSE + for(var/key in mother_pool) + var/datum/animal_gene/G = mother_pool[key] + if(is_type_in_list(G, candidate.required_parent_genes)) + mother_match = TRUE + break + if(!mother_match) + return FALSE + // Check father's pool for at least one match + var/father_match = FALSE + for(var/key in father_pool) + var/datum/animal_gene/G = father_pool[key] + if(is_type_in_list(G, candidate.required_parent_genes)) + father_match = TRUE + break + return father_match + +/datum/animal_genetics/proc/inherit_to(mob/living/simple_animal/baby, mob/living/simple_animal/father) + if(!baby.genetics) + baby.genetics = new /datum/animal_genetics(baby) + var/list/mother_pool = _build_allele_pool() + var/list/father_pool = istype(father?.genetics) ? father.genetics._build_allele_pool() : list() + var/list/datum/animal_gene/candidates = _breed_pools(mother_pool, father_pool) + // Shuffle so no ordering bias when inserting up to the gene cap + candidates = shuffle(candidates) + for(var/datum/animal_gene/G in candidates) + if(!baby.genetics._insert_gene(G)) + qdel(G) + // Mutation pass on remaining open slots for a chance at +1 + var/open_slots = GENETICS_MAX_GENES - baby.genetics.get_gene_count() + if(open_slots) + if(prob(GENETICS_MUTATION_CHANCE)) + var/datum/animal_gene/mutant = genetics_roll_mutation(baby, FALSE, mother_pool, father_pool) + if(mutant && !baby.genetics._insert_gene(mutant)) + qdel(mutant) + + var/list/emergence_candidates = list() + for(var/gene_type in GLOB.all_animal_genes_weighted) + var/datum/animal_gene/tmp = new gene_type() + if(!(tmp.gene_flags & GENE_FLAG_EMERGENCE)) + qdel(tmp) + continue + if(!tmp.allowed_for(baby)) + qdel(tmp) + continue + if(_check_emergence_requirements(tmp, mother_pool, father_pool)) + emergence_candidates[gene_type] = GLOB.all_animal_genes_weighted[gene_type] + qdel(tmp) + + if(length(emergence_candidates) && prob(GENETICS_EMERGENCE_CHANCE)) + var/datum/animal_gene/path = pickweight(emergence_candidates) + var/datum/animal_gene/emergent = new path() + if(!baby.genetics._insert_gene(emergent)) + qdel(emergent) + + baby.genetics.refresh() + var/list/expressed = list() + for(var/datum/animal_gene/G in baby.genetics.genes) + if(baby.genetics.should_express(G)) + expressed += G.name + if(length(expressed)) + baby.visible_message(span_notice("[baby] is born showing distinct traits: [english_list(expressed)].")) + +/datum/animal_genetics/proc/copy_to(mob/living/simple_animal/target) + if(!target.genetics) + target.genetics = new /datum/animal_genetics(target) + else + for(var/datum/animal_gene/G in target.genetics.genes) + G.remove_from(target) + qdel(G) + target.genetics.genes = list() + + for(var/datum/animal_gene/G in genes) + var/datum/animal_gene/copy = new G.type() + copy.dominant = G.dominant + copy.intensity = G.intensity + copy.recessive_state = G.recessive_state + copy.gene_flags = G.gene_flags + if(!target.genetics._insert_gene(copy)) + qdel(copy) + target.genetics.refresh() + +/proc/genetics_roll_mutation(mob/living/simple_animal/hostile/target, wild = FALSE, list/mother_pool = null, list/father_pool = null) + var/list/valid = list() + for(var/gene_type in GLOB.all_animal_genes_weighted) + var/datum/animal_gene/tmp = new gene_type() + if(wild && (tmp.gene_flags & GENE_FLAG_EXLUDE_WILD)) + qdel(tmp) + continue + // Emergence-flagged genes are never rolled in the wild + if(wild && (tmp.gene_flags & GENE_FLAG_EMERGENCE)) + qdel(tmp) + continue + // If emergence is required during breeding, check parent pools + if((tmp.gene_flags & GENE_FLAG_EMERGENCE) && (mother_pool || father_pool)) + if(!_check_emergence_requirements(tmp, mother_pool || list(), father_pool || list())) + qdel(tmp) + continue + if(tmp.emergence_chance && !prob(tmp.emergence_chance)) + qdel(tmp) + continue + if(tmp.allowed_for(target)) + valid[gene_type] = GLOB.all_animal_genes_weighted[gene_type] + qdel(tmp) + if(!length(valid)) + return null + var/datum/animal_gene/new_gene = pickweight(valid) + return new new_gene() + +/proc/_check_emergence_requirements(datum/animal_gene/candidate, list/mother_pool, list/father_pool) + if(!candidate.required_parent_genes || !length(candidate.required_parent_genes)) + return TRUE + + var/list/remaining = candidate.required_parent_genes.Copy() + + for(var/key in mother_pool) + var/datum/animal_gene/G = mother_pool[key] + for(var/req_type in remaining) + if(istype(G, req_type)) + remaining -= req_type + break + + for(var/key in father_pool) + var/datum/animal_gene/G = father_pool[key] + for(var/req_type in remaining) + if(istype(G, req_type)) + remaining -= req_type + break + + return !length(remaining) + +/mob/living/simple_animal/proc/roll_initial_genetics(max_genes = 2, intensity_bound_cap = 0.4, recessive_bias = 30) + if(!genetics) + genetics = new /datum/animal_genetics(src) + + var/attempts = 0 + while(genetics.get_gene_count() < max_genes && attempts < max_genes * 4) + attempts++ + + var/datum/animal_gene/G = genetics_roll_mutation(src, TRUE) + if(!G) + continue + + G.intensity = rand(G.intensity_min, round(G.intensity_max * intensity_bound_cap, 1)) * 0.1 + + if(prob(recessive_bias)) + G.dominant = FALSE + + if(!G.dominant) + G.recessive_state = RECESSIVE_CARRIED + + if(!genetics._insert_gene(G)) + qdel(G) + + genetics.refresh() + +/mob/living/simple_animal/proc/debug_apply_max_genetics() + if(!genetics) + genetics = new /datum/animal_genetics(src) + + var/datum/animal_gene/swift/swift = new() + swift.intensity = initial(swift.intensity_max) * 0.1 + if(!genetics._insert_gene(swift)) + qdel(swift) + + var/datum/animal_gene/lean/lean = new() + lean.intensity = initial(lean.intensity_max) * 0.1 + if(!genetics._insert_gene(lean)) + qdel(lean) + + var/datum/animal_gene/hardy/hardy = new() + hardy.dominant = TRUE + hardy.intensity = initial(hardy.intensity_max) * 0.1 + + if(!genetics._insert_gene(hardy)) + qdel(hardy) + + var/datum/animal_gene/hide/ironhide/ironhide = new() + ironhide.dominant = TRUE + ironhide.intensity = initial(ironhide.intensity_max) * 0.1 + if(!genetics._insert_gene(ironhide)) + qdel(ironhide) + + genetics.refresh() + +/mob/living/simple_animal/proc/debug_breed_with(mob/living/simple_animal/father) + var/mob/living/simple_animal/baby = new type(loc) + baby.name = "Debug [name]" + if(genetics) + genetics.inherit_to(baby, father) + return baby diff --git a/code/modules/mob/living/simple_animal/genetics/genetic_types.dm b/code/modules/mob/living/simple_animal/genetics/genetic_types.dm new file mode 100644 index 00000000000..aa0aab76ea4 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/genetic_types.dm @@ -0,0 +1,5 @@ +/datum/animal_genetics/honse + guaranteed_genes = list(/datum/animal_gene/coat_color) + +/datum/animal_genetics/saiga + guaranteed_genes = list(/datum/animal_gene/coat_color, /datum/animal_gene/undercoat) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm index 26c819d49b5..3068aab3890 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm @@ -131,13 +131,22 @@ /mob/living/simple_animal/hostile/retaliate/chicken/Life() ..() if(SEND_SIGNAL(src, COMSIG_MOB_RETURN_HUNGER) > 0) - production = min(production + 1, 100) + var/productive = 1 + if(HAS_TRAIT(src, TRAIT_ANIMAL_PRODUCTIVE)) + productive *= 3 + production = min(production + productive, 100) /mob/living/simple_animal/hostile/retaliate/chicken/proc/hatch_eggs() for(var/obj/item/reagent_containers/food/snacks/egg/egg in loc) if(!egg.fertile) continue - egg.hatch(src) + var/mob/living/simple_animal/hostile/retaliate/chicken/suprise_father + for(var/mob/living/simple_animal/hostile/retaliate/chicken/potential_father in range(5, src)) + if(potential_father.gender == MALE) + suprise_father = potential_father + break + + egg.hatch(src, suprise_father) qdel(egg) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm index f7e6c3b71fe..f0539fbefb9 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm @@ -98,9 +98,6 @@ if("idle") return pick('sound/vo/mobs/cow/idle (1).ogg','sound/vo/mobs/cow/idle (2).ogg','sound/vo/mobs/cow/idle (3).ogg','sound/vo/mobs/cow/idle (4).ogg','sound/vo/mobs/cow/idle (5).ogg') -/mob/living/simple_animal/hostile/retaliate/cow/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /mob/living/simple_animal/hostile/retaliate/cow/simple_limb_hit(zone) switch(zone) if(BODY_ZONE_PRECISE_NOSE, BODY_ZONE_PRECISE_MOUTH) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm index c71b705dc33..cae0feb3a34 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm @@ -104,9 +104,6 @@ if(can_buckle) AddComponent(/datum/component/riding/gote) -/mob/living/simple_animal/hostile/retaliate/goat/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /// Called when we attack something in order to piece together the intent of the AI/user and provide desired behavior. The element might be okay here but I'd rather the fluff. /// Goats are really good at beating up plants by taking bites out of them, but we use the default attack for everything else /mob/living/simple_animal/hostile/retaliate/goat/proc/on_pre_attack(datum/source, atom/target) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm index 90ddf3b2015..e282e9c333b 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm @@ -190,9 +190,6 @@ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/trufflepig/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /mob/living/simple_animal/hostile/retaliate/trufflepig/get_sound(input) switch(input) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm index 7d1b15abf7f..12d97735142 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm @@ -51,15 +51,13 @@ GLOBAL_LIST_INIT(valid_honse_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_ aggressive = TRUE remains_type = /obj/effect/decal/remains/saiga ai_controller = /datum/ai_controller/saiga + generate_genetics = TRUE + genetics = /datum/animal_genetics/honse var/honse_color var/can_breed = TRUE -/mob/living/simple_animal/hostile/retaliate/honse/Initialize(mapload, set_honse_color) +/mob/living/simple_animal/hostile/retaliate/honse/Initialize(mapload) . = ..() - honse_color = set_honse_color - if(!honse_color) - honse_color = pick(GLOB.valid_honse_colors) - color = GLOB.valid_honse_colors[honse_color] if(can_breed) AddComponent(\ @@ -70,9 +68,6 @@ GLOBAL_LIST_INIT(valid_honse_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/honse/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /mob/living/simple_animal/hostile/retaliate/honse/tame tame = TRUE @@ -232,6 +227,7 @@ GLOBAL_LIST_INIT(valid_honse_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_ aggressive = TRUE ai_controller = /datum/ai_controller/saiga_kid can_breed = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/honse/kid/male name = "honse colt" diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm index ca9644253e2..e1dad9eb47b 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm @@ -59,6 +59,9 @@ ai_controller = /datum/ai_controller/saiga + genetics = /datum/animal_genetics/saiga + generate_genetics = TRUE + var/can_breed = TRUE var/static/list/pet_commands = list( @@ -89,6 +92,26 @@ /mob/living/simple_animal/hostile/retaliate/saiga/update_overlays() . = ..() + + if(genetics) + var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) + var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) + var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) + + var/mutable_appearance/body = mutable_appearance(icon, "[icon_state]_reg1") + var/mutable_appearance/underbody = mutable_appearance(icon, "[icon_state]_reg2") + if(emissive) + var/mutable_appearance/glowing = emissive_appearance(icon, "[icon_state]_reg2") + . += glowing + if(CC) + body.color = CC.get_color() + if(!UC) + underbody.color = CC.get_color() + else + underbody.color = UC.get_color() + . += body + . += underbody + if(stat <= DEAD) return if(ssaddle) @@ -114,8 +137,6 @@ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/saiga/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return /mob/living/simple_animal/hostile/retaliate/saiga/get_sound(input) switch(input) @@ -198,6 +219,9 @@ ai_controller = /datum/ai_controller/saiga + genetics = /datum/animal_genetics/saiga + generate_genetics = TRUE + var/static/list/pet_commands = list( /datum/pet_command/idle, /datum/pet_command/free, @@ -226,6 +250,26 @@ /mob/living/simple_animal/hostile/retaliate/saigabuck/update_overlays() . = ..() + + if(genetics) + var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) + var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) + var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) + + var/mutable_appearance/body = mutable_appearance(icon, "[icon_state]_reg1") + var/mutable_appearance/underbody = mutable_appearance(icon, "[icon_state]_reg2") + if(emissive) + var/mutable_appearance/glowing = emissive_appearance(icon, "[icon_state]_reg2") + . += glowing + if(CC) + body.color = CC.get_color() + if(!UC) + underbody.color = CC.get_color() + else + underbody.color = UC.get_color() + . += body + . += underbody + if(stat <= DEAD) return if(ssaddle) @@ -301,6 +345,8 @@ can_breed = FALSE + generate_genetics = FALSE + ai_controller = /datum/ai_controller/saiga_kid /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm index a1d0b638259..a6f3657c2bb 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm @@ -157,6 +157,8 @@ var/old_hunger_percentage = old_hunger.current_hunger / old_hunger.max_hunger hunger.current_hunger = hunger.max_hunger * old_hunger_percentage + if(istype(genetics)) + genetics?.copy_to(A) qdel(src) return diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 8ae192644c7..7cbf19f0e0d 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -175,6 +175,11 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) var/obj/item/clothing/barding/bbarding var/caparison_over_barding = FALSE + var/datum/animal_genetics/genetics = /datum/animal_genetics + var/generate_genetics = FALSE + var/genetic_butcher_scale = 1.0 + var/genetic_speed_delta = 0 + /mob/living/simple_animal/Initialize() . = ..() if(gender == PLURAL) @@ -196,6 +201,10 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(happy_funtime_mob) AddComponent(/datum/component/friendship_container, mob_friends, "friend") AddComponent(/datum/component/happiness_container, 30, list(), list(), food_type) + if(generate_genetics) + genetics = new genetics(src) + genetics.roll_guaranteed_genes() + roll_initial_genetics() /mob/living/simple_animal/Destroy() if(nest) @@ -209,6 +218,7 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) QDEL_NULL(ccaparison) ccaparison = null + QDEL_NULL(genetics) return ..() @@ -399,6 +409,11 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(stuttering) stuttering = 0 +/mob/living/simple_animal/proc/after_birth(mob/living/simple_animal/baby, mob/living/partner) + if(genetics && !ispath(genetics)) + genetics.inherit_to(baby, partner) + return + /mob/living/simple_animal/proc/handle_automated_speech(override) set waitfor = FALSE if(speak_chance) @@ -523,7 +538,7 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) var/bonus_count = 0 // Track bonus items from happiness for(var/path in butcher_results) - var/amount = butcher_results[path] + var/amount = round(butcher_results[path] * genetic_butcher_scale) if(!do_after(user, time_per_cut, target = src)) if(botch_count || normal_count || perfect_count || bonus_count) to_chat(user, span_notice("I stop butchering: [butcher_summary(botch_count, normal_count, perfect_count, bonus_count, botch_chance, perfect_chance, happiness_bonus)].")) @@ -534,12 +549,12 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(prob(botch_chance)) botch_count++ if(length(botched_butcher_results) && (path in botched_butcher_results)) - amount = botched_butcher_results[path] + amount = round(botched_butcher_results[path] * genetic_butcher_scale, 1) else amount = 0 // Otherwise check for perfect else if(length(perfect_butcher_results) && (path in perfect_butcher_results) && prob(perfect_chance)) - amount = perfect_butcher_results[path] + amount = round(perfect_butcher_results[path] * genetic_butcher_scale, 1) perfect_count++ else normal_count++ @@ -1022,3 +1037,9 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) /mob/living/simple_animal/proc/eat_food_after(obj/item/reagent_containers/food/snacks/eaten) qdel(eaten) + +/mob/living/simple_animal/proc/apply_gene(datum/animal_gene/G) + G.apply_to(src) + +/mob/living/simple_animal/proc/remove_gene(datum/animal_gene/G) + G.remove_from(src) diff --git a/icons/roguetown/mob/monster/saiga.dmi b/icons/roguetown/mob/monster/saiga.dmi index 78b9ae6d4379914b8d16952fee936a77c407ec2c..3140e4d60ca7c0ab5de4e0c7efc5623affa9172b 100644 GIT binary patch literal 22290 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