From d6ed342a6deb49b91e347827b4d6247e1b899a33 Mon Sep 17 00:00:00 2001
From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Date: Fri, 21 Nov 2025 19:31:59 -0500
Subject: [PATCH] Adds midround event: Swarmer Infestation (#30673)
* 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
---------
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 ++++
.../basic_subtrees/attack_obstacle_in_path.dm | 9 +
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 | 365 ++++++++++++++++++
.../basic/hostile/swarmers/swarmer_actions.dm | 99 +++++
.../basic/hostile/swarmers/swarmer_ai.dm | 284 ++++++++++++++
.../basic/hostile/swarmers/swarmer_effects.dm | 15 +
icons/mob/swarmer.dmi | Bin 0 -> 23284 bytes
paradise.dme | 6 +
14 files changed, 951 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/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
index d2691e9b3f2..e191d5d5465 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
@@ -99,3 +99,12 @@
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl
attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches
target_key = BB_PROWL_TARGET
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches/walls
+
+/datum/ai_behavior/attack_obstructions/avoid_breaches/walls
+ can_attack_turfs = TRUE
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..03feca3189e
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer.dm
@@ -0,0 +1,365 @@
+// =====================
+// 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
+ 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.")
+ 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.loc)
+ 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/machinery/target)
+ if(spacecheck(target))
+ 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 = "ui_light"
+ layer = MOB_LAYER
+ anchored = TRUE
+ 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."
+ icon_state = "barricade"
+ 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..4efc90e82e9
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/swarmers/swarmer_ai.dm
@@ -0,0 +1,284 @@
+/// 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
+
+ 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/find_hunt_target/swarmer_objects
+
+/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(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 = 5 SECONDS
+ /// 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_DELAY | 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_DELAY | 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_DELAY | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(target_key, pick(final_turfs))
+ return AI_BEHAVIOR_DELAY | 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_DELAY | AI_BEHAVIOR_FAILED
+ if(!trap_action || !target_turf || user.resources < 25)
+ return AI_BEHAVIOR_DELAY | 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_DELAY | AI_BEHAVIOR_FAILED
+ if(!barrier_action || !target_turf || user.resources < 25)
+ return AI_BEHAVIOR_DELAY | 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_DELAY | AI_BEHAVIOR_SUCCEEDED
+ return AI_BEHAVIOR_DELAY | 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..798a32f1ab0
--- /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 = BELOW_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..97c95ebdfdb145e490b9886f46d4d9199830f488
GIT binary patch
literal 23284
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