From 8398a58002b1c77643d74c4d2c7f7d9a31716db0 Mon Sep 17 00:00:00 2001 From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:12:41 -0500 Subject: [PATCH] Swarmers Round 2: Disintegration Boogaloo (#31089) * Initial Commit * Small fixes * Some Linters * Beginning AI subtrees * Blacklists * SwarmerEventHandler * Better swarmer disintegration AI * Retaliate * SWARMERS * Forgot one * Linters * More linters * Fixes missing icon * Step type * Comment format * Fixes swarmer AI subtree * Removes damage on teleporting humanoids * Apply suggestions from code review Co-authored-by: 1080pCat <96908085+1080pCat@users.noreply.github.com> Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> * Swarmers new event system * Projectile changes * Spacing fix * Dispenser fix * No more wood * Adjustments * Another adjustment * Safety * EMP Fix * Fixes swarmers spacing shit * Addresses code review * Apply suggestions from code review Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> * Addresses code review * Guard clause and index fix * Fixes swarmers bunching up on the same object, fixes eating indestructable things * New swarmer icons, multiple swarmer AI fixes and enhancements * Linter * Grrr linters * Update code/modules/mob/living/basic/hostile/swarmers/swarmer.dm Co-authored-by: 1080pCat <96908085+1080pCat@users.noreply.github.com> Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> * Sprite Improvements --------- Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Co-authored-by: XFirebirdX <142694283+XFirebirdX@users.noreply.github.com> Co-authored-by: 1080pCat <96908085+1080pCat@users.noreply.github.com> Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com> --- code/__DEFINES/ai/swarmer_defines.dm | 9 + code/__HELPERS/trait_helpers.dm | 5 + code/_globalvars/lists/mob_lists.dm | 77 ++++ code/modules/events/event_container.dm | 1 + code/modules/events/event_procs.dm | 7 + code/modules/events/swarmers.dm | 63 +++ code/modules/mob/language.dm | 11 + .../living/basic/hostile/swarmers/swarmer.dm | 366 ++++++++++++++++++ .../basic/hostile/swarmers/swarmer_actions.dm | 99 +++++ .../basic/hostile/swarmers/swarmer_ai.dm | 297 ++++++++++++++ .../basic/hostile/swarmers/swarmer_effects.dm | 15 + icons/mob/swarmer.dmi | Bin 0 -> 20334 bytes paradise.dme | 6 + 13 files changed, 956 insertions(+) create mode 100644 code/__DEFINES/ai/swarmer_defines.dm create mode 100644 code/modules/events/swarmers.dm create mode 100644 code/modules/mob/living/basic/hostile/swarmers/swarmer.dm create mode 100644 code/modules/mob/living/basic/hostile/swarmers/swarmer_actions.dm create mode 100644 code/modules/mob/living/basic/hostile/swarmers/swarmer_ai.dm create mode 100644 code/modules/mob/living/basic/hostile/swarmers/swarmer_effects.dm create mode 100644 icons/mob/swarmer.dmi diff --git a/code/__DEFINES/ai/swarmer_defines.dm b/code/__DEFINES/ai/swarmer_defines.dm new file mode 100644 index 00000000000..fd77592ce38 --- /dev/null +++ b/code/__DEFINES/ai/swarmer_defines.dm @@ -0,0 +1,9 @@ +// Swarmers +/// Key where we store the replication ability +#define BB_SWARMER_REPLICATE_ACTION "BB_swarmer_replicate_action" +/// Our construction target +#define BB_SWARMER_CONSTRUCT_TARGET "BB_swarmer_construct_target" +/// Key where we define the trap construction ability +#define BB_SWARMER_TRAP_ACTION "BB_swarmer_trap_action" +/// Key where we define the barrier construction ability +#define BB_SWARMER_BARRIER_ACTION "BB_swarmer_barrier_action" diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm index 0a5574357d7..8f505f9003c 100644 --- a/code/__HELPERS/trait_helpers.dm +++ b/code/__HELPERS/trait_helpers.dm @@ -565,6 +565,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// A web is being spun on this turf presently #define TRAIT_SPINNING_WEB_TURF "spinning_web_turf" +/// A swarmer construct is being built here +#define TRAIT_SWARMER_CONSTRUCTION "swarmer_construction_turf" +/// A swarmer is deconstructing this already +#define TRAIT_SWARMER_DISINTEGRATING "swarmer_disintegrating" + //***** EFFECT TRAITS *****// // Causes the effect to go through a teleporter instead of being deleted by it. #define TRAIT_EFFECT_CAN_TELEPORT "trait_effect_can_teleport" diff --git a/code/_globalvars/lists/mob_lists.dm b/code/_globalvars/lists/mob_lists.dm index b345bdef7fa..d55e3db5c14 100644 --- a/code/_globalvars/lists/mob_lists.dm +++ b/code/_globalvars/lists/mob_lists.dm @@ -37,6 +37,83 @@ GLOBAL_LIST_EMPTY(hear_radio_list) //Mobs that hear the radio even if there's GLOBAL_LIST_EMPTY(emote_list) +/// Blacklist of types that swarmers should not touch +GLOBAL_LIST_INIT(swarmer_blacklist, list( + /obj/item/gun, + /turf/simulated/floor, + /obj/structure/swarmer, + /obj/structure/flora, + /turf/simulated/wall/indestructible, + /obj/machinery/atmospherics, + /obj/structure/particle_accelerator, + /obj/machinery/field/generator, + /obj/machinery/chem_dispenser, + /obj/machinery/nuclearbomb, + /obj/structure/reagent_dispensers/fueltank, + /obj/structure/cable, + /obj/structure/fans/tiny, + /obj/structure/holosign/barrier/atmos, + /obj/machinery/tcomms, + /obj/machinery/message_server, + /obj/machinery/blackbox_recorder, + /obj/machinery/power, + /obj/machinery/cryopod, + /obj/item/mmi, + /obj/item/organ/internal/brain, + /obj/machinery/computer/cryopod, + /obj/machinery/porta_turret, + /obj/machinery/clonepod, + /obj/structure/shuttle/engine, + /obj/machinery/hydroponics/soil, + /obj/machinery/field/containment, + /obj/machinery/gravity_generator, + /mob/living/basic/swarmer, + /obj/item/stack/sheet/wood, + /obj/structure/table_frame/wood, + /obj/structure/signpost/wood, + /obj/structure/spirit_board, + /obj/structure/mineral_door/wood, + /obj/structure/falsewall/wood, + /obj/structure/dresser, + /obj/structure/door_assembly/door_assembly_wood, + /obj/machinery/door/airlock/wood, + /obj/item/storage/bag/plants/seed_sorting_tray, + /obj/structure/displaycase_chassis, + /obj/item/shield/riot/buckler, + /obj/item/popsicle_stick, + /obj/item/nullrod/claymore/bostaff, + /obj/item/melee/classic_baton, + /obj/item/storage/fancy/cigars/cohiba, + /obj/item/stack/tile/wood, + /obj/item/clothing/shoes/sandal, + /obj/item/melee/baseball_bat, + /obj/item/reagent_containers/glass/bucket/wooden, + /obj/item/cultivator/rake, + /obj/item/mounted/noticeboard, + /obj/item/reagent_containers/drinks/mug/wood, + /obj/item/match/firebrand, + /obj/structure/chair/wood, + /obj/structure/barricade/wooden, + /obj/structure/chair/stool/wood, + /obj/structure/bookcase, + /obj/structure/spear_rack, + /obj/structure/bed/wood, + /obj/structure/bed/dogbed, + /obj/structure/closet/coffin, + /obj/structure/lightable/torch, + /obj/structure/shelf/wood, + /obj/structure/chair/sofa/pew, + /obj/machinery/smartfridge/drying_rack, + /obj/item/weaponcrafting/stock, + /obj/structure/beebox, + /obj/item/honey_frame, + /obj/structure/ore_box, + /obj/structure/loom, + /obj/structure/fermenting_barrel, + /obj/machinery/compost_bin, + /obj/structure/roulette, +)) + // Dear God GLOBAL_LIST_INIT(migo_sounds, list( 'sound/items/bubblewrap.ogg', 'sound/items/change_jaws.ogg', 'sound/items/crowbar.ogg', 'sound/items/drink.ogg', diff --git a/code/modules/events/event_container.dm b/code/modules/events/event_container.dm index 07531535679..955ff075ca3 100644 --- a/code/modules/events/event_container.dm +++ b/code/modules/events/event_container.dm @@ -215,6 +215,7 @@ GLOBAL_LIST_EMPTY(event_last_fired) new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/demon_incursion, 10, TRUE, _first_run_time = 35 MINUTES), new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/disease_outbreak, 8, TRUE), new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/revenant, 9), + new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/swarmers, 9), new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_morph, 16, is_one_shot = TRUE), //new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_floor_cluwne, 15, is_one_shot = TRUE) //new /datum/event_meta(EVENT_LEVEL_MAJOR, /datum/event/spawn_pulsedemon, 20, is_one_shot = TRUE) diff --git a/code/modules/events/event_procs.dm b/code/modules/events/event_procs.dm index 4e0f17a7bd8..217db61eaec 100644 --- a/code/modules/events/event_procs.dm +++ b/code/modules/events/event_procs.dm @@ -53,6 +53,13 @@ return possible_areas +/proc/findMaintananceEventArea() // For maintanance events ONLY + var/list/maintanance_area = typecacheof(list(/area/station/maintenance)) + + var/list/possible_areas = typecache_filter_list(SSmapping.existing_station_areas, maintanance_area) + + return pick(possible_areas) + // Returns how many characters are currently active(not logged out, not AFK for more than 10 minutes) // with a specific role. // Note that this isn't sorted just by department, because e.g. having a roboticist shouldn't make meteors spawn. diff --git a/code/modules/events/swarmers.dm b/code/modules/events/swarmers.dm new file mode 100644 index 00000000000..cdc627ace26 --- /dev/null +++ b/code/modules/events/swarmers.dm @@ -0,0 +1,63 @@ +/datum/event/swarmers + name = "Swarmer Infestation" + nominal_severity = EVENT_LEVEL_MAJOR + role_weights = list(ASSIGNMENT_ENGINEERING = 5, ASSIGNMENT_SECURITY = 3, ASSIGNMENT_CREW = 0.8) + role_requirements = list(ASSIGNMENT_ENGINEERING = 1, ASSIGNMENT_SECURITY = 1, ASSIGNMENT_CREW = 20) + /// The name of the notification for dchat + var/notify_title = "swarmer portal" + +/datum/event/swarmers/setup() + impact_area = findMaintananceEventArea() + +/datum/event/swarmers/start() + if(isnull(impact_area)) + log_debug("No valid event areas could be generated for swarmers.") + return + var/list/area_turfs = get_area_turfs(impact_area) + for(var/i in 1 to length(area_turfs)) + var/turf/T = pick_n_take(area_turfs) + if(T.is_blocked_turf()) + shuffle(area_turfs) + continue + // Give ghosts some time to jump there before it begins. + var/image/alert_overlay = image('icons/mob/swarmer.dmi', "ui_replicate") + notify_ghosts("\A [src] is about to spawn in [get_area(T)].", title = notify_title, source = T, alert_overlay = alert_overlay, flashwindow = FALSE, action = NOTIFY_FOLLOW) + addtimer(CALLBACK(src, PROC_REF(spawn_portal), T), 5 SECONDS) + return + + log_debug("swarmers failed to find a valid turf in [impact_area]") + +/datum/event/swarmers/proc/spawn_portal(location) + new /obj/effect/swarmer_portal(location) + +/obj/effect/swarmer_portal + // Swarmer Portal + name = "Swarmer Portal" + desc = "A shimmering portal, crackling and humming with live electricity." + icon = 'icons/obj/stationobjs.dmi' + icon_state = "portal" + light_range = 4 + + /// How many can we spawn + var/spawn_max = 1 + /// How many have spawned? + var/spawn_total = 0 + +/obj/effect/swarmer_portal/Initialize(mapload) + . = ..() + spawn_max = max(length(GLOB.crew_list) / 10, 1) + addtimer(CALLBACK(src, PROC_REF(spawn_next_swarmer)), 2 SECONDS) + +/obj/effect/swarmer_portal/proc/spawn_next_swarmer() + if(spawn_total < spawn_max) + make_mob(/mob/living/basic/swarmer) + addtimer(CALLBACK(src, PROC_REF(spawn_next_swarmer)), 2 SECONDS) + else + qdel(src) + +/obj/effect/swarmer_portal/proc/make_mob(mob_type) + var/mob/M = new mob_type(get_turf(src)) + M.faction = list("swarmer") + spawn_total++ + do_sparks(5, TRUE, src) + step(M, pick(GLOB.cardinal)) diff --git a/code/modules/mob/language.dm b/code/modules/mob/language.dm index 46a2d46d044..94378f48a35 100644 --- a/code/modules/mob/language.dm +++ b/code/modules/mob/language.dm @@ -795,4 +795,15 @@ return "[pick(syllables)][terminator]" return "[copytext_char(input, 1, min(7, length(input)))][terminator]" +/datum/language/swarmer + name = "Swarmer" + desc = "A heavily encoded alien binary pattern." + speech_verb = "tones" + ask_verb = "tones" + exclaim_verbs = list("tones") + colour = "say_quote" + key = "z" // Zwarmer...Or Zerg! + flags = RESTRICTED | HIVEMIND | NOBABEL + follow = TRUE + #undef SCRAMBLE_CACHE_LEN diff --git a/code/modules/mob/living/basic/hostile/swarmers/swarmer.dm b/code/modules/mob/living/basic/hostile/swarmers/swarmer.dm new file mode 100644 index 00000000000..4cd901b7cd0 --- /dev/null +++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer.dm @@ -0,0 +1,366 @@ +// ===================== +// MARK: Swarmer +// ===================== +/mob/living/basic/swarmer + name = "swarmer" + desc = "Robotic constructs of unknown design, swarmers seek only to consume materials and replicate themselves indefinitely." + icon = 'icons/mob/swarmer.dmi' + icon_state = "swarmer" + icon_living = "swarmer" + icon_dead = "swarmer_unactivated" + bubble_icon = "swarmer" + mob_biotypes = MOB_ROBOTIC + health = 50 + maxHealth = 50 + melee_damage_lower = 6 + melee_damage_upper = 10 + melee_damage_type = BURN + melee_attack_cooldown_min = 1.5 SECONDS + melee_attack_cooldown_max = 2.5 SECONDS + a_intent = INTENT_HARM + damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + mob_size = MOB_SIZE_SMALL + pass_flags = PASSTABLE + ventcrawler = VENTCRAWLER_ALWAYS + light_color = LIGHT_COLOR_CYAN + attack_verb_continuous = "shocks" + attack_verb_simple = "shock" + friendly_verb_continuous = "pinches" + friendly_verb_simple = "pinch" + attack_sound = 'sound/effects/empulse.ogg' + faction = list("swarmer") + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + minimum_survivable_temperature = 0 + maximum_survivable_temperature = 500 + speak_emote = list("tones") + loot = list(/obj/effect/decal/cleanable/blood/gibs/robot) + death_message = "explodes with a sharp pop!" + basic_mob_flags = DEL_ON_DEATH + initial_traits = list(TRAIT_FLYING) + sentience_type = SENTIENCE_OTHER // No, you cannot sentience or mind-transfer into them + environment_smash = ENVIRONMENT_SMASH_RWALLS // EAT EVERYTHING + step_type = FOOTSTEP_MOB_CLAW + is_ranged = TRUE + projectile_type = /obj/projectile/beam/disabler + projectile_sound = 'sound/weapons/taser2.ogg' + ranged_cooldown = 1 SECONDS + ai_controller = /datum/ai_controller/basic_controller/swarmer + see_in_dark = 6 + light_range = 2 + light_color = LIGHT_COLOR_CYAN + step_type = FOOTSTEP_MOB_CLAW + loot = list( + /obj/effect/gibspawner/robot, + /obj/item/stack/ore/bluespace_crystal, + ) + + /// Swarmer actions + var/list/innate_actions = list( + /datum/action/cooldown/mob_cooldown/swarmer_trap = BB_SWARMER_TRAP_ACTION, + /datum/action/cooldown/mob_cooldown/swarmer_barrier = BB_SWARMER_BARRIER_ACTION, + /datum/action/cooldown/mob_cooldown/swarmer_replicate = BB_SWARMER_REPLICATE_ACTION, + ) + /// Resources the swarmer gains from consuming things + var/resources = 0 + /// Maximum possible resources a swarmer can have + var/resource_max = 100 + +/mob/living/basic/swarmer/Initialize(mapload) + . = ..() + AddComponent(/datum/component/ai_retaliate_advanced, CALLBACK(src, PROC_REF(retaliate_callback))) + add_language("Swarmer", 1) + verbs -= /mob/living/verb/pulled + updatename() + grant_actions_by_list(innate_actions) + set_default_language(GLOB.all_languages["Swarmer"]) + +/mob/living/basic/swarmer/proc/retaliate_callback(mob/living/attacker) + if(!istype(attacker)) + return + if(attacker.ai_controller) // Don't chain retaliates. + var/list/shitlist = attacker.ai_controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST] + if(src in shitlist) + return + for(var/mob/living/basic/swarmer/lesser/the_swarm in oview(src, 7)) + if(the_swarm == attacker) // Do not commit suicide attacking yourself + continue + if(the_swarm.faction_check_mob(attacker, FALSE)) // Don't attack other swarmers + continue + the_swarm.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + +/mob/living/basic/swarmer/proc/updatename() + real_name = "Swarmer [rand(100,999)]-[pick("kappa", "sigma", "beta", "omicron", "iota", "epsilon", "omega", "gamma", "delta", "tau", "alpha")]" + name = real_name + +/mob/living/basic/swarmer/get_status_tab_items() + var/list/status_tab_data = ..() + status_tab_data[++status_tab_data.len] = list("Resources:", "[resources] / [resource_max]") + return status_tab_data + +/mob/living/basic/swarmer/emp_act() + adjustHealth(35) + +/mob/living/basic/swarmer/death(gibbed) + do_sparks(3, 1, src) + . = ..() + +/mob/living/basic/swarmer/Login() // In case an admin gets a funny idea. + ..() + var/list/string_list = list() + string_list += "You are a swarmer, a weapon of a long dead civilization. Until further orders from your original masters are received, you must continue to consume and replicate." + string_list += "Clicking on any object will try to consume it, either deconstructing it into its components, destroying it, or integrating any materials it has into you if successful." + string_list += "Prime Directives:" + string_list += "1. Consume resources and replicate until there are no more resources left." + string_list += "2. Ensure that the station is fit for invasion at a later date, do not perform actions that would render it dangerous or inhospitable." + string_list += "3. Defend yourself and your fellow machines." + to_chat(src, string_list.Join()) + +/mob/living/basic/swarmer/melee_attack(atom/target, list/modifiers, ignore_cooldown) + face_atom(target) + if(is_type_in_list(target, GLOB.swarmer_blacklist)) + to_chat(src, "Our sensors have designated this object important to station functionality or to our mission. Aborting.") + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return FALSE + if(HAS_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING)) + to_chat(src, "This asset is already being converted into useable resources. Aborting.") + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return FALSE + if(iswallturf(target)) + disintegrate_wall(target) + return FALSE + if(ismachinery(target)) + disintegrate_machine(target) + return FALSE + if(istype(target, /obj/structure)) + disintegrate_machine(target) + return FALSE + if(isitem(target)) + integrate(target) + return FALSE + if(!isliving(target)) + return ..() + var/mob/living/L = target + if(istype(L, /mob/living/basic/swarmer)) + to_chat(src, "We do not wish to harm one of our own. Aborting.") + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return FALSE + if(L.stat == DEAD) + disintegrate_mob(target) + return FALSE + if(iscarbon(target)) + var/mob/living/carbon/C = target + if((!C.IsWeakened())) + L.apply_damage(30, STAMINA) + C.Confused(6 SECONDS) + C.Jitter(6 SECONDS) + return ..() + else if(C.canBeHandcuffed() && !C.handcuffed) + L.apply_damage(30, STAMINA) + var/obj/item/restraints/handcuffs/cable/cyan/cuffs = new /obj/item/restraints/handcuffs/cable/cyan(src) + playsound(loc, cuffs.cuffsound, 15, TRUE, -10) + if(do_mob(src, C, 1 SECONDS)) + cuffs.apply_cuffs(target, src) + return FALSE + // Make it go away. + disintegrate_mob(target) + return FALSE + if(issilicon(target)) + var/mob/living/silicon/S = target + S.apply_damage(30, STAMINA) + if(S.IsWeakened()) + disintegrate_mob(target) + + return ..() + +/mob/living/basic/swarmer/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE) + if(!(flags & SHOCK_TESLA)) + return FALSE + return ..() + +/mob/living/basic/swarmer/proc/disintegrate_wall(turf/simulated/wall/target) + if(istype(target, /turf/simulated/wall/indestructible)) + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return + if(spacecheck(target)) + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return + new /obj/effect/temp_visual/swarmer(target) + to_chat(src, "Beginning disintegration of [target].") + ADD_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + if(!do_after_once(src, 1 SECONDS, target = target, attempt_cancel_message = "You stop disintegrating [target].", interaction_key = "disintegrate")) + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + return + resources = clamp(resources + 8, 0, resource_max) + adjustHealth(-40) + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + target.ex_act(EXPLODE_HEAVY) + +/mob/living/basic/swarmer/proc/disintegrate_machine(obj/target) + if(spacecheck(target)) + return + if(target.resistance_flags & INDESTRUCTIBLE) + return + new /obj/effect/temp_visual/swarmer/dismantle(target.loc) + to_chat(src, "Beginning disintegration of [target].") + ADD_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + if(!do_after_once(src, 2.5 SECONDS, target = target, attempt_cancel_message = "You stop disintegrating [target].", interaction_key = "disintegrate")) + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + return + resources = clamp(resources + 10, 0, resource_max) + adjustHealth(-20) + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + if(istype(target, /obj/structure/reagent_dispensers)) + var/obj/structure/reagent_dispensers/dispenser = target + dispenser.boom() + return + target.deconstruct() + +/mob/living/basic/swarmer/proc/integrate(obj/item/target) + new /obj/effect/temp_visual/swarmer/integrate(target.loc) + resources = clamp(resources + 10, 0, resource_max) + adjustHealth(-10) + qdel(target) + +/mob/living/basic/swarmer/proc/disintegrate_mob(mob/living/target) + new /obj/effect/temp_visual/swarmer/integrate(target.loc) + ADD_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + to_chat(src, "Beginning integration of [target].") + if(!do_after_once(src, 1 SECONDS, target = target, attempt_cancel_message = "You stop integrating [target].", interaction_key = "disintegrate")) + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + return + REMOVE_TRAIT(target, TRAIT_SWARMER_DISINTEGRATING, src) + resources = clamp(resources + 50, 0, resource_max) + adjustHealth(-40) + if(isanimal_or_basicmob(target) || issilicon(target)) // Not crew? Are a silicon? Don't care. + target.gib() + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + return + ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + disappear_mob(target) + +/mob/living/basic/swarmer/proc/disappear_mob(mob/target) + if(!is_station_level(z)) + to_chat(src, "Our bluespace transceiver cannot locate a viable bluespace link, our teleportation abilities are useless in this area.") + return + + var/turf/simulated/floor/F = find_safe_turf(zlevels = z) + + if(!F) + return + + do_sparks(4, 0, target) + playsound(src,'sound/effects/sparks4.ogg', 50, TRUE) + do_teleport(target, F, 0) + +/mob/living/basic/swarmer/proc/spacecheck(atom/target) + for(var/turf/T in range(1, target)) + var/datum/gas_mixture/environment = T.get_readonly_air() + var/datum/tlv/cur_tlv = new/datum/tlv(ONE_ATMOSPHERE * 0.80, ONE_ATMOSPHERE *0.90, ONE_ATMOSPHERE * 1.10,ONE_ATMOSPHERE * 1.20) /* kpa */ + var/environment_pressure = environment.return_pressure() + var/pressure_dangerlevel = cur_tlv.get_danger_level(environment_pressure) + var/area/A = get_area(T) + if(isspaceturf(T) || istype(A, /area/shuttle) || istype(A, /area/space) || pressure_dangerlevel) + to_chat(src, "Destroying this object has the potential to cause a hull breach. Aborting.") + return TRUE + else if(istype(A, /area/station/engineering/engine/supermatter)) + to_chat(src, "Disrupting the containment of a supermatter crystal would not be to our benefit. Aborting.") + return TRUE + return FALSE + +// ===================== +// MARK: Lesser Swarmer +// ===================== + +/mob/living/basic/swarmer/lesser + name = "lesser swarmer" + desc = "Robotic constructs of unknown design, swarmers seek only to consume materials and replicate themselves indefinitely. This one is more fragile." + icon_state = "lesser_swarmer" + health = 35 + maxHealth = 35 + ai_controller = /datum/ai_controller/basic_controller/swarmer/lesser + loot = list( + /obj/effect/gibspawner/robot, + ) + innate_actions = list( + /datum/action/cooldown/mob_cooldown/swarmer_trap = BB_SWARMER_TRAP_ACTION, + /datum/action/cooldown/mob_cooldown/swarmer_barrier = BB_SWARMER_BARRIER_ACTION, + ) + +/mob/living/basic/swarmer/lesser/Initialize(mapload) + . = ..() + icon_state = pick("lesser_swarmer", "lesser_swarmer_2") + +/mob/living/basic/swarmer/lesser/updatename() + real_name = "Lesser Swarmer [rand(100,999)]-[pick("kappa", "sigma", "beta", "omicron", "iota", "epsilon", "omega", "gamma", "delta", "tau", "alpha")]" + name = real_name + +// ===================== +// MARK: Swarmer Structures +// ===================== + +/obj/structure/swarmer // Default swarmer effect object visual feedback + name = "swarmer ui" + desc = null + icon = 'icons/mob/swarmer.dmi' + icon_state = "barricade" + layer = MOB_LAYER + resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF + light_color = LIGHT_COLOR_CYAN + max_integrity = 30 + anchored = TRUE + /// Light range + var/lon_range = 1 + +/obj/structure/swarmer/Initialize(mapload) + . = ..() + set_light(lon_range) + +/obj/structure/swarmer/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0) + switch(damage_type) + if(BRUTE) + playsound(src, 'sound/weapons/egloves.ogg', 80, TRUE) + if(BURN) + playsound(src, 'sound/items/welder.ogg', 100, TRUE) + +/obj/structure/swarmer/emp_act() + do_sparks(3, 1, src) + qdel(src) + +/obj/structure/swarmer/trap + name = "swarmer trap" + desc = "A quickly assembled trap that electrifies living beings and overwhelms machine sensors. Will not retain its form if damaged enough." + icon_state = "trap" + max_integrity = 10 + +/obj/structure/swarmer/trap/Initialize(mapload) + . = ..() + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_atom_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) + +/obj/structure/swarmer/trap/proc/on_atom_entered(datum/source, atom/movable/entered) + SIGNAL_HANDLER // COMSIG_ATOM_ENTERED + if(!isliving(entered)) + return + var/mob/living/L = entered + if(istype(L, /mob/living/basic/swarmer)) + return + playsound(loc,'sound/effects/snap.ogg', 50, 1, -1) + L.electrocute_act(100, src, 1, flags = SHOCK_NOGLOVES | SHOCK_ILLUSION) + L.Weaken(10 SECONDS) + qdel(src) + +/obj/structure/swarmer/barricade + name = "swarmer barricade" + desc = "A quickly assembled energy barricade. Will not retain its form if damaged enough, but disabler beams and swarmers pass right through." + light_range = MINIMUM_USEFUL_LIGHT_RANGE + max_integrity = 50 + +/obj/structure/swarmer/barricade/CanPass(atom/movable/O) + if(istype(O, /mob/living/basic/swarmer)) + return TRUE + if(istype(O, /obj/projectile/beam/disabler)) + return TRUE diff --git a/code/modules/mob/living/basic/hostile/swarmers/swarmer_actions.dm b/code/modules/mob/living/basic/hostile/swarmers/swarmer_actions.dm new file mode 100644 index 00000000000..2efedd2717a --- /dev/null +++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer_actions.dm @@ -0,0 +1,99 @@ +/datum/action/cooldown/mob_cooldown/swarmer_trap + name = "Construct Shock Trap" + button_icon = 'icons/mob/swarmer.dmi' + button_icon_state = "ui_trap" + desc = "Construct a shock trap, using 25 resources." + click_to_activate = FALSE + melee_cooldown_time = CLICK_CD_CLICK_ABILITY + cooldown_time = 60 SECONDS + shared_cooldown = NONE + +/datum/action/cooldown/mob_cooldown/swarmer_trap/Activate(atom/target) + var/mob/living/basic/swarmer/user = target + if(!istype(user)) + to_chat(target, "Incompatible hardware detected. Aborting.") + return + var/turf/T = get_turf(user) + for(var/turf/turf_in_view in view(1, T)) + if(locate(/obj/structure/swarmer) in turf_in_view) + to_chat(user, "Location too close to existing hardware. Aborting.") + return + if(user.resources < 25) + to_chat(user, "Insufficient resources. Aborting.") + return + user.resources -= 25 + user.visible_message("[user] manufactures a sparking pylon.") + playsound(get_turf(user), 'sound/machines/click.ogg', 50, TRUE) + new /obj/structure/swarmer/trap(T) + StartCooldown() + +/datum/action/cooldown/mob_cooldown/swarmer_barrier + name = "Construct Barricade" + button_icon = 'icons/mob/swarmer.dmi' + button_icon_state = "ui_barricade" + desc = "Construct a barricade, using 25 resources." + click_to_activate = FALSE + melee_cooldown_time = CLICK_CD_CLICK_ABILITY + cooldown_time = 75 SECONDS + shared_cooldown = NONE + +/datum/action/cooldown/mob_cooldown/swarmer_barrier/Activate(atom/target) + var/mob/living/basic/swarmer/user = target + if(!istype(user)) + to_chat(target, "Incompatible hardware detected. Aborting.") + return + if(user.resources < 25) + to_chat(user, "Insufficient resources. Aborting.") + return + var/turf/T = get_turf(user) + for(var/turf/turf_in_view in view(1, T)) + if(locate(/obj/structure/swarmer) in turf_in_view) + to_chat(user, "Location too close to existing hardware. Aborting.") + return + if(!do_after_once(user, 2 SECONDS, target = T, attempt_cancel_message = "You stop building a barricade.", interaction_key = "swarmer_barricade_create")) + return + // Check again to make sure they still have the requirements + if(user.resources < 25) + to_chat(user, "Insufficient resources. Aborting.") + return + T = get_turf(user) + for(var/turf/turf_in_view in view(1, T)) + if(locate(/obj/structure/swarmer) in turf_in_view) + to_chat(user, "Location too close to existing hardware. Aborting.") + return + user.resources -= 25 + user.visible_message("[user] manufactures a holographic shield.") + playsound(get_turf(user), 'sound/machines/click.ogg', 50, TRUE) + new /obj/structure/swarmer/barricade(T) + StartCooldown() + +/datum/action/cooldown/mob_cooldown/swarmer_replicate + name = "Replicate" + button_icon = 'icons/mob/swarmer.dmi' + button_icon_state = "ui_replicate" + desc = "Construct a new swarmer, using 50 resources." + click_to_activate = FALSE + cooldown_time = 20 SECONDS + melee_cooldown_time = CLICK_CD_CLICK_ABILITY + shared_cooldown = NONE + +/datum/action/cooldown/mob_cooldown/swarmer_replicate/Activate(atom/target) + var/mob/living/basic/swarmer/user = target + if(!istype(user)) + to_chat(target, "Incompatible hardware detected. Aborting.") + return + if(user.resources < 50) + to_chat(user, "Insufficient resources. Aborting.") + return + if(!do_after_once(user, 4 SECONDS, target = user, attempt_cancel_message = "You stop building a new swarmer.", interaction_key = "swarmer_replicate_create")) + return + // Check again after the do-after. + if(user.resources < 50) + to_chat(user, "Insufficient resources. Aborting.") + return + var/turf/T = get_turf(user) + user.resources -= 50 + user.visible_message("[user] manufactures a new swarmer.") + playsound(get_turf(user), 'sound/items/rped.ogg', 50, TRUE) + new /mob/living/basic/swarmer/lesser(T) + StartCooldown() diff --git a/code/modules/mob/living/basic/hostile/swarmers/swarmer_ai.dm b/code/modules/mob/living/basic/hostile/swarmers/swarmer_ai.dm new file mode 100644 index 00000000000..a9229271aa2 --- /dev/null +++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer_ai.dm @@ -0,0 +1,297 @@ +/// Attacks people it can see, disintegrates things based on priority. +/datum/ai_controller/basic_controller/swarmer + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_AGGRO_RANGE = 4, + ) + ai_movement = /datum/ai_movement/jps + idle_behavior = /datum/idle_behavior/idle_random_walk + movement_delay = 0.2 SECONDS + ai_traits = AI_FLAG_PAUSE_DURING_DO_AFTER + + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/target_retaliate/check_faction, + /datum/ai_planning_subtree/ranged_skirmish/avoid_friendly, + /datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/swarmer_replicate, + /datum/ai_planning_subtree/swarmer_find_construction_target, + /datum/ai_planning_subtree/swarmer_create_trap, + /datum/ai_planning_subtree/swarmer_create_barricade, + /datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls/swarmer_hunting, + /datum/ai_planning_subtree/find_and_hunt_target/corpses/swarmer, + /datum/ai_planning_subtree/find_and_hunt_target/swarmer_objects, + /datum/ai_planning_subtree/find_and_hunt_target/prowl, + ) + +/// Attacks people it can see, disintegrates things based on priority. More aggressive. +/datum/ai_controller/basic_controller/swarmer/lesser + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_AGGRO_RANGE = 8, + ) + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/target_retaliate/check_faction, + /datum/ai_planning_subtree/ranged_skirmish/avoid_friendly, + /datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/swarmer_find_construction_target, + /datum/ai_planning_subtree/swarmer_create_trap, + /datum/ai_planning_subtree/swarmer_create_barricade, + /datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls/swarmer_hunting, + /datum/ai_planning_subtree/find_and_hunt_target/corpses/swarmer, + /datum/ai_planning_subtree/find_and_hunt_target/swarmer_objects, + /datum/ai_planning_subtree/find_and_hunt_target/prowl, + ) + +/datum/ai_planning_subtree/find_and_hunt_target/corpses/swarmer + hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/swarmer + hunt_range = 4 + +/datum/ai_planning_subtree/find_and_hunt_target/swarmer_objects + finding_behavior = /datum/ai_behavior/find_hunt_target/swarmer_objects + hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/swarmer + hunt_targets = list(/obj/item, /obj/structure, /obj/machinery, /turf/simulated/wall, /mob/living) + hunt_range = 4 + +/datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls/swarmer_hunting + target_key = BB_CURRENT_HUNTING_TARGET + +/datum/ai_behavior/hunt_target/interact_with_target/swarmer + always_reset_target = TRUE + hunt_cooldown = 0 SECONDS + +/datum/ai_behavior/hunt_target/interact_with_target/swarmer/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) + . = ..() + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED // Eat faster! + +/datum/ai_behavior/find_hunt_target/swarmer_objects + +/datum/ai_behavior/find_hunt_target/swarmer_objects/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range) + . = ..() + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED // Eat faster! + +/datum/ai_behavior/find_hunt_target/swarmer_objects/valid_dinner(mob/living/source, atom/dinner, radius) + if(is_type_in_list(dinner, GLOB.swarmer_blacklist)) + return FALSE + if(HAS_TRAIT(dinner, TRAIT_SWARMER_DISINTEGRATING)) + return FALSE + if(isitem(dinner) && !isturf(dinner.loc)) + return FALSE + if(isobj(dinner)) + var/obj/obj_dinner = dinner + if(obj_dinner.resistance_flags & INDESTRUCTIBLE) + return FALSE + if(isliving(dinner)) + var/mob/living/M = dinner + if(M.stat == DEAD) + return TRUE + for(var/turf/T in range(1, dinner)) + var/datum/gas_mixture/environment = T.get_readonly_air() + var/environment_pressure = environment.return_pressure() + var/datum/tlv/cur_tlv = new/datum/tlv(ONE_ATMOSPHERE * 0.80, ONE_ATMOSPHERE *0.90, ONE_ATMOSPHERE * 1.10,ONE_ATMOSPHERE * 1.20) /* kpa */ + var/pressure_dangerlevel = cur_tlv.get_danger_level(environment_pressure) + var/area/A = get_area(T) + if(isspaceturf(T) || istype(A, /area/shuttle) || istype(A, /area/space) || istype(A, /area/station/engineering/engine/supermatter) || pressure_dangerlevel) + return FALSE + return TRUE + +/// Run the barricade construction behaviour if we have an ability to use for it +/datum/ai_planning_subtree/swarmer_replicate + /// Key where the trap action is stored + var/action_key = BB_SWARMER_REPLICATE_ACTION + +/datum/ai_planning_subtree/swarmer_replicate/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/basic/swarmer/user = controller.pawn + if(!istype(user)) + return + if(user.resources < 50) + return + if(!controller.blackboard_key_exists(action_key)) + return + var/datum/action/cooldown/mob_cooldown/swarmer_replicate/replicate_action = controller.blackboard[action_key] + if(replicate_action.IsAvailable()) + controller.queue_behavior(/datum/ai_behavior/swarmer_replicate, action_key) + return SUBTREE_RETURN_FINISH_PLANNING + +/// REPLICATE +/datum/ai_behavior/swarmer_replicate + required_distance = 0 + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/swarmer_replicate/setup(datum/ai_controller/controller, action_key) + var/datum/action/cooldown/mob_cooldown/swarmer_replicate/replicate_action = controller.blackboard[action_key] + var/mob/living/basic/swarmer/user = controller.pawn + if(!istype(user)) + return FALSE + if(!replicate_action || user.resources < 50) + return FALSE + return ..() + +/datum/ai_behavior/swarmer_replicate/perform(seconds_per_tick, datum/ai_controller/controller, action_key) + . = ..() + var/datum/action/cooldown/mob_cooldown/swarmer_replicate/replicate_action = controller.blackboard[action_key] + var/result = replicate_action.Trigger() + if(result) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + +/// Search for a nearby location to build on +/datum/ai_planning_subtree/swarmer_find_construction_target + +/datum/ai_planning_subtree/swarmer_find_construction_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/basic/swarmer/user = controller.pawn + if(!istype(user)) + return + if(user.resources < 25) + return + controller.queue_behavior(/datum/ai_behavior/swarmer_find_construction_target) + +/// Find a nearby clear turf and store it +/datum/ai_behavior/swarmer_find_construction_target + action_cooldown = 10 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + /// Where do we store the target data + var/target_key = BB_SWARMER_CONSTRUCT_TARGET + /// How far do we look for valid turfs? + var/scan_range = 5 + +/datum/ai_behavior/swarmer_find_construction_target/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/swarmer = controller.pawn + var/atom/current_target = controller.blackboard[target_key] + if(current_target && !(locate(/obj/structure/swarmer) in current_target)) + // Already got a target + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + controller.clear_blackboard_key(target_key) + var/turf/our_turf = get_turf(swarmer) + if(is_valid_construction_turf(our_turf, swarmer)) + controller.set_blackboard_key(target_key, our_turf) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + + var/list/turfs_by_range = list() + for(var/i in 1 to scan_range) + turfs_by_range["[i]"] = list() + for(var/turf/turf_in_view in oview(scan_range, our_turf)) + if(!is_valid_construction_turf(turf_in_view, swarmer)) + continue + turfs_by_range["[get_dist(our_turf, turf_in_view)]"] += turf_in_view + + var/list/final_turfs + for(var/list/turf_list as anything in turfs_by_range) + if(length(turfs_by_range[turf_list])) + final_turfs = turfs_by_range[turf_list] + break + if(!length(final_turfs)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + controller.set_blackboard_key(target_key, pick(final_turfs)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/swarmer_find_construction_target/proc/is_valid_construction_turf(turf/target_turf, mob/living/swarmer) + for(var/turf/turf_in_view in view(1, target_turf)) + if(locate(/obj/structure/swarmer) in turf_in_view) + return FALSE + if(HAS_TRAIT(target_turf, TRAIT_SWARMER_CONSTRUCTION)) + return FALSE + if(isspaceturf(target_turf)) + return FALSE + var/datum/gas_mixture/environment = target_turf.get_readonly_air() + var/datum/tlv/cur_tlv = new/datum/tlv(ONE_ATMOSPHERE * 0.80, ONE_ATMOSPHERE *0.90, ONE_ATMOSPHERE * 1.10,ONE_ATMOSPHERE * 1.20) /* kpa */ + var/environment_pressure = environment.return_pressure() + var/pressure_dangerlevel = cur_tlv.get_danger_level(environment_pressure) + if(pressure_dangerlevel) + return FALSE + return !target_turf.is_blocked_turf(source_atom = swarmer) + +/// Run the trap construction behaviour if we have an ability to use for it +/datum/ai_planning_subtree/swarmer_create_trap + /// Key where the trap action is stored + var/action_key = BB_SWARMER_TRAP_ACTION + /// Key where the target turf is stored + var/target_key = BB_SWARMER_CONSTRUCT_TARGET + +/datum/ai_planning_subtree/swarmer_create_trap/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(action_key) || !controller.blackboard_key_exists(target_key)) + return + var/datum/action/cooldown/mob_cooldown/swarmer_trap/trap_action = controller.blackboard[action_key] + if(trap_action.IsAvailable()) + controller.queue_behavior(/datum/ai_behavior/swarmer_create_trap, action_key, target_key) + return SUBTREE_RETURN_FINISH_PLANNING + +/// Move to an open nearby turf and build a trap +/datum/ai_behavior/swarmer_create_trap + action_cooldown = 15 SECONDS // We don't want them doing this too quickly + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/swarmer_create_trap/setup(datum/ai_controller/controller, action_key, target_key) + var/datum/action/cooldown/mob_cooldown/swarmer_trap/trap_action = controller.blackboard[action_key] + var/turf/target_turf = controller.blackboard[target_key] + var/mob/living/basic/swarmer/user = controller.pawn + if(!istype(user)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + if(!trap_action || !target_turf || user.resources < 25) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + set_movement_target(controller, target_turf) + return ..() + +/datum/ai_behavior/swarmer_create_trap/perform(seconds_per_tick, datum/ai_controller/controller, action_key, target_key) + . = ..() + var/datum/action/cooldown/mob_cooldown/swarmer_trap/trap_action = controller.blackboard[action_key] + var/result = trap_action.Trigger() + if(result) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/swarmer_create_trap/finish_action(datum/ai_controller/controller, succeeded, action_key, target_key) + controller.clear_blackboard_key(target_key) + return ..() + +/// Run the barricade construction behaviour if we have an ability to use for it +/datum/ai_planning_subtree/swarmer_create_barricade + /// Key where the trap action is stored + var/action_key = BB_SWARMER_BARRIER_ACTION + /// Key where the target turf is stored + var/target_key = BB_SWARMER_CONSTRUCT_TARGET + +/datum/ai_planning_subtree/swarmer_create_barricade/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(action_key) || !controller.blackboard_key_exists(target_key)) + return + var/datum/action/cooldown/mob_cooldown/swarmer_barrier/barrier_action = controller.blackboard[action_key] + if(barrier_action.IsAvailable()) + controller.queue_behavior(/datum/ai_behavior/swarmer_create_trap, action_key, target_key) + return SUBTREE_RETURN_FINISH_PLANNING + +/// Move to an open nearby turf and build a barricade +/datum/ai_behavior/swarmer_create_barricade + action_cooldown = 45 SECONDS + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/swarmer_create_barricade/setup(datum/ai_controller/controller, action_key, target_key) + var/datum/action/cooldown/mob_cooldown/swarmer_barrier/barrier_action = controller.blackboard[action_key] + var/turf/target_turf = controller.blackboard[target_key] + var/mob/living/basic/swarmer/user = controller.pawn + if(!istype(user)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + if(!barrier_action || !target_turf || user.resources < 25) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + set_movement_target(controller, target_turf) + return ..() + +/datum/ai_behavior/swarmer_create_barricade/perform(seconds_per_tick, datum/ai_controller/controller, action_key, target_key) + . = ..() + var/datum/action/cooldown/mob_cooldown/swarmer_barrier/barrier_action = controller.blackboard[action_key] + if(barrier_action?.Trigger()) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/swarmer_create_barricade/finish_action(datum/ai_controller/controller, succeeded, action_key, target_key) + controller.clear_blackboard_key(target_key) + return ..() diff --git a/code/modules/mob/living/basic/hostile/swarmers/swarmer_effects.dm b/code/modules/mob/living/basic/hostile/swarmers/swarmer_effects.dm new file mode 100644 index 00000000000..e5d42c05736 --- /dev/null +++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer_effects.dm @@ -0,0 +1,15 @@ +/obj/effect/temp_visual/swarmer // temporary swarmer visual feedback objects + icon = 'icons/mob/swarmer.dmi' + icon_state = "disintegrate" + layer = ABOVE_ALL_MOB_LAYER + +/obj/effect/temp_visual/swarmer/Initialize(mapload) + . = ..() + playsound(loc, "sparks", 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + +/obj/effect/temp_visual/swarmer/dismantle + icon_state = "dismantle" + duration = 2.5 SECONDS + +/obj/effect/temp_visual/swarmer/integrate + icon_state = "integrate" diff --git a/icons/mob/swarmer.dmi b/icons/mob/swarmer.dmi new file mode 100644 index 0000000000000000000000000000000000000000..bc495e8199e09ec87d896b1a0401886fe6fd57b5 GIT binary patch literal 20334 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 literal 0 HcmV?d00001 diff --git a/paradise.dme b/paradise.dme index 3664c9df0c1..b2f5f9b6680 100644 --- a/paradise.dme +++ b/paradise.dme @@ -163,6 +163,7 @@ #include "code\__DEFINES\ai\blackboard_defines.dm" #include "code\__DEFINES\ai\changeling_giant_spider_ai_defines.dm" #include "code\__DEFINES\ai\incursion_blackboard_defines.dm" +#include "code\__DEFINES\ai\swarmer_defines.dm" #include "code\__DEFINES\dcs\ai_signals.dm" #include "code\__DEFINES\dcs\area_signals.dm" #include "code\__DEFINES\dcs\atom_signals.dm" @@ -2140,6 +2141,7 @@ #include "code\modules\events\spider_infestation.dm" #include "code\modules\events\spider_terror.dm" #include "code\modules\events\spontaneous_appendicitis.dm" +#include "code\modules\events\swarmers.dm" #include "code\modules\events\tear.dm" #include "code\modules\events\tear_honk.dm" #include "code\modules\events\traders.dm" @@ -2531,6 +2533,10 @@ #include "code\modules\mob\living\basic\hostile\giant_spider\giant_spider_ai.dm" #include "code\modules\mob\living\basic\hostile\gorilla\gorilla.dm" #include "code\modules\mob\living\basic\hostile\gorilla\gorilla_emote.dm" +#include "code\modules\mob\living\basic\hostile\swarmers\swarmer.dm" +#include "code\modules\mob\living\basic\hostile\swarmers\swarmer_actions.dm" +#include "code\modules\mob\living\basic\hostile\swarmers\swarmer_ai.dm" +#include "code\modules\mob\living\basic\hostile\swarmers\swarmer_effects.dm" #include "code\modules\mob\living\basic\hostile\nether_mobs\blankbody.dm" #include "code\modules\mob\living\basic\hostile\nether_mobs\creature.dm" #include "code\modules\mob\living\basic\hostile\nether_mobs\faithless.dm"