From ae90e35b52fb5831692a7c1672a9bc4d915085f3 Mon Sep 17 00:00:00 2001 From: Spy <31124786+SpypigDev@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:18:55 +1000 Subject: [PATCH 1/6] port Anomaly AI initial system framework --- .../signals/atom/mob/living/signals_human.dm | 2 + code/__DEFINES/mobs.dm | 1 + code/__DEFINES/mode.dm | 1 + code/datums/components/human_ai.dm | 5 +- code/datums/pain/pain_anomaly.dm | 15 ++ .../game/objects/effects/temporary_visuals.dm | 4 + code/modules/admin/admin_verbs.dm | 1 + code/modules/asset_cache/asset_list_items.dm | 10 ++ code/modules/client/client_defines.dm | 4 + .../living/carbon/human/ai/brain/ai_brain.dm | 5 + code/modules/mob/living/carbon/human/say.dm | 2 + .../carbon/human/species/anomaly/_species.dm | 47 +++++ .../anomaly/anomaly_management_menu.dm | 118 +++++++++++++ .../anomaly/duplicate/ai_brain_duplicate.dm | 165 ++++++++++++++++++ .../species/anomaly/duplicate/duplicate.dm | 22 +++ .../living/carbon/human/species/species.dm | 2 + colonialmarines.dme | 5 + icons/misc/anomaly_menu.dmi | Bin 0 -> 1756 bytes .../styles/interfaces/AnomalyAISpawner.scss | 118 +++++++++++++ tgui/packages/tgui/styles/main.scss | 1 + 20 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 code/datums/pain/pain_anomaly.dm create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/_species.dm create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm create mode 100644 icons/misc/anomaly_menu.dmi create mode 100644 tgui/packages/tgui/styles/interfaces/AnomalyAISpawner.scss diff --git a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm index e34ea5b870..2a52e9d71f 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm @@ -1,6 +1,8 @@ ///from /mob/living/carbon/human/proc/force_say(): () #define COMSIG_HUMAN_FORCESAY "human_forcesay" +#define COMSIG_HUMAN_SAY "human_say" + #define COMSIG_HUMAN_TAKE_DAMAGE "human_take_damage" #define COMPONENT_BLOCK_DAMAGE (1<<0) diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index f6295f7a8d..3f58d4a42a 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -349,6 +349,7 @@ GLOBAL_LIST_INIT(default_xeno_onmob_icons, list( #define SPECIES_SYNTHETIC "Synthetic" #define SPECIES_MONKEY "Monkey" #define SPECIES_ZOMBIE "Zombie" +#define SPECIES_ANOMALY "Anomaly" #define ALL_LIMBS list("head","chest","groin","l_leg","l_foot","r_leg","r_foot","l_arm","l_hand","r_arm","r_hand") #define MOVEMENT_LIMBS list("l_leg", "l_foot", "r_leg", "r_foot") diff --git a/code/__DEFINES/mode.dm b/code/__DEFINES/mode.dm index cdec216f6b..8dddd65772 100644 --- a/code/__DEFINES/mode.dm +++ b/code/__DEFINES/mode.dm @@ -294,6 +294,7 @@ DEFINE_BITFIELD(whitelist_status, list( #define FACTION_COLONIST "Colonist" #define FACTION_YAUTJA "Yautja" #define FACTION_ZOMBIE "Zombie" +#define FACTION_ANOMALY "Anomaly" #define FACTION_MONKEY "Monkey" // Nanu #define FACTION_MALF_SYNTH "Malfunctioning Synthetic" diff --git a/code/datums/components/human_ai.dm b/code/datums/components/human_ai.dm index f118e427bd..03f999ba99 100644 --- a/code/datums/components/human_ai.dm +++ b/code/datums/components/human_ai.dm @@ -17,7 +17,10 @@ SSticker.mode.toggleable_flags ^= MODE_HUMAN_AI_TWEAKS message_admins("Human AI tweaks have been enabled by spawning an AI. This can be disabled with the \"Toggle Human AI Tweaks\" verb.") - ai_brain = new(ai_human) + if(ai_human.species.default_ai_brain_type) + ai_brain = new ai_human.species.default_ai_brain_type(ai_human) + else + ai_brain = new(ai_human) GLOB.ai_humans += ai_human ai_human.mob_flags |= AI_CONTROLLED diff --git a/code/datums/pain/pain_anomaly.dm b/code/datums/pain/pain_anomaly.dm new file mode 100644 index 0000000000..c813f67c22 --- /dev/null +++ b/code/datums/pain/pain_anomaly.dm @@ -0,0 +1,15 @@ +/datum/pain/anomaly + current_pain = null + max_pain = 1000 + + threshold_mild = null + threshold_discomforting = null + threshold_moderate = null + threshold_distressing = null + threshold_severe = null + threshold_horrible = null + + feels_pain = FALSE + +/datum/pain/anomaly/apply_pain(amount = 0, type = BRUTE) + return FALSE diff --git a/code/game/objects/effects/temporary_visuals.dm b/code/game/objects/effects/temporary_visuals.dm index 87455b8d68..f9b28d7269 100644 --- a/code/game/objects/effects/temporary_visuals.dm +++ b/code/game/objects/effects/temporary_visuals.dm @@ -109,6 +109,10 @@ splatter_type = "csplatter" color = BLOOD_COLOR_SYNTHETIC +/obj/effect/temp_visual/dir_setting/bloodsplatter/anomaly + splatter_type = "csplatter" + color = BLOOD_COLOR_ZOMBIE + //------------------------------------------ //Shockwaves //------------------------------------------ diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index d58c62bbcf..52e42546e7 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -91,6 +91,7 @@ GLOBAL_LIST_INIT(admin_verbs_default, list( /client/proc/toggle_human_ai_tweaks, /client/proc/open_human_squad_spawner_panel, /client/proc/open_human_ai_spawner_panel, + /client/proc/open_anomaly_ai_spawner_panel, /client/proc/toggle_barricade_creation, // Stops cades from being built )) diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index de8c9c5de0..84a94e9817 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -381,6 +381,16 @@ return ..() +/datum/asset/spritesheet/anomaly_menu + name = "anomaly_menu" + +/datum/asset/spritesheet/anomaly_menu/register() + for(var/icon_state in icon_states('icons/misc/anomaly_menu.dmi')) + var/icon/icon_sprite = icon('icons/misc/anomaly_menu.dmi', icon_state) + icon_sprite.Scale(128, 128) + Insert(icon_state, icon_sprite) + + return ..() /datum/asset/spritesheet/gun_lineart name = "gunlineart" diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 45ae2956b7..58b6ff3063 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -152,5 +152,9 @@ /// Holds the human squad spawner panel for this client var/datum/human_ai_spawner_menu/human_spawn_menu + + /// Holds the anomaly spawner panel for this client + var/datum/anomaly_ai_spawner_menu/anomaly_spawn_menu + /// Sets LOOC messaging cooldown COOLDOWN_DECLARE(looc_cooldown) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm index 70d74e1853..dae0030219 100644 --- a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -362,3 +362,8 @@ GLOBAL_LIST_EMPTY(human_ai_brains) try_cover(bullet.angle, bullet.firer) else if(in_cover) on_shot_inside_cover(bullet.angle, bullet.firer) + +/// used when conditional AI spawns are required +/datum/human_ai_brain/proc/configure_custom_spawn(mob/living/carbon/human/parent) + return + diff --git a/code/modules/mob/living/carbon/human/say.dm b/code/modules/mob/living/carbon/human/say.dm index 5abaed78e9..f7c34669e6 100644 --- a/code/modules/mob/living/carbon/human/say.dm +++ b/code/modules/mob/living/carbon/human/say.dm @@ -180,6 +180,8 @@ ..(message, speaking, verb, alt_name, italics, message_range, speech_sound, sound_vol, 0, message_mode) //ohgod we should really be passing a datum here. + SEND_SIGNAL(src, COMSIG_HUMAN_SAY, message) + INVOKE_ASYNC(src, TYPE_PROC_REF(/mob/living/carbon/human, say_to_radios), used_radios, message, message_mode, verb, speaking) /mob/living/carbon/human/proc/say_to_radios(used_radios, message, message_mode, verb, speaking) diff --git a/code/modules/mob/living/carbon/human/species/anomaly/_species.dm b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm new file mode 100644 index 0000000000..15dd59552a --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm @@ -0,0 +1,47 @@ +/* + Datum-based species. Should make for much cleaner and easier to maintain mutantrace code. +*/ + +/datum/species/anomaly + ///Used for isx(y) checking of species groups + group = SPECIES_ANOMALY + name = SPECIES_ANOMALY + name_plural = "Anomalies" + +/datum/species/anomaly/larva_impregnated(obj/item/alien_embryo/embryo) + return + +/// Override to add an emote panel to a species +/datum/species/anomaly/open_emote_panel() + return + +/datum/species/anomaly/handle_npc(mob/living/carbon/human/H) + return + +/datum/species/anomaly/attempt_rock_paper_scissors() + return + +/datum/species/anomaly/attempt_high_five() + return + +/datum/species/anomaly/attempt_fist_bump() + return + +/datum/species/anomaly/apply_signals(mob/living/carbon/human/H) + return + +//Only used by horrors at the moment. Only triggers if the mob is alive and not dead. +/datum/species/anomaly/handle_unique_behavior(mob/living/carbon/human/H) + return + +// Used for checking on how each species would scream when they are burning +/datum/species/anomaly/handle_on_fire(humanoidmob) + // call this for each species so each has their own unique scream options when burning alive + // heebie-jebies made me do all this effort, I HATE YOU + return + +/datum/species/anomaly/handle_head_loss(mob/living/carbon/human/human) + return + +/datum/species/anomaly/handle_paygrades() + return diff --git a/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm b/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm new file mode 100644 index 0000000000..c20f282d11 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm @@ -0,0 +1,118 @@ +GLOBAL_LIST_EMPTY(anomaly_ai_spawn_presets) + +#define ANOMALY_CLASS_KETER "Keter" + +// TO DO +// +// menu icons +// scss design stuff + + +/datum/anomaly_ai_spawner_menu + var/static/list/lazy_ui_data = list() + +/datum/anomaly_ai_spawner_menu/New() + if(!length(GLOB.anomaly_ai_spawn_presets)) + for(var/datum/anomaly_ai_spawn_preset/preset_type as anything in subtypesof(/datum/anomaly_ai_spawn_preset)) + if(!preset_type::name || !preset_type::anomaly_type_ref) + continue + + if(!lazy_ui_data[preset_type::anomaly_class]) + lazy_ui_data[preset_type::anomaly_class] = list() + + var/datum/anomaly_ai_spawn_preset/preset_obj = new preset_type() + GLOB.anomaly_ai_spawn_presets["[preset_type]"] = preset_obj + + lazy_ui_data[preset_type::anomaly_class] += list(list( + "name" = preset_obj.name, + "description" = preset_obj.desc, + "path" = preset_type, + "anomaly_type" = preset_obj.anomaly_type_ref, + "icon" = preset_obj.icon_state, + )) + + +/datum/anomaly_ai_spawner_menu/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AnomalyAISpawner") + ui.open() + +/datum/anomaly_ai_spawner_menu/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/anomaly_menu), + ) + +/datum/anomaly_ai_spawner_menu/ui_state(mob/user) + return GLOB.admin_state + +/datum/anomaly_ai_spawner_menu/ui_data(mob/user) + var/list/data = list() + + return data + +/datum/anomaly_ai_spawner_menu/ui_static_data(mob/user) + var/list/data = list() + + data["presets"] = lazy_ui_data + + return data + +/datum/anomaly_ai_spawner_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("create_ai") + if(!params["path"]) + return + + var/datum/anomaly_ai_spawn_preset/gotten_path = text2path(params["path"]) + if(!gotten_path) + return + + var/mob/living/carbon/human/ai_human = new() + ai_human.set_species(gotten_path.anomaly_type_ref) + ai_human.AddComponent(/datum/component/human_ai) + //arm_equipment(ai_human, gotten_path::path, TRUE) + + ai_human.face_dir(ui.user.dir) + ai_human.forceMove(get_turf(ui.user)) + ai_human.get_ai_brain().configure_custom_spawn(ai_human) + return TRUE + +/client/proc/open_anomaly_ai_spawner_panel() + set name = "Manage Anomaly AI" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + if(!SSticker.mode) + to_chat(src, SPAN_WARNING("The round hasn't started yet!")) + return + + if(anomaly_spawn_menu) + anomaly_spawn_menu.tgui_interact(mob) + return + + anomaly_spawn_menu = new /datum/anomaly_ai_spawner_menu(src) + anomaly_spawn_menu.tgui_interact(mob) + +/datum/anomaly_ai_spawn_preset + var/name = "" + var/icon_state = "ss13" + var/anomaly_class = ANOMALY_CLASS_KETER + var/desc = "" + var/faction = FACTION_ANOMALY + var/anomaly_type_ref + +/datum/anomaly_ai_spawn_preset/keter + anomaly_class = ANOMALY_CLASS_KETER + +/datum/anomaly_ai_spawn_preset/keter/duplicate + name = "Duplicate" + icon_state = "duplicate" + desc = "Dangerous entity that mimics players before attacking" + anomaly_type_ref = "Duplicate" diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm new file mode 100644 index 0000000000..1489769b8e --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -0,0 +1,165 @@ +/datum/human_ai_brain/duplicate + /// The original mob the duplicant has copied + var/mob/living/carbon/human/alter + var/pretending_to_be_human = TRUE + var/mimic_timer + + micro_action_delay = 0.2 SECONDS + short_action_delay = 0.5 SECONDS + medium_action_delay = 2 SECONDS + long_action_delay = 5 SECONDS + /// Global multiplier for all AI action delays + action_delay_mult = 2 // Doubled from 1, gives hAI a believable time between actions + /// Factions that the AI won't engage in hostilities with. Controlled by the AI's faction + friendly_factions = list() + /// Factions that the AI will not become hostile to unless attacked + neutral_factions = list() + /// If TRUE, the AI will throw grenades at enemies who enter cover + grenading_allowed = FALSE + /// If TRUE, we care about the target being in view after shooting at them. If not, then we only do a line check instead + requires_vision = TRUE + + COOLDOWN_DECLARE(replicate_speech) + + enter_combat_lines = list( + "*scream", + "*warcry", + ) + + in_combat_line_chance = 100 + +/datum/human_ai_brain/duplicate/say_in_combat_line(chance) + if(!length(enter_combat_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT)) + return + tied_human.say(pick(enter_combat_lines)) + +/datum/human_ai_brain/duplicate/say_exit_combat_line() + return + +/datum/human_ai_brain/duplicate/on_squad_member_death() + return + +/datum/human_ai_brain/duplicate/say_grenade_thrown_line() + return + +/datum/human_ai_brain/duplicate/say_reload_line() + return + +/datum/human_ai_brain/duplicate/say_need_healing_line() + return + +/datum/human_ai_brain/duplicate/configure_custom_spawn(mob/living/carbon/human/target) + var/datum/squad/alter_target_squad = tgui_input_list(usr, "Select a squad for [tied_human] to join", "Select a squad", GLOB.RoleAuthority.squads) + var/list/alters_list = list() + if (!alter_target_squad) + return + if(!alter_target_squad.active || alter_target_squad.name == "Root") + return FALSE + + for(var/mob/living/carbon/human/marine_human as anything in alter_target_squad.marines_list) + alters_list |= marine_human + var/target_alter = tgui_input_list(usr, "Select a player for [tied_human] to imitate", "Select a player for [tied_human] to imitate", alters_list) + + if(!target_alter) + return + if(QDELETED(tied_human)) + return + alter = target_alter + neutral_factions |= alter.faction + replicate_alter(alter) + + COOLDOWN_START(src, replicate_speech, 1 SECONDS) + +/datum/human_ai_brain/duplicate/process(delta_time) + if(hold_position) + return + if(!alter) + return + if(tied_human.is_mob_incapacitated()) + for(var/action in ongoing_actions) + qdel(action) + ongoing_actions.Cut() + lose_target() + return + var/distance_to_alter = get_dist(tied_human, alter) + if(pretending_to_be_human && distance_to_alter < 9) + for(var/mob/living/viewing_mob in view(view_distance, tied_human)) + if(viewing_mob == tied_human) + continue + + if(viewing_mob == alter) + initial_contact_alter() + break + if(distance_to_alter < 36) + quick_approach = get_turf(alter) + ..() + +/datum/human_ai_brain/duplicate/proc/initial_contact_alter() + if(tied_human.client || !alter.client) + return + if(mimic_timer) + return + mimic_timer = addtimer(CALLBACK(src, PROC_REF(engage_alter)), 6 SECONDS, TIMER_STOPPABLE) + addtimer(CALLBACK(src, PROC_REF(turn_off_armor_lights)), 4 SECONDS) + RegisterSignal(alter, COMSIG_HUMAN_SAY, PROC_REF(replicate_speech)) + pretending_to_be_human = FALSE + hold_position = TRUE + +/datum/human_ai_brain/duplicate/proc/turn_off_armor_lights() + var/obj/item/clothing/suit/storage/marine/armor = tied_human.get_item_by_slot(WEAR_JACKET) + if(armor) + armor.turn_light(tied_human, FALSE) + +/datum/human_ai_brain/duplicate/proc/replicate_alter(mob/living/carbon/human/alter) + var/list/alter_equipment_list = list() + alter_equipment_list |= alter.get_equipped_items() + tied_human.create_hud() + for(var/obj/item/item in alter_equipment_list) + var/obj/item/new_item = new item.type() + tied_human.equip_to_appropriate_slot(new_item) + var/obj/item/clothing/suit/storage/marine/armor = tied_human.get_item_by_slot(WEAR_JACKET) + if(armor) + armor.turn_light(tied_human, TRUE) + tied_human.body_size = alter.body_size + tied_human.body_type = alter.body_type + tied_human.skin_color = alter.skin_color + + tied_human.gender = alter.gender + tied_human.r_hair = alter.r_hair + tied_human.g_hair = alter.g_hair + tied_human.b_hair = alter.b_hair + tied_human.r_facial = alter.r_facial + tied_human.g_facial = alter.g_facial + tied_human.b_facial = alter.b_facial + tied_human.h_style = alter.h_style + tied_human.f_style = alter.f_style + + tied_human.change_real_name(tied_human, alter.real_name) + + tied_human.regenerate_icons() + +/datum/human_ai_brain/duplicate/proc/replicate_speech(source, message) + if(!COOLDOWN_FINISHED(src, replicate_speech)) + return + COOLDOWN_START(src, replicate_speech, 1 SECONDS) + + tied_human.say(message) + +/datum/human_ai_brain/duplicate/proc/engage_alter() + UnregisterSignal(alter, COMSIG_HUMAN_SAY) + //emote + mimic_timer = null + holster_primary() + holster_melee() + //tied_human.has_fine_manipulation = FALSE + tied_human.a_intent_change(INTENT_HARM) + hold_position = FALSE + friendly_factions -= alter.faction + neutral_factions -= alter.faction + current_target = alter + quick_approach = get_turf(alter) + tied_human.r_eyes = 255 + tied_human.g_eyes = 0 + tied_human.b_eyes = 0 + tied_human.update_body() + enter_combat() diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm new file mode 100644 index 0000000000..6a60e6316b --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -0,0 +1,22 @@ +/datum/species/anomaly/duplicate + ///Used for isx(y) checking of species groups + group = SPECIES_ANOMALY + name = "Duplicate" + name_plural = "Duplicates" + uses_skin_color = TRUE + special_body_types = TRUE + unarmed_type = /datum/unarmed_attack/claws + secondary_unarmed_type = /datum/unarmed_attack/bite + pain_type = /datum/pain/anomaly + flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|HAS_UNDERWEAR + default_ai_brain_type = /datum/human_ai_brain/duplicate + gibbed_anim = "gibbed-h" + dusted_anim = "dust-h" + flesh_color = BLOOD_COLOR_ZOMBIE + blood_color = BLOOD_COLOR_ZOMBIE + bloodsplatter_type = /obj/effect/temp_visual/dir_setting/bloodsplatter/anomaly + death_sound = 'sound/voice/scream_horror1.ogg' + death_message = "falls still, one last inhuman screech escaping their stolen lungs..." + + brute_mod = 0.5 + burn_mod = 2 diff --git a/code/modules/mob/living/carbon/human/species/species.dm b/code/modules/mob/living/carbon/human/species/species.dm index 05d1b54863..62d69a73ca 100644 --- a/code/modules/mob/living/carbon/human/species/species.dm +++ b/code/modules/mob/living/carbon/human/species/species.dm @@ -119,6 +119,8 @@ var/has_species_tab_items = FALSE + var/default_ai_brain_type + /datum/species/New() if(unarmed_type) unarmed = new unarmed_type() diff --git a/colonialmarines.dme b/colonialmarines.dme index e2b70b1e95..ada702a3d4 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -631,6 +631,7 @@ #include "code\datums\origin\upp.dm" #include "code\datums\origin\uscm.dm" #include "code\datums\pain\_pain.dm" +#include "code\datums\pain\pain_anomaly.dm" #include "code\datums\pain\pain_human.dm" #include "code\datums\pain\pain_human_hero.dm" #include "code\datums\pain\pain_monkey.dm" @@ -2141,6 +2142,10 @@ #include "code\modules\mob\living\carbon\human\species\species.dm" #include "code\modules\mob\living\carbon\human\species\synthetic.dm" #include "code\modules\mob\living\carbon\human\species\zombie.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\_species.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\anomaly_management_menu.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\duplicate\ai_brain_duplicate.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\duplicate\duplicate.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\_emote.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\_species.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\damage.dm" diff --git a/icons/misc/anomaly_menu.dmi b/icons/misc/anomaly_menu.dmi new file mode 100644 index 0000000000000000000000000000000000000000..aee42db11bd71a537eae02bbc4a2f1875bdd8b42 GIT binary patch literal 1756 zcmV<21|#{2P)A|G{004JLA3P`!=BJhIv6#4xhgm5AKsqoE7$rG8RU|4vU~7{{QgR~~2x47D zjErpA+3@`Q{0ImW1_%u;EiEi7D>pSYWkoHpf=+m2M{id+Nk~YPaz=w+K*q+kpP-5l z4;~Q_5e*Fu9UUDuHa1dHQcO%tBqSsp92_YwHaSpaBOe<-a&nr5bg-n7a%-AnR!tcd z5;rm~8yXuUCn2<_!W$bGn|y4=nS15Nq=92#4+sk~JY+vIEL%%F$i1YfnR|qCS9fDh zz_y@`dSYp2ksKNsKR7Ozh;my~Mm0W9VOL69QbwnsuMiLrdT(GuJ~t*LBEYGDVOUJ4 zoQh>yLmdfFDZ*Bkpc$`yKaB_9`^iy#0_2eo`Eh^5;&r`5fFwryM;w;ZhDainGjE%TB zGg33tGfE(w;*!LYR3K9+H?b%)FI9<)GbOXA7${)K#hF%=n41b=!<7{m8yXW-RFRsO zPf$^EVo`d2NosNiE|tm(u6{0HzW@L$wmTJ$u|16d00g8-L_t(og{7DMU(-kshc|De zcZ#n)!eP?%MUVifgou!Gc%gluPbWmrj!h)&a6Zox+4SqXg3H*wz z%BF5YtBB&Wwhf1Xpr~?6hhEfEvO11nXL1=km&@_<7{9EhBt_D79om$d90_(N3tPs@ z@bee~jN`ndrcxHvroDEux4u$JMlK!Ii1WZ6mpiVs(c za$a77EiV^W9u{N%)wM?@$N>~U2Bk-9s|mo$`noCS4a3OG=KA_d0`Pck15@Y6#@gdC zfR$pQzn~QGCIQ$efqHJ4(nbwtwM3wRwq;`rf}RiotiA$q4P@yVc?SI;QebffmPo;7xm>A`N~K)hoXWrT{Kd;M zyik7m;`!DTe)1dICMomt6#gNjj^ohDuS^Sgh4v$W*W&@Ns@vP!)$bFivm*fPR;$(B z{{eXOc6WF8?VHK5gt#^5ccSgA30~5*m}^iAx=ydLHg@5t9%ELA_pwb|Qd69Cav` zAV%2RZ@+`+ckTVX2>{oJP>-PoHcIHUJMY__PN)4Iu7m*UyB^9%2JQnwJ?I2LIDmM# z5*C8`K%mGt^c49Bj}8!cR~~vFhXphn0QmFhucHqDXfy>9 z7fdq(gUFv#K8f-8S>dh7j{*EjlEs+93APl@*jo}7e{ zK?#NcHNWN$UWWCM_2Wkea`&H4-G5IH+6SlYARHFpN3)Mz0d^t+t_TT0fOZgs!;Ts+ z^%a5oJ3fozYa;+mp}QBH27B$j5r98BXQ}USK#kE+08GX(2*NXLMy6xUi38|Z1%!nB z+hJURN6B!q%=LLmlJSuOl2G75FAO0(iU7hqKe4cfU%r0pk>0njUq%!F0kY-@Mmz%3 zfDa%qz8oQ;*E=I;y Date: Fri, 10 Apr 2026 17:25:13 +1000 Subject: [PATCH 2/6] updates --- .../anomaly/duplicate/ai_brain_duplicate.dm | 5 ++++- .../human/species/anomaly/duplicate/duplicate.dm | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm index 1489769b8e..30dfec4cc2 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -147,7 +147,10 @@ /datum/human_ai_brain/duplicate/proc/engage_alter() UnregisterSignal(alter, COMSIG_HUMAN_SAY) - //emote + tied_human.emote("roar") + tied_human.speed = -1.5 + playsound(tied_human, 'sound/weapons/wristblades_on.ogg', 25) + tied_human.add_filter("empower_rage", 1, list("type" = "outline", "color" = "#440202", "size" = 1)) mimic_timer = null holster_primary() holster_melee() diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm index 6a60e6316b..c65b22f19f 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -5,8 +5,8 @@ name_plural = "Duplicates" uses_skin_color = TRUE special_body_types = TRUE - unarmed_type = /datum/unarmed_attack/claws - secondary_unarmed_type = /datum/unarmed_attack/bite + unarmed_type = /datum/unarmed_attack/claws/shredding + secondary_unarmed_type = /datum/unarmed_attack/bite/strong pain_type = /datum/pain/anomaly flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|HAS_UNDERWEAR default_ai_brain_type = /datum/human_ai_brain/duplicate @@ -20,3 +20,13 @@ brute_mod = 0.5 burn_mod = 2 + +//datum/species/anomaly/duplicate/handle_on_fire(humanoidmob) +// . = ..() +// INVOKE_ASYNC(humanoidmob, TYPE_PROC_REF(/mob, emote), pick("pain", "scream")) + +/datum/emote/living/carbon/human/anomaly/duplicate/roar + key = "roar" + message = "releases an inhuman roar!" + sound = 'sound/voice/xeno_praetorian_screech.ogg' + emote_type = EMOTE_AUDIBLE|EMOTE_VISIBLE From 17db808b5daeb22916626395b535658e9450bfda Mon Sep 17 00:00:00 2001 From: Spy <31124786+SpypigDev@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:33:07 +1000 Subject: [PATCH 3/6] Revert "updates" This reverts commit 20e9cdf542630a38e84d9d3dd018dc2ceb3e5b46. --- .../anomaly/duplicate/ai_brain_duplicate.dm | 5 +---- .../human/species/anomaly/duplicate/duplicate.dm | 14 ++------------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm index 30dfec4cc2..1489769b8e 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -147,10 +147,7 @@ /datum/human_ai_brain/duplicate/proc/engage_alter() UnregisterSignal(alter, COMSIG_HUMAN_SAY) - tied_human.emote("roar") - tied_human.speed = -1.5 - playsound(tied_human, 'sound/weapons/wristblades_on.ogg', 25) - tied_human.add_filter("empower_rage", 1, list("type" = "outline", "color" = "#440202", "size" = 1)) + //emote mimic_timer = null holster_primary() holster_melee() diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm index c65b22f19f..6a60e6316b 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -5,8 +5,8 @@ name_plural = "Duplicates" uses_skin_color = TRUE special_body_types = TRUE - unarmed_type = /datum/unarmed_attack/claws/shredding - secondary_unarmed_type = /datum/unarmed_attack/bite/strong + unarmed_type = /datum/unarmed_attack/claws + secondary_unarmed_type = /datum/unarmed_attack/bite pain_type = /datum/pain/anomaly flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|HAS_UNDERWEAR default_ai_brain_type = /datum/human_ai_brain/duplicate @@ -20,13 +20,3 @@ brute_mod = 0.5 burn_mod = 2 - -//datum/species/anomaly/duplicate/handle_on_fire(humanoidmob) -// . = ..() -// INVOKE_ASYNC(humanoidmob, TYPE_PROC_REF(/mob, emote), pick("pain", "scream")) - -/datum/emote/living/carbon/human/anomaly/duplicate/roar - key = "roar" - message = "releases an inhuman roar!" - sound = 'sound/voice/xeno_praetorian_screech.ogg' - emote_type = EMOTE_AUDIBLE|EMOTE_VISIBLE From a610d9250709e9c2ad6e846f0d792681b64a446f Mon Sep 17 00:00:00 2001 From: Spy <31124786+SpypigDev@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:42:43 +1000 Subject: [PATCH 4/6] initial --- .../carbon/human/species/anomaly/_species.dm | 4 ++++ .../anomaly/duplicate/ai_brain_duplicate.dm | 20 +++++++++++++------ .../species/anomaly/duplicate/duplicate.dm | 13 +++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/code/modules/mob/living/carbon/human/species/anomaly/_species.dm b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm index 15dd59552a..465df6a267 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/_species.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm @@ -15,6 +15,10 @@ /datum/species/anomaly/open_emote_panel() return +/datum/emote/living/carbon/human/anomaly + species_type_allowed_typecache = list(/datum/species/anomaly) + emote_type = EMOTE_AUDIBLE + /datum/species/anomaly/handle_npc(mob/living/carbon/human/H) return diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm index 1489769b8e..f72c8ac417 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -18,12 +18,12 @@ grenading_allowed = FALSE /// If TRUE, we care about the target being in view after shooting at them. If not, then we only do a line check instead requires_vision = TRUE - + ignore_looting = TRUE COOLDOWN_DECLARE(replicate_speech) enter_combat_lines = list( - "*scream", - "*warcry", + "*roar", + "*roar", ) in_combat_line_chance = 100 @@ -99,6 +99,11 @@ return if(mimic_timer) return + // no using guns allowed + if(primary_weapon) + qdel(primary_weapon) + for(var/obj/item/weapon as anything in secondary_weapons) + qdel(weapon) mimic_timer = addtimer(CALLBACK(src, PROC_REF(engage_alter)), 6 SECONDS, TIMER_STOPPABLE) addtimer(CALLBACK(src, PROC_REF(turn_off_armor_lights)), 4 SECONDS) RegisterSignal(alter, COMSIG_HUMAN_SAY, PROC_REF(replicate_speech)) @@ -106,6 +111,7 @@ hold_position = TRUE /datum/human_ai_brain/duplicate/proc/turn_off_armor_lights() + playsound(tied_human, pick('sound/voice/pred_laugh3.ogg', 'sound/voice/pred_over_there.ogg', 'sound/voice/pred_itsatrap.ogg', 'sound/voice/pred_helpme.ogg'), 25) var/obj/item/clothing/suit/storage/marine/armor = tied_human.get_item_by_slot(WEAR_JACKET) if(armor) armor.turn_light(tied_human, FALSE) @@ -147,10 +153,12 @@ /datum/human_ai_brain/duplicate/proc/engage_alter() UnregisterSignal(alter, COMSIG_HUMAN_SAY) - //emote + tied_human.emote("roar") + tied_human.speed = -1 + playsound(tied_human, 'sound/weapons/wristblades_on.ogg', 25) + tied_human.add_filter("empower_rage", 1, list("type" = "outline", "color" = "#440202", "size" = 1)) mimic_timer = null - holster_primary() - holster_melee() + //tied_human.has_fine_manipulation = FALSE tied_human.a_intent_change(INTENT_HARM) hold_position = FALSE diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm index 6a60e6316b..06aa3151b2 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -5,8 +5,8 @@ name_plural = "Duplicates" uses_skin_color = TRUE special_body_types = TRUE - unarmed_type = /datum/unarmed_attack/claws - secondary_unarmed_type = /datum/unarmed_attack/bite + unarmed_type = /datum/unarmed_attack/claws/strong + secondary_unarmed_type = /datum/unarmed_attack/bite/strong pain_type = /datum/pain/anomaly flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|HAS_UNDERWEAR default_ai_brain_type = /datum/human_ai_brain/duplicate @@ -17,6 +17,13 @@ bloodsplatter_type = /obj/effect/temp_visual/dir_setting/bloodsplatter/anomaly death_sound = 'sound/voice/scream_horror1.ogg' death_message = "falls still, one last inhuman screech escaping their stolen lungs..." - + speech_sounds = list('sound/voice/hiss2.ogg', 'sound/voice/hiss3.ogg', 'sound/voice/hiss4.ogg') + speech_chance = 100 brute_mod = 0.5 burn_mod = 2 + +/datum/emote/living/carbon/human/anomaly/duplicate/roar + key = "roar" + message = "releases an inhuman roar!" + sound = 'sound/voice/xeno_praetorian_screech.ogg' + emote_type = EMOTE_AUDIBLE|EMOTE_VISIBLE From 2108595828df4397fdcca97ee9f5f6e42d521972 Mon Sep 17 00:00:00 2001 From: Spy <31124786+SpypigDev@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:45:20 +1000 Subject: [PATCH 5/6] updates --- .../carbon/human/ai/brain/ai_brain_items.dm | 2 +- .../anomaly/duplicate/ai_brain_duplicate.dm | 48 ++++++++++++++++++- .../species/anomaly/duplicate/duplicate.dm | 6 ++- .../living/carbon/human/unarmed_attacks.dm | 5 ++ code/modules/projectiles/projectile.dm | 1 - 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm index e4d88b3769..235ab57539 100644 --- a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm @@ -366,7 +366,7 @@ SIGNAL_HANDLER if(dropped == primary_weapon) - if(!(gun_data.disposable && !primary_weapon.ai_can_use(tied_human, src))) + if(!(gun_data?.disposable && !primary_weapon.ai_can_use(tied_human, src))) to_pickup |= dropped set_primary_weapon(null) diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm index f72c8ac417..61ef6777f1 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -1,3 +1,6 @@ +// TO DO LIST +// + /datum/human_ai_brain/duplicate /// The original mob the duplicant has copied var/mob/living/carbon/human/alter @@ -20,6 +23,14 @@ requires_vision = TRUE ignore_looting = TRUE COOLDOWN_DECLARE(replicate_speech) + COOLDOWN_DECLARE(pain_scream) + + var/static/list/pain_sounds = list( + 'sound/voice/pred_pain5.ogg', + 'sound/voice/pred_pain4.ogg', + 'sound/voice/pred_pain3.ogg', + 'sound/voice/pred_pain2.ogg' + ) enter_combat_lines = list( "*roar", @@ -67,8 +78,9 @@ alter = target_alter neutral_factions |= alter.faction replicate_alter(alter) - + RegisterSignal(tied_human, COMSIG_MOB_DEATH, PROC_REF(post_death), TRUE) COOLDOWN_START(src, replicate_speech, 1 SECONDS) + COOLDOWN_START(src, pain_scream, 2 SECONDS) /datum/human_ai_brain/duplicate/process(delta_time) if(hold_position) @@ -99,11 +111,14 @@ return if(mimic_timer) return + if(!pretending_to_be_human) + return // no using guns allowed if(primary_weapon) qdel(primary_weapon) for(var/obj/item/weapon as anything in secondary_weapons) qdel(weapon) + mimic_timer = addtimer(CALLBACK(src, PROC_REF(engage_alter)), 6 SECONDS, TIMER_STOPPABLE) addtimer(CALLBACK(src, PROC_REF(turn_off_armor_lights)), 4 SECONDS) RegisterSignal(alter, COMSIG_HUMAN_SAY, PROC_REF(replicate_speech)) @@ -116,6 +131,16 @@ if(armor) armor.turn_light(tied_human, FALSE) +/datum/human_ai_brain/duplicate/proc/post_death() + tied_human.clear_filters() + addtimer(CALLBACK(src, PROC_REF(transform_corpse)), 3 SECONDS) + +/datum/human_ai_brain/duplicate/proc/transform_corpse() + playsound(tied_human, 'sound/weapons/vehicles/flamethrower.ogg', 35) + tied_human.fire_stacks = 25 // avert your gaze + tied_human.IgniteMob(TRUE) + tied_human.name = "\improper mangled corpse" + /datum/human_ai_brain/duplicate/proc/replicate_alter(mob/living/carbon/human/alter) var/list/alter_equipment_list = list() alter_equipment_list |= alter.get_equipped_items() @@ -144,6 +169,20 @@ tied_human.regenerate_icons() +/datum/human_ai_brain/duplicate/unholster_melee() + if(pretending_to_be_human) + return ..() + +/datum/human_ai_brain/duplicate/proc/scream_in_pain() + if(!COOLDOWN_FINISHED(src, pain_scream)) + return + // shh, we're trying to sleep + if(tied_human.stat) + return + COOLDOWN_START(src, replicate_speech, 2 SECONDS) + + playsound(tied_human, pick(pain_sounds), 50) + /datum/human_ai_brain/duplicate/proc/replicate_speech(source, message) if(!COOLDOWN_FINISHED(src, replicate_speech)) return @@ -152,9 +191,11 @@ tied_human.say(message) /datum/human_ai_brain/duplicate/proc/engage_alter() + if(pretending_to_be_human) + return UnregisterSignal(alter, COMSIG_HUMAN_SAY) tied_human.emote("roar") - tied_human.speed = -1 + tied_human.speed = -1.5 playsound(tied_human, 'sound/weapons/wristblades_on.ogg', 25) tied_human.add_filter("empower_rage", 1, list("type" = "outline", "color" = "#440202", "size" = 1)) mimic_timer = null @@ -170,4 +211,7 @@ tied_human.g_eyes = 0 tied_human.b_eyes = 0 tied_human.update_body() + + RegisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT, PROC_REF(scream_in_pain), TRUE) + enter_combat() diff --git a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm index 06aa3151b2..8503c10a49 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -5,7 +5,7 @@ name_plural = "Duplicates" uses_skin_color = TRUE special_body_types = TRUE - unarmed_type = /datum/unarmed_attack/claws/strong + unarmed_type = /datum/unarmed_attack/claws/shredding secondary_unarmed_type = /datum/unarmed_attack/bite/strong pain_type = /datum/pain/anomaly flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|HAS_UNDERWEAR @@ -22,6 +22,10 @@ brute_mod = 0.5 burn_mod = 2 +//datum/species/anomaly/duplicate/handle_on_fire(humanoidmob) +// . = ..() +// INVOKE_ASYNC(humanoidmob, TYPE_PROC_REF(/mob, emote), pick("pain", "scream")) + /datum/emote/living/carbon/human/anomaly/duplicate/roar key = "roar" message = "releases an inhuman roar!" diff --git a/code/modules/mob/living/carbon/human/unarmed_attacks.dm b/code/modules/mob/living/carbon/human/unarmed_attacks.dm index f933e85320..cd168c7da6 100644 --- a/code/modules/mob/living/carbon/human/unarmed_attacks.dm +++ b/code/modules/mob/living/carbon/human/unarmed_attacks.dm @@ -57,6 +57,11 @@ damage = 10 shredding = TRUE +/datum/unarmed_attack/claws/shredding + attack_verb = list("slash") + damage = 25 + shredding = TRUE + /datum/unarmed_attack/bite/strong attack_verb = list("maul") damage = 15 diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 6dfc261fec..4d75b1e619 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -1059,7 +1059,6 @@ if(!stat && pain.feels_pain) emote("scream") to_chat(src, SPAN_HIGHDANGER("You scream in pain as the impact sends shrapnel into the wound!")) - SEND_SIGNAL(P, COMSIG_POST_BULLET_ACT_HUMAN, src, damage, damage_result) //Deal with xeno bullets. /mob/living/carbon/xenomorph/bullet_act(obj/projectile/P) From f0a861632f9bbccdec42626deb12744cf90428d0 Mon Sep 17 00:00:00 2001 From: Spy <31124786+SpypigDev@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:59:07 +1000 Subject: [PATCH 6/6] port shadow men --- code/__DEFINES/human_ai.dm | 1 + code/game/area/areas.dm | 1 + .../lighting_static/static_lighting_corner.dm | 2 +- .../lighting_static/static_lighting_object.dm | 2 +- .../human/ai/action_datums/action_datums.dm | 8 +- .../living/carbon/human/ai/brain/ai_brain.dm | 1 + .../carbon/human/species/anomaly/_species.dm | 79 ++++++++++++++++++ .../anomaly/anomaly_management_menu.dm | 10 +++ .../species/anomaly/shadow/ai_brain_shadow.dm | 73 ++++++++++++++++ .../species/anomaly/shadow/shadow_men.dm | 39 +++++++++ code/modules/power/lighting.dm | 25 +++--- colonialmarines.dme | 2 + icons/mob/humans/species/r_anomaly.dmi | Bin 0 -> 4028 bytes 13 files changed, 228 insertions(+), 15 deletions(-) create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/shadow/ai_brain_shadow.dm create mode 100644 code/modules/mob/living/carbon/human/species/anomaly/shadow/shadow_men.dm create mode 100644 icons/mob/humans/species/r_anomaly.dmi diff --git a/code/__DEFINES/human_ai.dm b/code/__DEFINES/human_ai.dm index 8ef1c651a3..c0637b4ff2 100644 --- a/code/__DEFINES/human_ai.dm +++ b/code/__DEFINES/human_ai.dm @@ -6,6 +6,7 @@ #define ACTION_USING_HANDS (1<<0) #define ACTION_USING_LEGS (1<<1) #define ACTION_USING_MOUTH (1<<2) +#define ACTION_UNIQUE (1<<4) /// Action is completed, delete this and move onto the next ongoing action #define ONGOING_ACTION_COMPLETED "completed" diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 3f78947d35..c88543a48f 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -30,6 +30,7 @@ // var/list/lights // list of all lights on this area var/list/all_doors = list() //Added by Strumpetplaya - Alarm Change - Contains a list of doors adjacent to this area var/air_doors_activated = 0 + var/list/all_lights = list() var/statistic_exempt = FALSE var/global/global_uid = 0 diff --git a/code/modules/lighting/lighting_static/static_lighting_corner.dm b/code/modules/lighting/lighting_static/static_lighting_corner.dm index 4f026e05e8..4d280829a1 100644 --- a/code/modules/lighting/lighting_static/static_lighting_corner.dm +++ b/code/modules/lighting/lighting_static/static_lighting_corner.dm @@ -86,7 +86,7 @@ light_source.recalc_corner(src) // God that was a mess, now to do the rest of the corner code! Hooray! -/datum/static_lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b) +/datum/static_lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b) // optimize me!! if(!(delta_r || delta_g || delta_b)) // 0 is falsey ok return diff --git a/code/modules/lighting/lighting_static/static_lighting_object.dm b/code/modules/lighting/lighting_static/static_lighting_object.dm index a3e6c5054b..8b1fc7e042 100644 --- a/code/modules/lighting/lighting_static/static_lighting_object.dm +++ b/code/modules/lighting/lighting_static/static_lighting_object.dm @@ -39,7 +39,7 @@ affected_turf = null return ..() -/datum/static_lighting_object/proc/update() +/datum/static_lighting_object/proc/update() // optimize me! // To the future coder who sees this and thinks // "Why didn't he just use a loop?" diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm index 67901c5ad7..868ef9424c 100644 --- a/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm +++ b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm @@ -2,9 +2,11 @@ GLOBAL_LIST_INIT_TYPED(AI_actions, /datum/ai_action, setup_ai_actions()) /proc/setup_ai_actions() var/list/action_list = list() - for(var/action in subtypesof(/datum/ai_action)) - var/datum/ai_action/A = new action - action_list[A.type] = A + for(var/datum/ai_action/action as anything in subtypesof(/datum/ai_action)) + //if(action.action_flags & ACTION_UNIQUE) // AI action is applied in unique_actions on human_ai_brain + // continue + var/datum/ai_action/new_action = new action + action_list[new_action.type] = new_action return action_list diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm index dae0030219..d0c72b41c1 100644 --- a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -23,6 +23,7 @@ GLOBAL_LIST_EMPTY(human_ai_brains) /// List of whitelisted/blacklisted action datums var/list/action_whitelist = null var/list/action_blacklist = null + var/list/unique_actions = list() /// List of current action datums var/list/ongoing_actions = list() diff --git a/code/modules/mob/living/carbon/human/species/anomaly/_species.dm b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm index 465df6a267..dd69b3f64d 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/_species.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm @@ -49,3 +49,82 @@ /datum/species/anomaly/handle_paygrades() return + +/datum/ai_action/anomaly + action_flags = ACTION_UNIQUE + +/datum/ai_action/anomaly/short_lighting + name = "Short Nearby Lighting" + var/ability_range = 9 + var/static/list/action_sound_effect_list = list( + 'sound/ambience/ambimalf.ogg', + 'sound/ambience/ambigen10.ogg', + 'sound/ambience/ambigen9.ogg', + 'sound/ambience/ambigen5.ogg', + 'sound/ambience/ambigen2.ogg', + 'sound/ambience/ambimo2.ogg', + 'sound/ambience/ambisin2.ogg' + ) + + var/static/list/sound_effect_list = list( + 'sound/machines/resource_node/node_marine_die.ogg', + 'sound/machines/resource_node/node_marine_die_2.ogg', + 'sound/machines/resource_node/node_marine_harvest.ogg', + 'sound/machines/resource_node/node_turn_off.ogg' + ) + + COOLDOWN_DECLARE(action_cooldown) + COOLDOWN_DECLARE(action_sound_effect_cooldown) + +/datum/ai_action/anomaly/short_lighting/Added() + . = ..() + COOLDOWN_START(src, action_cooldown, 5 SECONDS) + COOLDOWN_START(src, action_sound_effect_cooldown, 5 SECONDS) + +/datum/ai_action/anomaly/short_lighting/get_weight(datum/human_ai_brain/brain) + if(!is_type_in_list(src, brain.unique_actions)) + return 0 + return 20 + +/datum/ai_action/anomaly/short_lighting/trigger_action() + . = ..() + if(!COOLDOWN_FINISHED(src, action_cooldown)) + return + if(brain.in_combat) + COOLDOWN_START(src, action_cooldown, 2 SECONDS) + else + COOLDOWN_START(src, action_cooldown, 5 SECONDS) + var/mob/tied_mob = brain.tied_human + var/area/mob_area = get_area(tied_mob) + if(!mob_area) + return + var/list/nearby_lights = list() + var/list/areas_to_check = list(mob_area) + if(brain.current_target && get_area(brain.current_target) != mob_area) + areas_to_check |= get_area(brain.current_target) + for(var/area/lighting_area as anything in areas_to_check) + for(var/obj/structure/machinery/light/light as anything in lighting_area.all_lights) + if(!light.on || get_dist(light, tied_mob) > ability_range) + continue + nearby_lights |= light + // breaks more lights if used in combat, but always at least 1 + for(var/i in 1 to rand(1, (brain.in_combat * 4 + 1))) + if(!length(nearby_lights)) + break + var/obj/structure/machinery/light/target_light = pick(nearby_lights) + if(prob(60) && brain.in_combat) + target_light.flicker(rand(2, 10), 1, 3) + playsound(target_light, pick(sound_effect_list), 20) + addtimer(CALLBACK(target_light, TYPE_PROC_REF(/obj/structure/machinery/light, broken), FALSE, FALSE), rand(2, 4) SECONDS) + else + addtimer(CALLBACK(target_light, TYPE_PROC_REF(/obj/structure/machinery/light, flicker)), rand(1, 3) SECONDS) + nearby_lights -= target_light + if(brain.current_target && ishuman_strict(brain.current_target) && brain.in_combat) + var/mob/living/carbon/human/target = brain.current_target + if(COOLDOWN_FINISHED(src, action_sound_effect_cooldown)) + playsound_client(target.client, pick(action_sound_effect_list), target, 40, TRUE) + COOLDOWN_START(src, action_sound_effect_cooldown, 10 SECONDS) + if(prob(20)) // 20% chance to turn off their targets armor light every 2 seconds + var/obj/item/clothing/suit/storage/marine/target_armor = target.get_item_by_slot(WEAR_JACKET) + target_armor?.turn_light(target, FALSE) + diff --git a/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm b/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm index c20f282d11..cefa784fc3 100644 --- a/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm +++ b/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm @@ -1,6 +1,7 @@ GLOBAL_LIST_EMPTY(anomaly_ai_spawn_presets) #define ANOMALY_CLASS_KETER "Keter" +#define ANOMALY_CLASS_EUCLID "Euclid" // TO DO // @@ -116,3 +117,12 @@ GLOBAL_LIST_EMPTY(anomaly_ai_spawn_presets) icon_state = "duplicate" desc = "Dangerous entity that mimics players before attacking" anomaly_type_ref = "Duplicate" + +/datum/anomaly_ai_spawn_preset/euclid + anomaly_class = ANOMALY_CLASS_EUCLID + +/datum/anomaly_ai_spawn_preset/euclid/shadow_men + name = "Shadow Man" + icon_state = "shadow" + desc = "very dark" + anomaly_type_ref = "Shadow Man" diff --git a/code/modules/mob/living/carbon/human/species/anomaly/shadow/ai_brain_shadow.dm b/code/modules/mob/living/carbon/human/species/anomaly/shadow/ai_brain_shadow.dm new file mode 100644 index 0000000000..706c3eb492 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/shadow/ai_brain_shadow.dm @@ -0,0 +1,73 @@ +/datum/human_ai_brain/shadow_men + micro_action_delay = 0.2 SECONDS + short_action_delay = 0.5 SECONDS + medium_action_delay = 2 SECONDS + long_action_delay = 5 SECONDS + action_delay_mult = 2 // Doubled from 1, gives hAI a believable time between actions + unique_actions = list(/datum/ai_action/anomaly/short_lighting) + grenading_allowed = FALSE + requires_vision = TRUE + can_assign_squad = FALSE + has_nightvision = TRUE + +// ------------------- +// OPERATION OPTIMIZE! +// +// review flickering lights code +// reduce the calls from short lights ability +// reduce the lighting subsystem in general +// review static lighting modules as marked +// investigate lightweight light interactions that dont use the subsystem +// review the human_ai/fire subsystem proc +// review the shadow men processing proc +// reduce pathfinding usage +// apply optimized pathfinding code +// ------------------- + + + + +/datum/human_ai_brain/shadow_men/say_in_combat_line(chance) + return + +/datum/human_ai_brain/shadow_men/say_exit_combat_line() + return + +/datum/human_ai_brain/shadow_men/on_squad_member_death() + return + +/datum/human_ai_brain/shadow_men/say_grenade_thrown_line() + return + +/datum/human_ai_brain/shadow_men/say_reload_line() + return + +/datum/human_ai_brain/shadow_men/say_need_healing_line() + return + +//datum/human_ai_brain/shadow_men/enter_combat() // proc not in use while optimizations underway +// if(!ishuman(current_target)) +// return ..() +// var/mob/target_mob = current_target +// if(target_mob.client && !in_combat) +// playsound_client(target_mob.client, 'sound/ambience/weymart.ogg', target_mob, 70) +// ..() + +/datum/human_ai_brain/shadow_men/configure_custom_spawn(mob/living/carbon/human/target) + //RegisterSignal(tied_human, COMSIG_MOB_MOVE_OR_LOOK, PROC_REF(calculate_mob_slowdown)) + tied_human.add_filter("motion_blur", 1, list("type" = "motion_blur", "x" = 1)) + +/// proc not in use while optimizations underway +/datum/human_ai_brain/shadow_men/proc/calculate_mob_slowdown() + if(tied_human.stat) + UnregisterSignal(tied_human, COMSIG_MOB_MOVE_OR_LOOK) + return + var/turf/mob_turf = get_turf(tied_human) + var/slowdown_factor = (mob_turf.luminosity * 2) + mob_turf.dynamic_lumcount + if(!slowdown_factor) + return FALSE + if(!tied_human.stamina) + return + var/shadow_slowdown = listgetindex(tied_human.stamina.stamina_levels, slowdown_factor) + tied_human.stamina.activate_stamina_debuff(shadow_slowdown) + diff --git a/code/modules/mob/living/carbon/human/species/anomaly/shadow/shadow_men.dm b/code/modules/mob/living/carbon/human/species/anomaly/shadow/shadow_men.dm new file mode 100644 index 0000000000..4c83358b18 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/shadow/shadow_men.dm @@ -0,0 +1,39 @@ +/datum/species/anomaly/shadow_men + ///Used for isx(y) checking of species groups + group = SPECIES_ANOMALY + name = "Shadow Man" + name_plural = "Shadow Men" + icobase = 'icons/mob/humans/species/r_anomaly.dmi' // Normal icon set. + deform = 'icons/mob/humans/species/r_anomaly.dmi' // Mutated icon set. + unarmed_type = /datum/unarmed_attack/claws + secondary_unarmed_type = /datum/unarmed_attack/bite + pain_type = /datum/pain/anomaly + stamina_type = /datum/stamina + flags = NO_CLONE_LOSS|NO_POISON|NO_NEURO|NO_SLIP|NO_BLOOD|NO_BREATHE|NO_SHRAPNEL + default_ai_brain_type = /datum/human_ai_brain/shadow_men + gibbed_anim = "gibbed-h" + dusted_anim = "dust-h" + flesh_color = null + blood_color = null + blood_mask = null + base_color = COLOR_BLACK + hair_color = COLOR_BLACK + death_sound = 'sound/voice/4_xeno_roars.ogg' + death_message = "falls still, one last inhuman screech escaping their stolen lungs..." + has_fine_manipulation = FALSE + insulated = TRUE + brute_mod = 0.5 + burn_mod = 2 + mob_inherent_traits = list(TRAIT_FOREIGN_BIO, TRAIT_UNSTRIPPABLE, TRAIT_FORCED_STANDING, TRAIT_HARDCORE) + knock_down_reduction = -1 + stun_reduction = -1 + knock_out_reduction = -1 + weed_slowdown_mult = 0 + acid_blood_dodge_chance = 100 + + has_organ = list( + "heart" = /datum/internal_organ/heart, + "eyes" = /datum/internal_organ/eyes + ) + + diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index ad6e0f3b35..2d9abf94e8 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -272,6 +272,9 @@ if(prob(5)) broken(1) + var/area/area = get_area(src) + area?.all_lights |= src + active_power_usage = (brightness * 10) addtimer(CALLBACK(src, PROC_REF(update), 0), 1) @@ -299,9 +302,11 @@ pixel_x = -10 /obj/structure/machinery/light/Destroy() - var/area/A = get_area(src) - if(A) + var/area/area = get_area(src) + if(area) on = 0 + if(locate(src) in area?.all_lights) + area.all_lights -= src // A.update_lights() . = ..() @@ -473,7 +478,7 @@ return A.lightswitch return A.lightswitch && A.power_light -/obj/structure/machinery/light/proc/flicker(amount = rand(10, 20)) +/obj/structure/machinery/light/proc/flicker(amount = rand(10, 20), min_delay = 5, max_delay = 15) if(flickering) return flickering = 1 spawn(0) @@ -482,7 +487,7 @@ if(status != LIGHT_OK) break on = !on update(0) - sleep(rand(5, 15)) + sleep(rand(min_delay, max_delay)) on = (status == LIGHT_OK) update(0) flickering = 0 @@ -569,17 +574,17 @@ // break the light and make sparks if was on -/obj/structure/machinery/light/proc/broken(skip_sound_and_sparks = 0) +/obj/structure/machinery/light/proc/broken(skip_sound = FALSE, skip_sparks = TRUE) if(status == LIGHT_EMPTY) return - if(!skip_sound_and_sparks) + if(!skip_sound) if(status == LIGHT_OK || status == LIGHT_BURNED) playsound(src.loc, 'sound/effects/Glasshit.ogg', 25, 1) -// if(on) -// var/datum/effect_system/spark_spread/s = new /datum/effect_system/spark_spread -// s.set_up(3, 1, src) -// s.start() + if(!skip_sparks && on) + var/datum/effect_system/spark_spread/spark = new /datum/effect_system/spark_spread + spark.set_up(3, 1, src) + spark.start() status = LIGHT_BROKEN update() diff --git a/colonialmarines.dme b/colonialmarines.dme index ada702a3d4..a23ebb1ee4 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -2146,6 +2146,8 @@ #include "code\modules\mob\living\carbon\human\species\anomaly\anomaly_management_menu.dm" #include "code\modules\mob\living\carbon\human\species\anomaly\duplicate\ai_brain_duplicate.dm" #include "code\modules\mob\living\carbon\human\species\anomaly\duplicate\duplicate.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\shadow\ai_brain_shadow.dm" +#include "code\modules\mob\living\carbon\human\species\anomaly\shadow\shadow_men.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\_emote.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\_species.dm" #include "code\modules\mob\living\carbon\human\species\working_joe\damage.dm" diff --git a/icons/mob/humans/species/r_anomaly.dmi b/icons/mob/humans/species/r_anomaly.dmi new file mode 100644 index 0000000000000000000000000000000000000000..dd7e49b115791c6bd8194e1a310816e8b2ea4e09 GIT binary patch literal 4028 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