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/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/__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/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/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/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 70d74e1853..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() @@ -362,3 +363,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/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/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..dd69b3f64d --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/_species.dm @@ -0,0 +1,130 @@ +/* + 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/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 + +/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 + +/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 new file mode 100644 index 0000000000..cefa784fc3 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/anomaly_management_menu.dm @@ -0,0 +1,128 @@ +GLOBAL_LIST_EMPTY(anomaly_ai_spawn_presets) + +#define ANOMALY_CLASS_KETER "Keter" +#define ANOMALY_CLASS_EUCLID "Euclid" + +// 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" + +/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/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..61ef6777f1 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/ai_brain_duplicate.dm @@ -0,0 +1,217 @@ +// TO DO LIST +// + +/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 + 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", + "*roar", + ) + + 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) + 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) + 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 + 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)) + pretending_to_be_human = FALSE + 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) + +/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() + 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/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 + COOLDOWN_START(src, replicate_speech, 1 SECONDS) + + 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.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 + + //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() + + 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 new file mode 100644 index 0000000000..8503c10a49 --- /dev/null +++ b/code/modules/mob/living/carbon/human/species/anomaly/duplicate/duplicate.dm @@ -0,0 +1,33 @@ +/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/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 + 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..." + 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/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 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/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/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/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/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) diff --git a/colonialmarines.dme b/colonialmarines.dme index e2b70b1e95..a23ebb1ee4 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,12 @@ #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\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/misc/anomaly_menu.dmi b/icons/misc/anomaly_menu.dmi new file mode 100644 index 0000000000..aee42db11b Binary files /dev/null and b/icons/misc/anomaly_menu.dmi differ diff --git a/icons/mob/humans/species/r_anomaly.dmi b/icons/mob/humans/species/r_anomaly.dmi new file mode 100644 index 0000000000..dd7e49b115 Binary files /dev/null and b/icons/mob/humans/species/r_anomaly.dmi differ diff --git a/tgui/packages/tgui/styles/interfaces/AnomalyAISpawner.scss b/tgui/packages/tgui/styles/interfaces/AnomalyAISpawner.scss new file mode 100644 index 0000000000..908f6f64a2 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/AnomalyAISpawner.scss @@ -0,0 +1,118 @@ +.AnomalySpawner__Background { + width: 100%; + height: 100%; + position: absolute; + background-color: #0a0a0a; +} + +.AnomalySpawner__Casing { + height: 100%; + border-radius: 2px; + border: 0px outset rgba(0, 0, 0, 0.685); + background-color: #0a0a0a; + padding: 8px; + max-width: 700px; + max-height: 410px; +} + +.AnomalySpawner__Gradient { + background: radial-gradient(#042208, #000b02); +} + +.AnomalySpawner__Static { + width: 100%; + height: 100%; + outline: 3px inset #111111; + outline-offset: 2px; + border-radius: 5px; + border: 2px ridge #00e94e; + background: linear-gradient(hsla(0, 6%, 7%, 0) 50%, hsla(0, 0%, 0%, 0.25) 50%), + linear-gradient( + 90deg, + hsla(0, 100%, 50%, 0.06), + hsla(120, 100%, 50%, 0.02), + hsla(240, 100%, 50%, 0.06) + ); + background-size: + 100% 2px, + 3px 100%; + .AnomalySpawner__Screen { + mix-blend-mode: color-dodge; + background: radial-gradient(#05a11ab7, #00250786); + width: 100%; + height: 100%; + padding-left: 1%; + padding-right: 2%; + color: #00e94e; + font-family: monospace; + } +} + +.AnomalySpawner__TextConfirm { + color: #000b02; + background-color: #00e94e; +} + +.AnomalySpawner__FuelButton { + border: 1px solid #00e94e; + color: #00e94e; + background-color: transparent; +} + +.AnomalySpawner__Readout { + filter: brightness(1.5); +} + +.AnomalySpawner__MainDivider { + border-right: 2px solid #00e94ed3; + margin-right: 6px; +} + +.AnomalySpawner__HorizontalDivider { + width: 100%; + border-bottom: 2px solid #00e94ee3; +} + +.AnomalySpawner__Fuel { + .AnomalySpawner__Divider { + border-right: 2px solid #00e94ed3; + padding-right: 6px; + margin-right: 6px; + } + .AnomalySpawner__Subdivider { + border-right: 2px solid #00e94ed3; + padding-right: 6px; + margin-right: 6px; + } + .AnomalySpawner__Fuelnotch { + width: 30px; + margin-bottom: 14px; + } + .AnomalySpawner__Energynotch { + width: 40px; + margin-bottom: 17px; + } +} + +.AnomalySpawner__Main { + width: 100%; + height: 100%; + filter: brightness(2); + .AnomalyClass { + background-color: #02b83e42; + border-left: 1.5px #00e94ed3 solid; + padding-bottom: 1px; + font-weight: bold; + border-radius: 0px; + } + .AnomalyType { + background-color: transparent; + margin-left: 4%; + width: 92%; + margin-right: 4%; + border-radius: 0px; + border-bottom: 1px #00e94ed3 solid; + border-top: 1px #00e94ed3 solid; + text-align: center; + } +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 7c1c65a324..6afddd977d 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -83,6 +83,7 @@ @include meta.load-css('./interfaces/common/ElectricalPanel.scss'); @include meta.load-css('./interfaces/TacticalMap.scss'); @include meta.load-css('./interfaces/StripMenu.scss'); +@include meta.load-css('./interfaces/AnomalyAISpawner.scss'); // Layouts @include meta.load-css('./layouts/Layout.scss');