From 24845b238a810d45ffce731778800a9ff61b8a5a Mon Sep 17 00:00:00 2001
From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Date: Wed, 8 Jul 2026 17:31:28 -0400
Subject: [PATCH] The Divine Flock - DaedalusDock Flockmind Port (#31984)
* Initial commit - FLOCKMIND - Probably has like a billion things to fix
* Do after conversions
* Config
* Moved the files, icon fixes
* Tick everything, language work, event, spawn landmark, role prefs, beginning mob port
* Spans and some other fixes. Also the tickening
* More tickening
* More fixes. Lots of fixes.
* More Fixes
* A whole lot more. Also flock TGUI.
* Fixes fixes fixes fixes fixes
* FIXES
* More fixes - PR ready, still needs a fuckton of testing
* Fixes
* fix incomplete upstream merge
* fix FlockPanel + sort button name
* TGUI review
* Fixes tealprint list
* Fixes
* More fixes
* Incapacitator Fix
* Filenames
* Linters
* Interceptor range buff
* Reagent counts
* Linters
* Fabricator vendor fix
* Keybinds and HUD - Flockdrones, Fixes Vendor Conversion, Cube Materials
* Reworks reagent, adds flock grilles, fixes compute node overlays
* Intent-based flockdrone parts
* Intent based drone parts
* Radial control panel for controlling drones manually, phasing through windows/grilles
* Movement fixes
* Radio talk power, stare fix
* Flock health HUD
* Fixes flock lights, linters
* Unit tests
* Adds countdown to relay
* Relay improvements
* Small fix
* Logic Schmogic
* Relay overlay and looping sound effect
* Ignore air when converting turfs
* Cage fixes and improvements
* Improved flock bolt
* Turret conversions
* Flock bolts taze simple or basic mobs
* Sentience type
* Fixe
* Linter
* tgui review stage 2
* Concentrated Repair Burst
* Improves radio detection
* Removes extra space
* Adds healing visual effect
* Cube tech levels
* Ghooost
* Excess
* Flock doors, chairs, lattices. Centralizes conversion code. Crafting with Gnesis
* Update code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm
Co-authored-by: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
* Fixes the fix
* Astar movement detection
* Fix, extraneous code, language stuff
* Language fixes and wander fix
* Fixes
* Another fix
* Lints
* Another linter
* Language improvement
* More language improvements
* Time requirement and appearing in orbit menu as an antag
* Cube glow
* TGUI
* Minicache
* Linters
* Grammar
* Material ID fix
* Lid fix
* Reagent turf reaction
* Reagent fix
* Butcher results
* Conversion rates
* Flock stare fix
* Fixes stare behavior
* Staring
* Flock mob blood
* Flock mobs gibs and blood. Also some runtime fixes
* Flock mobs now resist out of grabs, buckles, lockers, and more
* Fixes flock orbit, fixes a runtime I think,
* Target mechs, damage mechs, other bug fixes
* Cage fix
* Cage resist change
* Some mind changes, gatecrash buff
* Drones now shoot mechs, stare improvement
* Cut down on spam a little
* Nest fix
* No more resist spam
* Fixed drone death control
* Resist statement
* Makes the relay alarm scarier
* Fixes dead flock camera mobs having no ghost sprite, something with ghosting
* Enhanced flockphasing
* Improved flockmob pathing
* Added required turf restriction to relay
* Increased needed bandwidth for relay construction
* Nerfed drone substrate rate
* Added new status tab items for relay progress
* Another relay cost adjustment
* Improves drone AI responsiveness
* Computer frames now become flock computers
* Improves target finding for conversion, building, and replicating
* Reduced flock event pop requirements
* Adjusts flock protection on structures. Adjusts overlays.
* Relay unlock tweak
* Fixwes flock being able to gib mech'd AIs with one button
* Map conflict
* Flock can no longer be outed by merely existing
* Fied bug causing drones to shoot themselves
* Prevents mobs from attacking while in a cage
* Converter tool can now open closets and crates.
* Adds descriptions to flockdrone tools.
* More informatic blurbs
* Adds xenobiology organs
* Organ lint
* TGUI merge
* bundle and mm
---------
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Co-authored-by: Toastical <20125180+Toastical@users.noreply.github.com>
Co-authored-by: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
---
_maps/map_files/stations/boxstation.dmm | 5 +
_maps/map_files/stations/cerestation.dmm | 4 +
_maps/map_files/stations/deltastation.dmm | 4 +
_maps/map_files/stations/emeraldstation.dmm | 4 +
_maps/map_files/stations/metastation.dmm | 70 +-
code/__DEFINES/ai/ai_defines.dm | 10 +
code/__DEFINES/ai/blackboard_defines.dm | 4 +
code/__DEFINES/ai/flockmind_ai_defines.dm | 22 +
code/__DEFINES/color_defines.dm | 1 +
code/__DEFINES/construction_defines.dm | 2 +
code/__DEFINES/dcs/ai_signals.dm | 3 +
code/__DEFINES/dcs/moveloop_signals.dm | 2 +
code/__DEFINES/dcs/signals_flock.dm | 4 +
code/__DEFINES/flags.dm | 5 +-
code/__DEFINES/flockmind_defines.dm | 70 ++
code/__DEFINES/gamemode.dm | 1 +
code/__DEFINES/is_helpers.dm | 18 +
code/__DEFINES/layers.dm | 2 +
code/__DEFINES/mob_defines.dm | 5 +
code/__DEFINES/role_preferences.dm | 2 +
code/__DEFINES/rolebans.dm | 1 +
code/__DEFINES/sight.dm | 3 +
code/__DEFINES/span_defines.dm | 2 +
code/__HELPERS/flock_helpers.dm | 118 +++
code/__HELPERS/lists.dm | 27 +
code/__HELPERS/paths/astar.dm | 276 +++++++
code/__HELPERS/paths/jps.dm | 4 +-
code/__HELPERS/paths/path.dm | 12 +-
code/__HELPERS/spatial_info.dm | 11 +
code/__HELPERS/trait_helpers.dm | 23 +
code/__HELPERS/unsorted.dm | 31 +
code/_globalvars/lists/misc_lists.dm | 4 +
code/_onclick/click.dm | 2 +-
code/_onclick/hud/alert.dm | 12 +
code/_onclick/hud/flock_hud.dm | 187 +++++
code/_onclick/hud/fullscreen.dm | 5 +
code/controllers/subsystem/SSmobs.dm | 2 +
.../subsystem/movement/movement_types.dm | 213 ++++++
.../subsystem/non_firing/SSpathfinder.dm | 71 ++
code/datums/actions/cooldown_action.dm | 13 +
code/datums/ai/ai_behavior.dm | 135 ++++
code/datums/ai/ai_controller.dm | 10 +-
code/datums/ai/ai_planning_subtree.dm | 6 +
.../basic_ai_behaviors/frustration.dm | 24 +
code/datums/ai/goap_subtree.dm | 47 ++
code/datums/ai/movement/ai_movement_astar.dm | 44 ++
code/datums/components/material_container.dm | 10 +
code/datums/keybindings/flock_keybinds.dm | 64 ++
.../datums/looping_sounds/machinery_sounds.dm | 9 +
code/datums/point_holder.dm | 72 ++
.../proximity/advanced_proximity_monitor.dm | 11 +
code/datums/proximity/interceptor_field.dm | 14 +
code/game/alternate_appearance.dm | 7 +
code/game/jobs/job_exp.dm | 1 +
.../effects/decals/Cleanable/alien_blood.dm | 6 +
code/game/objects/effects/gibs.dm | 3 +
code/game/objects/effects/info_tags.dm | 63 ++
code/game/objects/effects/landmarks.dm | 4 +
.../objects/effects/spawners/gibspawner.dm | 12 +
.../game/objects/items/stacks/sheets/glass.dm | 26 +
.../objects/items/stacks/sheets/mineral.dm | 25 +
code/game/objects/items/weapons/shards.dm | 9 +
code/game/objects/obj_defense.dm | 9 +
code/game/turfs/simulated/walls_misc.dm | 7 +
.../antagonists/flockmind/actions/cage_mob.dm | 42 ++
.../flockmind/actions/control_drone.dm | 148 ++++
.../flockmind/actions/create_rift.dm | 27 +
.../flockmind/actions/create_structure.dm | 49 ++
.../flockmind/actions/deconstruct.dm | 43 ++
.../antagonists/flockmind/actions/deposit.dm | 23 +
.../actions/designate_deconstruct.dm | 18 +
.../flockmind/actions/designate_enemy.dm | 65 ++
.../flockmind/actions/designate_turf.dm | 37 +
.../flockmind/actions/diffract_drone.dm | 23 +
.../flockmind/actions/flock_action.dm | 3 +
.../flockmind/actions/flock_control_panel.dm | 9 +
.../flockmind/actions/flock_convert_action.dm | 67 ++
.../flockmind/actions/flock_heal_action.dm | 24 +
.../flockmind/actions/flock_heal_surge.dm | 30 +
.../flockmind/actions/flock_nest_action.dm | 44 ++
.../flockmind/actions/flock_ping_action.dm | 14 +
.../flockmind/actions/gatecrash.dm | 44 ++
.../flockmind/actions/partition_mind.dm | 83 +++
.../flockmind/actions/radio_blast.dm | 39 +
.../flockmind/actions/radio_talk.dm | 40 ++
.../flockmind/actions/release_control.dm | 13 +
.../ai_behaviors/_flock_behavior_base.dm | 19 +
.../flockmind/ai_behaviors/flock_capture.dm | 86 +++
.../flockmind/ai_behaviors/flock_convert.dm | 158 ++++
.../ai_behaviors/flock_deconstruct.dm | 114 +++
.../flockmind/ai_behaviors/flock_deposit.dm | 101 +++
.../flockmind/ai_behaviors/flock_harvest.dm | 88 +++
.../flockmind/ai_behaviors/flock_heal.dm | 90 +++
.../ai_behaviors/flock_open_container.dm | 63 ++
.../flockmind/ai_behaviors/flock_rally.dm | 17 +
.../flockmind/ai_behaviors/flock_replicate.dm | 69 ++
.../flockmind/ai_behaviors/flock_resist.dm | 17 +
.../flockmind/ai_behaviors/flock_rummage.dm | 80 +++
.../flockmind/ai_behaviors/flock_shoot.dm | 91 +++
.../flockmind/ai_behaviors/flock_stare.dm | 49 ++
.../flockmind/ai_behaviors/flock_wander.dm | 72 ++
.../flockmind/components/flock_interest.dm | 65 ++
.../flockmind/components/flock_object.dm | 25 +
.../flockmind/components/flock_ping.dm | 63 ++
.../flockmind/components/flock_protection.dm | 51 ++
.../flockmind/flock_ai_controller.dm | 12 +
.../antagonists/flockmind/flock_antagonist.dm | 21 +
.../flockmind/flock_camera/_flock_camera.dm | 175 +++++
.../flockmind/flock_camera/flock_overmind.dm | 88 +++
.../flockmind/flock_camera/flocktrace.dm | 49 ++
.../flock_controller/_flock_controller.dm | 677 ++++++++++++++++++
.../flockmind/flock_controller/flock_ui.dm | 119 +++
.../flockmind/flock_convert_atom.dm | 141 ++++
.../antagonists/flockmind/flock_effects.dm | 23 +
.../flockmind/flock_global_lists.dm | 54 ++
.../antagonists/flockmind/flock_hooks.dm | 3 +
.../flockmind/flock_items/flock_cache.dm | 25 +
.../flockmind/flock_items/flock_cube.dm | 26 +
.../antagonists/flockmind/flock_language.dm | 79 ++
.../flockmind/flock_mob/flock_mob.dm | 224 ++++++
.../flockmind/flock_mob/flockbit.dm | 47 ++
.../flockmind/flock_mob/flockdrone.dm | 470 ++++++++++++
.../flock_mob/flockdrone_parts/absorber.dm | 95 +++
.../flock_mob/flockdrone_parts/converter.dm | 36 +
.../flockdrone_parts/flockdrone_part.dm | 26 +
.../flockdrone_parts/incapacitator.dm | 54 ++
.../antagonists/flockmind/flock_objectives.dm | 7 +
.../antagonists/flockmind/flock_say.dm | 13 +
.../flock_structure/_flock_structure.dm | 228 ++++++
.../flockmind/flock_structure/flock_cage.dm | 272 +++++++
.../flockmind/flock_structure/flock_chair.dm | 27 +
.../flockmind/flock_structure/flock_closet.dm | 68 ++
.../flock_structure/flock_collector.dm | 166 +++++
.../flock_structure/flock_compute_node.dm | 37 +
.../flockmind/flock_structure/flock_door.dm | 84 +++
.../flockmind/flock_structure/flock_egg.dm | 35 +
.../flock_structure/flock_fabricator.dm | 92 +++
.../flockmind/flock_structure/flock_grille.dm | 155 ++++
.../flock_structure/flock_interceptor.dm | 100 +++
.../flock_structure/flock_lattice.dm | 28 +
.../flockmind/flock_structure/flock_light.dm | 35 +
.../flockmind/flock_structure/flock_relay.dm | 228 ++++++
.../flockmind/flock_structure/flock_rift.dm | 63 ++
.../flock_structure/flock_sentinel.dm | 177 +++++
.../flock_structure/flock_tealprint.dm | 75 ++
.../flockmind/flock_structure/flock_turret.dm | 136 ++++
.../flockmind/flock_structure/flock_window.dm | 68 ++
.../antagonists/flockmind/flock_subtree.dm | 25 +
.../flockmind/flock_turfs/flock_floor.dm | 69 ++
.../flockmind/flock_turfs/flock_wall.dm | 47 ++
.../antagonists/flockmind/flock_unlockable.dm | 50 ++
.../antagonists/flockmind/gnesis_reagent.dm | 87 +++
code/modules/client/preference/preferences.dm | 1 +
code/modules/countdown/countdown.dm | 10 +
code/modules/events/event_container.dm | 1 +
code/modules/events/flockmind_event.dm | 54 ++
code/modules/mob/dead/observer/orbit.dm | 5 +-
code/modules/mob/living/damage_procs.dm | 4 +
code/modules/mob/living/default_language.dm | 2 +-
code/modules/mob/living/living.dm | 2 +
code/modules/mob/living/living_defines.dm | 2 -
code/modules/mob/mob_misc_procs.dm | 2 +-
code/modules/mob/mob_vars.dm | 2 +
.../projectiles/projectile/flock_bolt.dm | 43 ++
.../xenobiology/xenobiology_dissections.dm | 12 +
.../xenobiology/xenobiology_organs.dm | 91 +++
code/modules/tgui/states/flock_state.dm | 14 +
icons/effects/spawner_icons.dmi | Bin 50769 -> 50699 bytes
icons/goonstation/hud/flock_ui.dmi | Bin 0 -> 38451 bytes
icons/goonstation/hud/flockmindcircuit.dmi | Bin 0 -> 216894 bytes
icons/goonstation/mob/featherzone.dmi | Bin 0 -> 246881 bytes
.../objects/featherzone-160x160.dmi | Bin 0 -> 150851 bytes
icons/goonstation/objects/lighting.dmi | Bin 0 -> 5669 bytes
icons/goonstation/turf/flock.dmi | Bin 0 -> 10777 bytes
icons/hud/radial.dmi | Bin 29309 -> 29506 bytes
icons/obj/chairs.dmi | Bin 74713 -> 77157 bytes
icons/obj/closet.dmi | Bin 43905 -> 45421 bytes
icons/obj/stacks/minerals.dmi | Bin 24251 -> 28149 bytes
paradise.dme | 104 +++
sound/goonstation/effects/radio_sweep1.ogg | Bin 0 -> 14515 bytes
sound/goonstation/effects/radio_sweep2.ogg | Bin 0 -> 13879 bytes
sound/goonstation/effects/radio_sweep3.ogg | Bin 0 -> 13591 bytes
sound/goonstation/effects/radio_sweep4.ogg | Bin 0 -> 17167 bytes
sound/goonstation/effects/radio_sweep5.ogg | Bin 0 -> 18943 bytes
sound/goonstation/flockmind/ArtifactFea1.ogg | Bin 0 -> 83856 bytes
sound/goonstation/flockmind/ArtifactFea2.ogg | Bin 0 -> 225200 bytes
sound/goonstation/flockmind/ArtifactFea3.ogg | Bin 0 -> 72592 bytes
sound/goonstation/flockmind/Flock_Reactor.ogg | Bin 0 -> 63571 bytes
sound/goonstation/flockmind/click.ogg | Bin 0 -> 7761 bytes
sound/goonstation/flockmind/deny.ogg | Bin 0 -> 8589 bytes
.../flockmind/flock_broadcast_charge.ogg | Bin 0 -> 119147 bytes
.../flockmind/flock_broadcast_kaboom.ogg | Bin 0 -> 67877 bytes
sound/goonstation/flockmind/flock_sound.ogg | Bin 0 -> 15533 bytes
.../goonstation/flockmind/flockbit_wisp1.ogg | Bin 0 -> 8365 bytes
.../goonstation/flockmind/flockbit_wisp2.ogg | Bin 0 -> 8205 bytes
.../goonstation/flockmind/flockbit_wisp3.ogg | Bin 0 -> 8143 bytes
.../goonstation/flockmind/flockbit_wisp4.ogg | Bin 0 -> 11548 bytes
.../goonstation/flockmind/flockbit_wisp5.ogg | Bin 0 -> 12934 bytes
.../goonstation/flockmind/flockbit_wisp6.ogg | Bin 0 -> 8106 bytes
.../flockmind/flockdrone_beep1.ogg | Bin 0 -> 12471 bytes
.../flockmind/flockdrone_beep2.ogg | Bin 0 -> 9148 bytes
.../flockmind/flockdrone_beep3.ogg | Bin 0 -> 8342 bytes
.../flockmind/flockdrone_beep4.ogg | Bin 0 -> 10064 bytes
.../flockmind/flockdrone_build.ogg | Bin 0 -> 74926 bytes
.../flockmind/flockdrone_build_complete.ogg | Bin 0 -> 13462 bytes
.../flockmind/flockdrone_convert.ogg | Bin 0 -> 30357 bytes
.../goonstation/flockmind/flockdrone_door.ogg | Bin 0 -> 7379 bytes
.../flockmind/flockdrone_door_deny.ogg | Bin 0 -> 6742 bytes
.../goonstation/flockmind/flockdrone_fart.ogg | Bin 0 -> 12024 bytes
.../flockmind/flockdrone_floorrun.ogg | Bin 0 -> 6267 bytes
.../flockmind/flockdrone_grump1.ogg | Bin 0 -> 9947 bytes
.../flockmind/flockdrone_grump2.ogg | Bin 0 -> 10003 bytes
.../flockmind/flockdrone_grump3.ogg | Bin 0 -> 9686 bytes
.../flockmind/flockdrone_locker_close.ogg | Bin 0 -> 6247 bytes
.../flockmind/flockdrone_locker_open.ogg | Bin 0 -> 5658 bytes
.../flockmind/flockdrone_quickbuild.ogg | Bin 0 -> 16602 bytes
.../goonstation/flockmind/flockmind_cast.ogg | Bin 0 -> 12996 bytes
sound/goonstation/flockmind/flockmind_caw.ogg | Bin 0 -> 11566 bytes
.../flockmind/flockmind_deathcry.ogg | Bin 0 -> 29288 bytes
sound/goonstation/flockmind/hover.ogg | Bin 0 -> 5678 bytes
sound/goonstation/flockmind/ping.ogg | Bin 0 -> 4988 bytes
sound/goonstation/weapons/nano-blade-1.ogg | Bin 0 -> 4354 bytes
strings/flock.json | 81 +++
.../styles/tgchat/chat-default.scss | 5 +
.../tgui-panel/styles/tgchat/chat-light.scss | 4 +
tgui/packages/tgui/interfaces/FlockPanel.tsx | 438 +++++++++++
tgui/public/tgui-panel.bundle.css | 2 +-
tgui/public/tgui.bundle.js | 2 +-
228 files changed, 9724 insertions(+), 49 deletions(-)
create mode 100644 code/__DEFINES/ai/flockmind_ai_defines.dm
create mode 100644 code/__DEFINES/dcs/signals_flock.dm
create mode 100644 code/__DEFINES/flockmind_defines.dm
create mode 100644 code/__HELPERS/flock_helpers.dm
create mode 100644 code/__HELPERS/paths/astar.dm
create mode 100644 code/_onclick/hud/flock_hud.dm
create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/frustration.dm
create mode 100644 code/datums/ai/goap_subtree.dm
create mode 100644 code/datums/ai/movement/ai_movement_astar.dm
create mode 100644 code/datums/keybindings/flock_keybinds.dm
create mode 100644 code/datums/point_holder.dm
create mode 100644 code/datums/proximity/interceptor_field.dm
create mode 100644 code/game/objects/effects/info_tags.dm
create mode 100644 code/modules/antagonists/flockmind/actions/cage_mob.dm
create mode 100644 code/modules/antagonists/flockmind/actions/control_drone.dm
create mode 100644 code/modules/antagonists/flockmind/actions/create_rift.dm
create mode 100644 code/modules/antagonists/flockmind/actions/create_structure.dm
create mode 100644 code/modules/antagonists/flockmind/actions/deconstruct.dm
create mode 100644 code/modules/antagonists/flockmind/actions/deposit.dm
create mode 100644 code/modules/antagonists/flockmind/actions/designate_deconstruct.dm
create mode 100644 code/modules/antagonists/flockmind/actions/designate_enemy.dm
create mode 100644 code/modules/antagonists/flockmind/actions/designate_turf.dm
create mode 100644 code/modules/antagonists/flockmind/actions/diffract_drone.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_action.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_control_panel.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_convert_action.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_heal_action.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_heal_surge.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_nest_action.dm
create mode 100644 code/modules/antagonists/flockmind/actions/flock_ping_action.dm
create mode 100644 code/modules/antagonists/flockmind/actions/gatecrash.dm
create mode 100644 code/modules/antagonists/flockmind/actions/partition_mind.dm
create mode 100644 code/modules/antagonists/flockmind/actions/radio_blast.dm
create mode 100644 code/modules/antagonists/flockmind/actions/radio_talk.dm
create mode 100644 code/modules/antagonists/flockmind/actions/release_control.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/_flock_behavior_base.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_capture.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_convert.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_deconstruct.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_deposit.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_harvest.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_heal.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_open_container.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_rally.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_replicate.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_resist.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_rummage.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_shoot.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_stare.dm
create mode 100644 code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm
create mode 100644 code/modules/antagonists/flockmind/components/flock_interest.dm
create mode 100644 code/modules/antagonists/flockmind/components/flock_object.dm
create mode 100644 code/modules/antagonists/flockmind/components/flock_ping.dm
create mode 100644 code/modules/antagonists/flockmind/components/flock_protection.dm
create mode 100644 code/modules/antagonists/flockmind/flock_ai_controller.dm
create mode 100644 code/modules/antagonists/flockmind/flock_antagonist.dm
create mode 100644 code/modules/antagonists/flockmind/flock_camera/_flock_camera.dm
create mode 100644 code/modules/antagonists/flockmind/flock_camera/flock_overmind.dm
create mode 100644 code/modules/antagonists/flockmind/flock_camera/flocktrace.dm
create mode 100644 code/modules/antagonists/flockmind/flock_controller/_flock_controller.dm
create mode 100644 code/modules/antagonists/flockmind/flock_controller/flock_ui.dm
create mode 100644 code/modules/antagonists/flockmind/flock_convert_atom.dm
create mode 100644 code/modules/antagonists/flockmind/flock_effects.dm
create mode 100644 code/modules/antagonists/flockmind/flock_global_lists.dm
create mode 100644 code/modules/antagonists/flockmind/flock_hooks.dm
create mode 100644 code/modules/antagonists/flockmind/flock_items/flock_cache.dm
create mode 100644 code/modules/antagonists/flockmind/flock_items/flock_cube.dm
create mode 100644 code/modules/antagonists/flockmind/flock_language.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flock_mob.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockbit.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockdrone.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/absorber.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/converter.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm
create mode 100644 code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/incapacitator.dm
create mode 100644 code/modules/antagonists/flockmind/flock_objectives.dm
create mode 100644 code/modules/antagonists/flockmind/flock_say.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/_flock_structure.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_cage.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_chair.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_closet.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_collector.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_compute_node.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_door.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_egg.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_fabricator.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_grille.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_interceptor.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_lattice.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_light.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_relay.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_rift.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_sentinel.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_tealprint.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_turret.dm
create mode 100644 code/modules/antagonists/flockmind/flock_structure/flock_window.dm
create mode 100644 code/modules/antagonists/flockmind/flock_subtree.dm
create mode 100644 code/modules/antagonists/flockmind/flock_turfs/flock_floor.dm
create mode 100644 code/modules/antagonists/flockmind/flock_turfs/flock_wall.dm
create mode 100644 code/modules/antagonists/flockmind/flock_unlockable.dm
create mode 100644 code/modules/antagonists/flockmind/gnesis_reagent.dm
create mode 100644 code/modules/events/flockmind_event.dm
create mode 100644 code/modules/projectiles/projectile/flock_bolt.dm
create mode 100644 code/modules/tgui/states/flock_state.dm
create mode 100644 icons/goonstation/hud/flock_ui.dmi
create mode 100644 icons/goonstation/hud/flockmindcircuit.dmi
create mode 100644 icons/goonstation/mob/featherzone.dmi
create mode 100644 icons/goonstation/objects/featherzone-160x160.dmi
create mode 100644 icons/goonstation/objects/lighting.dmi
create mode 100644 icons/goonstation/turf/flock.dmi
create mode 100644 sound/goonstation/effects/radio_sweep1.ogg
create mode 100644 sound/goonstation/effects/radio_sweep2.ogg
create mode 100644 sound/goonstation/effects/radio_sweep3.ogg
create mode 100644 sound/goonstation/effects/radio_sweep4.ogg
create mode 100644 sound/goonstation/effects/radio_sweep5.ogg
create mode 100644 sound/goonstation/flockmind/ArtifactFea1.ogg
create mode 100644 sound/goonstation/flockmind/ArtifactFea2.ogg
create mode 100644 sound/goonstation/flockmind/ArtifactFea3.ogg
create mode 100644 sound/goonstation/flockmind/Flock_Reactor.ogg
create mode 100644 sound/goonstation/flockmind/click.ogg
create mode 100644 sound/goonstation/flockmind/deny.ogg
create mode 100644 sound/goonstation/flockmind/flock_broadcast_charge.ogg
create mode 100644 sound/goonstation/flockmind/flock_broadcast_kaboom.ogg
create mode 100644 sound/goonstation/flockmind/flock_sound.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp1.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp2.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp3.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp4.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp5.ogg
create mode 100644 sound/goonstation/flockmind/flockbit_wisp6.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_beep1.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_beep2.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_beep3.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_beep4.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_build.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_build_complete.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_convert.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_door.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_door_deny.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_fart.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_floorrun.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_grump1.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_grump2.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_grump3.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_locker_close.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_locker_open.ogg
create mode 100644 sound/goonstation/flockmind/flockdrone_quickbuild.ogg
create mode 100644 sound/goonstation/flockmind/flockmind_cast.ogg
create mode 100644 sound/goonstation/flockmind/flockmind_caw.ogg
create mode 100644 sound/goonstation/flockmind/flockmind_deathcry.ogg
create mode 100644 sound/goonstation/flockmind/hover.ogg
create mode 100644 sound/goonstation/flockmind/ping.ogg
create mode 100644 sound/goonstation/weapons/nano-blade-1.ogg
create mode 100644 strings/flock.json
create mode 100644 tgui/packages/tgui/interfaces/FlockPanel.tsx
diff --git a/_maps/map_files/stations/boxstation.dmm b/_maps/map_files/stations/boxstation.dmm
index 63f3ee55020..d4b23d57980 100644
--- a/_maps/map_files/stations/boxstation.dmm
+++ b/_maps/map_files/stations/boxstation.dmm
@@ -45581,6 +45581,7 @@
/obj/structure/chair/office/dark{
dir = 8
},
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server)
"ecJ" = (
@@ -48979,6 +48980,7 @@
c_tag = "Telecommunications Core";
dir = 4
},
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/telecomms/chamber)
"fAH" = (
@@ -55356,6 +55358,7 @@
/obj/effect/turf_decal/delivery/white/hollow,
/obj/effect/turf_decal/tiles/department/command,
/obj/machinery/ai_node/processing_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server/coldroom)
"iqM" = (
@@ -82024,6 +82027,7 @@
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
dir = 6
},
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/command/server)
"uhl" = (
@@ -85136,6 +85140,7 @@
/obj/effect/turf_decal/delivery/white/hollow,
/obj/effect/turf_decal/tiles/department/command,
/obj/machinery/ai_node/network_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server/coldroom)
"vJm" = (
diff --git a/_maps/map_files/stations/cerestation.dmm b/_maps/map_files/stations/cerestation.dmm
index 7f29f062e33..fab56cf76fc 100644
--- a/_maps/map_files/stations/cerestation.dmm
+++ b/_maps/map_files/stations/cerestation.dmm
@@ -34,6 +34,7 @@
dir = 6
},
/obj/machinery/ai_node/network_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark/telecomms{
icon_state = "bcircuit"
},
@@ -3422,6 +3423,7 @@
/obj/machinery/camera/autoname{
dir = 4
},
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel{
icon_state = "bcircuit"
},
@@ -30308,6 +30310,7 @@
icon_state = "4-8"
},
/obj/machinery/hologram/holopad,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server)
"fmI" = (
@@ -66815,6 +66818,7 @@
dir = 5
},
/obj/machinery/ai_node/processing_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark/telecomms{
icon_state = "bcircuit"
},
diff --git a/_maps/map_files/stations/deltastation.dmm b/_maps/map_files/stations/deltastation.dmm
index e843c0fc2f7..0d40760649a 100644
--- a/_maps/map_files/stations/deltastation.dmm
+++ b/_maps/map_files/stations/deltastation.dmm
@@ -27566,6 +27566,7 @@
/obj/structure/cable{
icon_state = "1-2"
},
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid,
/area/station/telecomms/chamber)
"cgw" = (
@@ -44022,6 +44023,7 @@
/area/station/science/server)
"dwk" = (
/obj/machinery/atmospherics/unary/vent_pump/on,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server)
"dwl" = (
@@ -44634,6 +44636,7 @@
},
/obj/machinery/light/small,
/obj/machinery/ai_node/network_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid{
icon_state = "gcircuit";
name = "Mainframe Floor";
@@ -44655,6 +44658,7 @@
dir = 9
},
/obj/machinery/ai_node/processing_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid{
icon_state = "gcircuit";
name = "Mainframe Floor";
diff --git a/_maps/map_files/stations/emeraldstation.dmm b/_maps/map_files/stations/emeraldstation.dmm
index 61eb3ce4239..c5989e00171 100644
--- a/_maps/map_files/stations/emeraldstation.dmm
+++ b/_maps/map_files/stations/emeraldstation.dmm
@@ -28608,6 +28608,7 @@
dir = 5
},
/obj/machinery/ai_node/processing_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server/coldroom)
"fHd" = (
@@ -38914,6 +38915,7 @@
"hJb" = (
/obj/structure/sign/poster/official/random/directional/east,
/obj/machinery/tcomms/core/station,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid{
icon_state = "gcircuit"
},
@@ -78818,6 +78820,7 @@
dir = 4
},
/obj/machinery/hologram/holopad,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server)
"pGM" = (
@@ -113511,6 +113514,7 @@
dir = 6
},
/obj/machinery/ai_node/network_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/plasteel/dark,
/area/station/science/server/coldroom)
"wAw" = (
diff --git a/_maps/map_files/stations/metastation.dmm b/_maps/map_files/stations/metastation.dmm
index 2e085478b87..742cccfc18e 100644
--- a/_maps/map_files/stations/metastation.dmm
+++ b/_maps/map_files/stations/metastation.dmm
@@ -36903,6 +36903,7 @@
icon_state = "1-2"
},
/obj/machinery/tcomms/core/station,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid,
/area/station/telecomms/chamber)
"gam" = (
@@ -38425,23 +38426,6 @@
/obj/effect/spawner/random/fungus/frequent,
/turf/simulated/wall,
/area/station/maintenance/fore2)
-"gBa" = (
-/obj/machinery/atmospherics/pipe/manifold/hidden{
- dir = 8
- },
-/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,
-/obj/machinery/atmospherics/pipe/manifold/hidden/supply{
- dir = 4
- },
-/obj/structure/cable{
- icon_state = "2-8"
- },
-/obj/structure/cable{
- icon_state = "1-2"
- },
-/obj/machinery/light/floor,
-/turf/simulated/floor/plasteel/dark,
-/area/station/science/server)
"gBj" = (
/obj/machinery/atmospherics/pipe/simple/hidden/supply{
dir = 4
@@ -40308,20 +40292,6 @@
},
/turf/simulated/floor/plating,
/area/station/maintenance/aft)
-"hdR" = (
-/obj/machinery/atmospherics/pipe/simple/heat_exchanging{
- dir = 10
- },
-/obj/machinery/firealarm/directional/east,
-/obj/effect/turf_decal/siding{
- dir = 8
- },
-/obj/machinery/ai_node/processing_node,
-/obj/machinery/light{
- dir = 1
- },
-/turf/simulated/floor/bluegrid/telecomms/server,
-/area/station/science/server/coldroom)
"hdX" = (
/obj/structure/cable{
icon_state = "4-8"
@@ -58367,6 +58337,24 @@
/obj/effect/turf_decal/tiles/dark/checker,
/turf/simulated/floor/plasteel,
/area/station/medical/break_room)
+"mXX" = (
+/obj/machinery/atmospherics/pipe/manifold/hidden{
+ dir = 8
+ },
+/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,
+/obj/machinery/atmospherics/pipe/manifold/hidden/supply{
+ dir = 4
+ },
+/obj/structure/cable{
+ icon_state = "2-8"
+ },
+/obj/structure/cable{
+ icon_state = "1-2"
+ },
+/obj/effect/landmark/spawner/flock,
+/obj/machinery/light/floor,
+/turf/simulated/floor/plasteel/dark,
+/area/station/science/server)
"mYa" = (
/obj/structure/grille/broken,
/obj/effect/mapping_helpers/turfs/damage,
@@ -68606,6 +68594,7 @@
dir = 4
},
/obj/machinery/ai_node/network_node,
+/obj/effect/landmark/spawner/flock,
/turf/simulated/floor/bluegrid/telecomms/server,
/area/station/science/server/coldroom)
"quh" = (
@@ -68908,6 +68897,21 @@
/obj/effect/spawner/random/barrier/grille_maybe,
/turf/simulated/floor/plating,
/area/station/maintenance/fore)
+"qyJ" = (
+/obj/machinery/firealarm/directional/east,
+/obj/machinery/ai_node/processing_node,
+/obj/effect/landmark/spawner/flock,
+/obj/machinery/atmospherics/pipe/simple/heat_exchanging{
+ dir = 10
+ },
+/obj/effect/turf_decal/siding{
+ dir = 8
+ },
+/obj/machinery/light{
+ dir = 1
+ },
+/turf/simulated/floor/bluegrid/telecomms/server,
+/area/station/science/server/coldroom)
"qyM" = (
/obj/effect/decal/cleanable/dirt,
/obj/effect/spawner/random/barrier/grille_often,
@@ -128405,7 +128409,7 @@ cmB
cbf
cyS
mHT
-gBa
+mXX
mdH
apS
kDU
@@ -128659,7 +128663,7 @@ cte
cte
cte
cmB
-hdR
+qyJ
xcw
cyP
dcs
diff --git a/code/__DEFINES/ai/ai_defines.dm b/code/__DEFINES/ai/ai_defines.dm
index 17553cb0f92..fcce754ebc6 100644
--- a/code/__DEFINES/ai/ai_defines.dm
+++ b/code/__DEFINES/ai/ai_defines.dm
@@ -3,6 +3,13 @@
#define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type)
//AI controller flags
+
+#ifdef DEBUG_AI
+#define DEBUG_AI_LOG(controller, message) if(controller.debug_focus) { to_chat(world, span_debug("AI DBG: [message]")) }
+#else
+#define DEBUG_AI_LOG(...)
+#endif
+
//If you add a new status, be sure to add it to the ai_controllers subsystem's ai_controllers_by_status list.
/// The AI is currently active.
#define AI_STATUS_ON "ai_on"
@@ -51,6 +58,9 @@
/// Does this behavior NOT block planning?
#define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<4)
+/// Magic value to tell the G.O.A.P selector to ignore a behavior.
+#define AI_GOAP_SKIP_BEHAVIOR (-1000001)
+
// AI flags
/// Don't move if being pulled
diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm
index 1ce03481c1e..49023ce6419 100644
--- a/code/__DEFINES/ai/blackboard_defines.dm
+++ b/code/__DEFINES/ai/blackboard_defines.dm
@@ -22,6 +22,10 @@
#define BB_BASIC_MOB_FLEE_DISTANCE "BB_BASIC_FLEE_DISTANCE"
#define DEFAULT_BASIC_FLEE_DISTANCE 9
+/// Path we should use next time we use the JPS movement datum
+#define BB_PATH_TO_USE "BB_path_to_use"
+/// Max path length we should use next time we use the JPS movement datum
+#define BB_PATH_MAX_LENGTH "BB_path_max_length"
// Searching
/// key holding a range to look for stuff in
diff --git a/code/__DEFINES/ai/flockmind_ai_defines.dm b/code/__DEFINES/ai/flockmind_ai_defines.dm
new file mode 100644
index 00000000000..3c1b059603f
--- /dev/null
+++ b/code/__DEFINES/ai/flockmind_ai_defines.dm
@@ -0,0 +1,22 @@
+// Flockmind Keys]
+#define BB_FLOCK_STARE_TARGET "BB_flock_stare_target"
+#define BB_FLOCK_WANDERING "BB_flock_wandering"
+#define BB_FLOCK_STARE_CD "BB_flock_stare_cooldown"
+#define BB_FLOCK_CONVERT_TARGET "BB_flock_convert_target"
+#define BB_FLOCK_DECON_TARGET "BB_flock_decon_target"
+#define BB_FLOCK_HARVEST_TARGET "BB_flock_harvest_target"
+#define BB_FLOCK_WANDER_FRUSTRATION "BB_flock_wander_frustration"
+#define BB_FLOCK_HEAL_TARGET "BB_flock_heal_target"
+#define BB_FLOCK_HEAL_FRUSTRATION "BB_flock_heal_frustation"
+#define BB_FLOCK_REPLICATE_TARGET "BB_flock_replicate_target"
+#define BB_FLOCK_NEST_TARGET "BB_flock_nest_target"
+#define BB_FLOCK_OVERMIND_CONTROL "BB_flock_overmind_control"
+#define BB_FLOCK_CONTAINER_TARGET "BB_flock_container_target"
+#define BB_FLOCK_RUMMAGE_TARGET "BB_flock_rummage_target"
+#define BB_FLOCK_CAPTURE_TARGET "BB_flock_capture_target"
+#define BB_FLOCK_ATTACK_TARGET "BB_flock_attack_target"
+#define BB_FLOCK_ATTACK_COOLDOWN "BB_flock_attack_cooldown"
+#define BB_FLOCK_ATTACK_RUN_COOLDOWN "BB_flock_attack_run_cooldown"
+#define BB_FLOCK_ATTACK_STRAFE_COOLDOWN "BB_flock_attack_strafe_cooldown"
+#define BB_FLOCK_ATTACK_FRUSTRATION "BB_flock_attack_frustration"
+#define BB_FLOCK_DEPOSIT_TARGET "BB_flock_deposit_target"
diff --git a/code/__DEFINES/color_defines.dm b/code/__DEFINES/color_defines.dm
index 59509bfc8c7..6bfff8c65da 100644
--- a/code/__DEFINES/color_defines.dm
+++ b/code/__DEFINES/color_defines.dm
@@ -120,6 +120,7 @@
#define COLOR_BLOOD_BASE "#A10808"
#define COLOR_BLOOD_MACHINE "#1F181F"
+#define COLOR_BLOOD_FLOCK "#4d736d"
#define COLOR_BLOOD_XENO "#05EE05"
// Pipe colours
diff --git a/code/__DEFINES/construction_defines.dm b/code/__DEFINES/construction_defines.dm
index a1bac1272dc..7c899bc12b5 100644
--- a/code/__DEFINES/construction_defines.dm
+++ b/code/__DEFINES/construction_defines.dm
@@ -85,6 +85,8 @@
#define MAT_IRIDIUM "iridium"
#define MAT_SILK "silk"
#define MAT_CARDBOARD "cardboard"
+#define MAT_GNESIS "gnesis"
+#define MAT_GNESIS_GLASS "gnesisglass"
/// The amount of materials you get from a sheet of mineral like iron/diamond/glass etc
#define MINERAL_MATERIAL_AMOUNT 2000
diff --git a/code/__DEFINES/dcs/ai_signals.dm b/code/__DEFINES/dcs/ai_signals.dm
index 66e44037e3d..5e8d5f47768 100644
--- a/code/__DEFINES/dcs/ai_signals.dm
+++ b/code/__DEFINES/dcs/ai_signals.dm
@@ -20,3 +20,6 @@
#define COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS "ai_controller_picked_behaviors"
///sent from ai controllers when they stop possessing a pawn: (datum/ai_controller/source_controller)
#define COMSIG_AI_CONTROLLER_UNPOSSESSED_PAWN "ai_controller_unpossessed_pawn"
+
+/// Signal sent when a mob's AI turns on or off.
+#define COMSIG_AI_STATUS_CHANGE "ai_status_change"
diff --git a/code/__DEFINES/dcs/moveloop_signals.dm b/code/__DEFINES/dcs/moveloop_signals.dm
index c93d9325183..e2b69d1d12b 100644
--- a/code/__DEFINES/dcs/moveloop_signals.dm
+++ b/code/__DEFINES/dcs/moveloop_signals.dm
@@ -9,6 +9,8 @@
#define COMSIG_MOVELOOP_POSTPROCESS "moveloop_postprocess"
//from [/datum/move_loop/has_target/jps/recalculate_path] ():
#define COMSIG_MOVELOOP_JPS_REPATH "moveloop_jps_repath"
+//from [/datum/move_loop/has_target/astar/recalculate_path] ():
+#define COMSIG_MOVELOOP_ASTAR_REPATH "moveloop_astar_repath"
///From base of /datum/move_loop/process() after attempting to move a movable: (datum/move_loop/loop, old_dir)
#define COMSIG_MOVABLE_MOVED_FROM_LOOP "movable_moved_from_loop"
///from [/datum/move_loop/has_target/jps/on_finish_pathing]
diff --git a/code/__DEFINES/dcs/signals_flock.dm b/code/__DEFINES/dcs/signals_flock.dm
new file mode 100644
index 00000000000..b87ec81ba5c
--- /dev/null
+++ b/code/__DEFINES/dcs/signals_flock.dm
@@ -0,0 +1,4 @@
+#define COMSIG_FLOCK_PROTECTION_TRIGGER "comsig_flock_protection_trigger"
+ #define COMPONENT_FLOCK_DENY_ATTACK (1<<0)
+
+#define COMSIG_TURF_CLAIMED_BY_FLOCK "comsig_turf_claimed_by_flock"
diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index 5e203d399e1..a2328aaa4fd 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -148,6 +148,8 @@
#define PASSGIRDER (1<<8)
#define PASSTAKE (1<<9)
#define PASSBARRICADE (1<<10)
+/// Pass through flock objects and mobs
+#define PASSFLOCK (1<<11)
//turf-only flags, under the flags variable
#define BLESSED_TILE (1<<0)
@@ -185,8 +187,9 @@
#define ZAP_MOB_DAMAGE (1<<3)
#define ZAP_MOB_STUN (1<<4)
#define ZAP_GENERATES_POWER (1<<5)
+#define ZAP_NO_COOLDOWN (1<<6)
-#define ZAP_DEFAULT_FLAGS (ZAP_MOB_STUN | ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE)
+#define ZAP_DEFAULT_FLAGS (ZAP_MOB_STUN | ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_NO_COOLDOWN)
#define ZAP_FUSION_FLAGS (ZAP_OBJ_DAMAGE | ZAP_MOB_DAMAGE | ZAP_MOB_STUN)
#define ZAP_SUPERMATTER_FLAGS (ZAP_GENERATES_POWER)
diff --git a/code/__DEFINES/flockmind_defines.dm b/code/__DEFINES/flockmind_defines.dm
new file mode 100644
index 00000000000..823757e62cb
--- /dev/null
+++ b/code/__DEFINES/flockmind_defines.dm
@@ -0,0 +1,70 @@
+#define FLOCK_TYPE_OVERMIND "overmind"
+#define FLOCK_TYPE_TRACE "trace"
+#define FLOCK_TYPE_DRONE "drone"
+#define FLOCK_TYPE_BIT "bit"
+
+// Turf reserved by a flockdrone
+#define FLOCK_NOTICE_RESERVED "reserved"
+// Turf marked for conversion
+#define FLOCK_NOTICE_PRIORITY "priority"
+// Mob marked as enemy
+#define FLOCK_NOTICE_ENEMY "enemy"
+// Mob marked as ignored
+#define FLOCK_NOTICE_IGNORE "ignore"
+// Flockdrone under flocktrace control
+#define FLOCK_NOTICE_FLOCKTRACE_CONTROL "flocktrace_control"
+// Flockdrone under overmind control
+#define FLOCK_NOTICE_FLOCKMIND_CONTROL "flockmind_control"
+// Flockdrone health bars
+#define FLOCK_NOTICE_HEALTH "flock_health"
+// Atom marked for deconstruction
+#define FLOCK_NOTICE_DECONSTRUCT "flock_deconstruct"
+
+#define FLOCK_UI_DRONES "drones"
+#define FLOCK_UI_TRACES "traces"
+#define FLOCK_UI_ENEMIES "enemies"
+#define FLOCK_UI_STRUCTURES "structures"
+
+#define FLOCK_COMPUTE_COST_FLOCKTRACE 100
+#define FLOCK_COMPUTE_COST_DRONE 10
+#define FLOCK_COMPUTE_COST_RELAY 750
+
+#define FLOCK_TURFS_FOR_RELAY 250
+
+/// Amount of substrate to add to a tealprint.
+#define FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT 10
+/// Amount to convert a turf and it's contents.
+#define FLOCK_SUBSTRATE_COST_CONVERT 20
+/// Amount to repair a flock construct.
+#define FLOCK_SUBSTRATE_COST_REPAIR 10
+/// BASE amount to lay an egg.
+#define FLOCK_SUBSTRATE_COST_LAY_EGG 100
+
+/// Egg cost does not start scaling until there are this many drones.
+#define FLOCK_MIN_DESIRED_POP 10
+/// Each drone above the min desired pop adds this much to the substrate required to be able to lay an egg.
+#define FLOCK_ADDITIONAL_RESOURCE_RESERVATION_PER_DRONE 8
+#define FLOCK_DRONE_LIMIT 50
+
+#define FLOCK_ENDGAME_LOST 1
+#define FLOCK_ENDGAME_RELAY_BUILT 2
+#define FLOCK_ENDGAME_RELAY_ACTIVATING 3
+#define FLOCK_ENDGAME_VICTORY 4
+
+/// The list of behaviors for the planners to select from. THIS IS ACTUALLY A SUBKEY!
+#define BB_PLANNER_BEHAVIORS "BB_planner_behaviors"
+
+// G.O.A.P weights for flock behaviors, for easy comparison/adjustment.
+#define FLOCK_BEHAVIOR_WEIGHT_WANDER 1
+#define FLOCK_BEHAVIOR_WEIGHT_STARE 1
+#define FLOCK_BEHAVIOR_WEIGHT_CONVERT 1
+#define FLOCK_BEHAVIOR_WEIGHT_HARVEST 2
+#define FLOCK_BEHAVIOR_WEIGHT_OPEN_CONTAINER 3
+#define FLOCK_BEHAVIOR_WEIGHT_RUMMAGE 3
+#define FLOCK_BEHAVIOR_WEIGHT_REPAIR 4
+#define FLOCK_BEHAVIOR_WEIGHT_NEST 6
+#define FLOCK_BEHAVIOR_WEIGHT_REPLICATE 7
+#define FLOCK_BEHAVIOR_WEIGHT_DECONSTRUCT 8
+#define FLOCK_BEHAVIOR_WEIGHT_DEPOSIT 8
+#define FLOCK_BEHAVIOR_WEIGHT_SHOOT 10
+#define FLOCK_BEHAVIOR_WEIGHT_CAPTURE 15
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index f5b94829d6a..8d58e72aec6 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -67,6 +67,7 @@
#define SPECIAL_ROLE_HERETIC "Heretic"
#define SPECIAL_ROLE_EVENTMISC "Event Role"
#define SPECIAL_ROLE_NINJA "Space Ninja"
+#define SPECIAL_ROLE_FLOCK "Divine Flock"
#define SPECIAL_ROLE_IRRADIATED_MOUSE "Irradiated Mouse"
// Constants used by code which checks the status of nuclear blasts during a
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index a650d6fd978..b47118223d3 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -31,6 +31,22 @@
#define isalienqueen(A) (istype(A, /mob/living/carbon/alien/humanoid/queen))
+#define isaicamera(A) (istype(A, /mob/camera/ai_eye))
+
+#define isflockmob(A) (istype(A, /mob/camera/flock) || istype(A, /mob/living/basic/flock))
+
+#define isflockdrone(A) (istype(A, /mob/living/basic/flock/drone))
+
+#define isflockbit(A) (istype(A, /mob/living/basic/flock/bit))
+
+#define isflocktrace(A) (istype(A, /mob/camera/flock/trace))
+
+#define isflockmind(A) (istype(A, /mob/camera/flock/overmind))
+
+#define isflockcontroller(A) (isflockmind(A) || isflocktrace(A))
+
+#define isflockworker(A) (isflockdrone(A) || isflockbit(A))
+
// Simple animals
#define isanimal(A) (istype(A, /mob/living/simple_animal))
@@ -134,6 +150,8 @@ GLOBAL_LIST_INIT(glass_sheet_types, typecacheof(list(
#define is_ancient_rock(A) (istype(A, /turf/simulated/mineral/ancient))
+#define isflockturf(A) (istype(A, /turf/simulated/floor/flock) || istype(A, /turf/simulated/wall/flock))
+
// Areas
//#define isarea(A, B, C...) BYOND proc, can test multiple arguments and only return TRUE if all are areas
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 6ffdcaa0bd8..1b9896f6496 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -133,6 +133,8 @@
#define FLOOR_OPENSPACE_PLANE 22
#define OPENSPACE_LAYER 22
+#define INFO_TAG_PLANE 23
+
#define BYOND_LIGHTING_PLANE 24
#define BYOND_LIGHTING_LAYER 24
diff --git a/code/__DEFINES/mob_defines.dm b/code/__DEFINES/mob_defines.dm
index 1fbb07ce255..a7e2513ad5b 100644
--- a/code/__DEFINES/mob_defines.dm
+++ b/code/__DEFINES/mob_defines.dm
@@ -190,6 +190,8 @@
#define SHOCK_ILLUSION (1<<2)
///The shock doesn't stun.
#define SHOCK_NOSTUN (1<<3)
+/// Shock damage is reduced by the average siemen's coeff
+#define SHOCK_USE_AVG_SIEMENS (1 << 4)
#define POCKET_STRIP_DELAY 4 SECONDS //time taken to search somebody's pockets
@@ -233,6 +235,9 @@
#define DIRECTION_LOCK_SLOWDOWN 3
+// Helpers
+#define DOING_INTERACTION(user, interaction_key) (LAZYACCESS(user.do_afters, interaction_key))
+
//Human sub-species
#define isabductor(A) (is_species(A, /datum/species/abductor))
#define isgolem(A) (is_species(A, /datum/species/golem))
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 7c178907bd8..4ae5be6858d 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -46,6 +46,7 @@
#define ROLE_GHOST "ghost role"
#define ROLE_ELITE "lavaland elite"
#define ROLE_NINJA "ninja"
+#define ROLE_FLOCK "flockmind"
#define ROLE_IRRADIATED_MOUSE "irradiated mouse"
#define ROLE_UPLIFTED_PRIMITIVE "uplifted primitive"
@@ -78,6 +79,7 @@ GLOBAL_LIST_INIT(special_roles_antags, list(
ROLE_IRRADIATED_MOUSE,
ROLE_UPLIFTED_PRIMITIVE,
+ ROLE_FLOCK,
// UNUSED/BROKEN ANTAGS
// ROLE_HOG_GOD = /datum/game_mode/hand_of_god,
// ROLE_HOG_CULTIST = /datum/game_mode/hand_of_god,
diff --git a/code/__DEFINES/rolebans.dm b/code/__DEFINES/rolebans.dm
index 62f6b39b497..287b2d8c5e5 100644
--- a/code/__DEFINES/rolebans.dm
+++ b/code/__DEFINES/rolebans.dm
@@ -15,6 +15,7 @@ GLOBAL_LIST_INIT(antag_roles, list(
ROLE_MORPH,
ROLE_TSPIDER,
ROLE_NINJA,
+ ROLE_FLOCK,
))
// Bannable other roles
diff --git a/code/__DEFINES/sight.dm b/code/__DEFINES/sight.dm
index 2c65397f364..b1d45bf917d 100644
--- a/code/__DEFINES/sight.dm
+++ b/code/__DEFINES/sight.dm
@@ -15,6 +15,9 @@
#define INVISIBILITY_HIDDEN_RUNES 30
#define SEE_INVISIBLE_HIDDEN_RUNES 30
+#define INVISIBILITY_FLOCK 30
+#define SEE_INVISIBLE_FLOCK 30
+
#define SEE_INVISIBLE_LEVEL_ONE 35 //Used by some stuff in code. It's really poorly organized.
#define SEE_INVISIBLE_LEVEL_TWO 45 //Used by some other stuff in code. It's really poorly organized.
diff --git a/code/__DEFINES/span_defines.dm b/code/__DEFINES/span_defines.dm
index 8f0a23b3e52..ebda57b116b 100644
--- a/code/__DEFINES/span_defines.dm
+++ b/code/__DEFINES/span_defines.dm
@@ -130,3 +130,5 @@
#define SPAN_WHISPER(str) ("" + str + "")
#define SPAN_ZOMBIE(str) ("" + str + "")
#define SPAN_ZOMBIELARGE(str) ("" + str + "")
+#define SPAN_FLOCKSAY(str) ("" + str + "")
+#define SPAN_FLOCKSAY_GRADIENT(str) ("[gradient_text(str, "#3cb5a3", "#1e806e")]")
diff --git a/code/__HELPERS/flock_helpers.dm b/code/__HELPERS/flock_helpers.dm
new file mode 100644
index 00000000000..1e9c0c46c22
--- /dev/null
+++ b/code/__HELPERS/flock_helpers.dm
@@ -0,0 +1,118 @@
+/// A copy of is_blocked_turf(), ignoring flock mobs
+/turf/proc/can_flock_occupy(atom/source_atom)
+ if(density)
+ return FALSE
+
+ for(var/atom/movable/movable_content as anything in contents)
+ // We don't want to block ourselves or consider any ignored atoms.
+ if((movable_content == source_atom) || isflockmob(movable_content))
+ continue
+ // If the thing is dense AND we're including mobs or the thing isn't a mob AND if there's a source atom and
+ // it cannot pass through the thing on the turf, we consider the turf blocked.
+ if(movable_content.density)
+ if(source_atom && movable_content.CanPass(source_atom, get_dir(src, source_atom)))
+ continue
+ return FALSE
+
+ return TRUE
+
+/proc/pointer_image_to(atom/from, atom/towards, color = "#00ff9dff")
+ var/image/pointer = image(icon = 'icons/mob/screen_gen.dmi', icon_state = "arrow_greyscale", loc = from)
+
+ pointer.plane = HUD_PLANE
+ pointer.appearance_flags |= RESET_COLOR
+ pointer.color = color
+
+ var/angle = 180 + get_angle(from, towards)
+ var/matrix/final_matrix = pointer.transform.Scale(2,2)
+ final_matrix = final_matrix.Turn(angle)
+ pointer.transform = final_matrix
+
+ pointer.pixel_x = sin(angle) * -48
+ pointer.pixel_y = cos(angle) * -48
+ return pointer
+
+/proc/flock_realname(flock_type)
+ var/attempts = 0
+ var/name = ""
+ do
+ attempts++
+
+ switch(flock_type)
+ if(FLOCK_TYPE_OVERMIND)
+ name = "\proper[pick(GLOB.consonants_lower)][pick(GLOB.vowels_lower)].[pick(GLOB.consonants_lower)][pick(GLOB.vowels_lower)]"
+ if(FLOCK_TYPE_TRACE)
+ name = "[pick(GLOB.consonants_upper)][pick(GLOB.vowels_lower)].[pick(GLOB.vowels_lower)]"
+ if(FLOCK_TYPE_DRONE)
+ name = "\proper[pick(GLOB.consonants_lower)][pick(GLOB.vowels_lower)].[pick(GLOB.consonants_lower)][pick(GLOB.vowels_lower)].[pick(GLOB.consonants_lower)][pick(GLOB.vowels_lower)]"
+ if(FLOCK_TYPE_BIT)
+ name = "[pick(GLOB.consonants_upper)].[rand(10,99)].[rand(10,99)]"
+
+ while(attempts <= 10 && findname(name))
+
+ return name
+
+/proc/flock_name(flock_type)
+ var/attempts = 0
+ var/name = ""
+ do
+ attempts++
+
+ switch(flock_type)
+ if(FLOCK_TYPE_DRONE)
+ name = "[pick(GLOB.flockdrone_name_adjectives)] [pick(GLOB.flockdrone_name_nouns)]"
+ if(FLOCK_TYPE_BIT)
+ name = "[pick(GLOB.consonants_upper)].[rand(10,99)].[rand(10,99)]"
+
+ while(attempts <= 10 && findname(name))
+
+ return name
+
+/proc/animate_flockpass(atom/thing)
+ var/list/color_matrix = list(1,0,0, 0,1,0, 0,0,1, 0.15,0.77,0.66)
+ var/matrix/shrink = thing.transform.Scale(0.4)
+ var/old_transform = thing.transform
+
+ animate(thing, color = color_matrix, transform = shrink, time = 3, easing=BOUNCE_EASING)
+ animate(color = null, transform = old_transform, time = 3, easing=BOUNCE_EASING)
+
+/// Make a color a repeating gradient between two colors. Note: This is inaccurate because its a linear transformation, but human eyes do not perceive color this way.
+/proc/gradient_text(message, color_1, color_2)
+ var/list/color_list_1 = rgb2num(color_1)
+ var/list/color_list_2 = rgb2num(color_2)
+
+ var/r1 = color_list_1[1]
+ var/g1 = color_list_1[2]
+ var/b1 = color_list_1[3]
+
+ // The difference in value between each color part
+ var/delta_r = color_list_2[1] - r1
+ var/delta_g = color_list_2[2] - g1
+ var/delta_b = color_list_2[3] - b1
+
+ var/list/result = list()
+
+ // Start at a random point between the two, in increments of 0.1
+ var/coeff = rand(0,10) / 10.0
+ var/dir = prob(50) ? -1 : 1
+
+ for(var/i in 1 to length(message) step 3)
+ coeff += dir * 0.2
+ // 20% chance to start going in the opposite direction
+ if(prob(20))
+ dir = -dir
+
+ // Wrap back around
+ if(coeff < 0)
+ coeff = 0
+ dir = 1
+
+ else if(coeff > 1)
+ coeff = 1
+ dir = -1
+
+ var/col = rgb(r1 + delta_r*coeff, g1 + delta_g*coeff, b1 + delta_b*coeff)
+ var/chars = copytext(message, i, i + 3)
+ result += "[chars]"
+
+ . = jointext(result, "")
diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm
index f942c0d87e9..20b72108441 100644
--- a/code/__HELPERS/lists.dm
+++ b/code/__HELPERS/lists.dm
@@ -49,6 +49,33 @@
};\
} while(FALSE)
+/// The above but the tree is sorted in reverse.
+#define BINARY_INSERT_REVERSE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \
+ do {\
+ var/list/__BIN_LIST = LIST;\
+ var/__BIN_CTTL = length(__BIN_LIST);\
+ if(!__BIN_CTTL) {\
+ __BIN_LIST += INPUT;\
+ } else {\
+ var/__BIN_LEFT = 1;\
+ var/__BIN_RIGHT = __BIN_CTTL;\
+ var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
+ var ##TYPECONT/__BIN_ITEM;\
+ while(__BIN_LEFT < __BIN_RIGHT) {\
+ __BIN_ITEM = COMPTYPE;\
+ if(__BIN_ITEM.##COMPARISON >= COMPARE.##COMPARISON) {\
+ __BIN_LEFT = __BIN_MID + 1;\
+ } else {\
+ __BIN_RIGHT = __BIN_MID;\
+ };\
+ __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
+ };\
+ __BIN_ITEM = COMPTYPE;\
+ __BIN_MID = __BIN_ITEM.##COMPARISON < COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\
+ __BIN_LIST.Insert(__BIN_MID, INPUT);\
+ };\
+ } while(FALSE)
+
#define SORT_FIRST_INDEX(list) (list[1])
#define SORT_COMPARE_DIRECTLY(thing) (thing)
#define SORT_VAR_NO_TYPE(varname) var/varname
diff --git a/code/__HELPERS/paths/astar.dm b/code/__HELPERS/paths/astar.dm
new file mode 100644
index 00000000000..6b215c58cb5
--- /dev/null
+++ b/code/__HELPERS/paths/astar.dm
@@ -0,0 +1,276 @@
+#define ASTAR_NODE(turf, dist_from_start, heuristic, prev_node) list(turf, dist_from_start + heuristic, dist_from_start, heuristic, prev_node)
+#define ASTAR_CLOSE_ENOUGH_TO_END(end, checking_turf) (end == checking_turf || (mintargetdist && (get_dist(checking_turf, end) <= mintargetdist)))
+
+/datum/astar_node
+ var/turf/turf
+ var/total_cost_f
+ var/dist_from_start_g
+ /// Distance_g is affected by bias to smooth out diagonals, this tracks the raw number of steps required.
+ var/real_dist_from_start
+ var/heuristic_h
+
+ var/datum/astar_node/prev_node
+
+/datum/pathfind/astar
+ /// The thing that we're actually trying to path for
+ var/atom/movable/invoker
+ /// The turf we're trying to path to (note that this won't track a moving target)
+ var/turf/end
+ /// The list we compile at the end if successful to pass back
+ var/list/path
+
+ /// A k:v list of turf -> directions. The directions are directions the pathfinder attempted to step into the turf but failed.
+ var/list/closed
+ /// A binary search tree containing the discovered nodes.
+ var/list/open_binary_tree
+ /// A k:V list of turf -> astar node
+ var/list/open_turf_to_node
+
+ /// How far away we have to get to the end target before we can call it quits
+ var/mintargetdist = 0
+ /// If we should delete the first step in the path or not. Used often because it is just the starting tile
+ var/skip_first = FALSE
+ /// Defines how we handle diagonal moves. See __DEFINES/path.dm
+ var/use_diagonals = TRUE
+ /// An optional callback to invoke to return a positive value to add to the path's distance.
+ var/datum/callback/heuristic
+
+#ifdef DEBUG_PATHFINDING
+ /// List of all nodes we've parsed, used for debug spew.
+ var/list/all_nodes_ever = list()
+#endif
+
+
+/datum/pathfind/astar/New(
+ atom/movable/invoker,
+ atom/goal,
+ access,
+ max_steps,
+ mintargetdist,
+ simulated_only,
+ avoid,
+ skip_first,
+ use_diagonals,
+ datum/callback/on_finish,
+ datum/callback/heuristic,
+)
+ src.invoker = invoker
+ src.pass_info = new(invoker, access)
+
+ end = get_turf(goal)
+ open_binary_tree = new()
+ open_turf_to_node = new()
+ closed = new()
+
+ src.max_distance = max_steps
+ src.mintargetdist = mintargetdist
+ src.simulated_only = simulated_only
+ src.avoid = avoid
+ src.skip_first = skip_first
+ src.use_diagonals = use_diagonals
+ src.on_finish = on_finish
+ src.heuristic = heuristic || CALLBACK(src, PROC_REF(generic_heuristic))
+
+/datum/pathfind/astar/Destroy(force, ...)
+ . = ..()
+ invoker = null
+ end = null
+ open_binary_tree = null
+ open_turf_to_node = null
+ closed = null
+ heuristic = null // hard del generator if using generic_heuristic
+
+/**
+ * "starts" off the pathfinding, by storing the values this datum will need to work later on
+ * returns FALSE if it fails to setup properly, TRUE otherwise
+ */
+/datum/pathfind/astar/start()
+ start ||= get_turf(invoker)
+ . = ..()
+ if(!.)
+ return .
+
+ if(!get_turf(end))
+ stack_trace("Invalid A* destination")
+ return FALSE
+
+ if(start.z != end.z || start == end) //no pathfinding between z levels
+ return FALSE
+
+ // If the turf is out of the step range we already know it's too far.
+ if(max_distance && (max_distance < get_dist_manhattan(start, end)))
+ return FALSE
+
+ var/datum/astar_node/start_node = new /datum/astar_node()
+ start_node.turf = start
+ start_node.total_cost_f = 0
+ start_node.dist_from_start_g = 0
+ start_node.heuristic_h = 0
+ start_node.real_dist_from_start = 0
+
+ open_turf_to_node[start] = start_node
+ binary_insert_node(start_node)
+ return TRUE
+
+/**
+ * Cleanup pass for the pathfinder. This tidies up the path, and fufills the pathfind's obligations
+ */
+/datum/pathfind/astar/finished()
+ var/list/path = src.path || list()
+ if(length(path) > 0 && skip_first)
+ path.Cut(1,2)
+
+ hand_back(path)
+ #ifdef DEBUG_PATHFINDING
+ /// If the global flag is set, replace the current global node spew.
+ if(GLOB.__pathfinding_debug_generate)
+ GLOB.__pathfinding_debug_info = all_nodes_ever
+ #endif
+ return ..()
+
+/**
+ * search_step() is the workhorse of pathfinding. It'll do the searching logic, and will slowly build up a path
+ * returns TRUE if everything is stable, FALSE if the pathfinding logic has failed, and we need to abort
+ */
+/datum/pathfind/astar/search_step(tick_check = TRUE)
+ . = ..()
+ if(!.)
+ return .
+
+ if(QDELETED(invoker))
+ return FALSE
+
+ var/static/list/lateral_search_dirs = list(EAST, WEST, NORTH, SOUTH)
+ var/static/list/all_search_dirs = list(EAST, WEST, NORTH, SOUTH, NORTHEAST, SOUTHWEST, NORTHWEST, SOUTHEAST)
+
+ while(length(open_binary_tree) && !path)
+ var/datum/astar_node/current_node = open_binary_tree[length(open_binary_tree)]
+ open_binary_tree.len--
+
+ var/turf/current_node_turf = current_node.turf
+ closed[current_node_turf] = ALL
+
+ if(max_distance && current_node.real_dist_from_start > max_distance)
+ continue
+
+ // Check to see if we're close enough to the end destination.
+ if(ASTAR_CLOSE_ENOUGH_TO_END(end, current_node_turf))
+ unwind_path(current_node)
+ return TRUE
+
+ // Scan cardinal turfs for valid movements.
+ for(var/scan_direction in use_diagonals ? all_search_dirs : lateral_search_dirs)
+ var/turf/searching_turf = get_step(current_node_turf, scan_direction)
+ var/IS_DIR_DIAGONAL = IS_DIR_DIAGONAL(scan_direction)
+ if(closed[searching_turf] & scan_direction)
+ continue // Turf is known to be blocked from this direction, skip!
+
+ if(!(IS_DIR_DIAGONAL ? can_step_diagonal(current_node_turf, searching_turf) : CAN_STEP(current_node_turf, searching_turf, simulated_only, pass_info, avoid)))
+ closed[searching_turf] |= scan_direction
+ continue // Turf cannot be entered, atleast from this direction. Skip!
+
+ // At this point we consider this turf a valid node.
+
+ var/datum/astar_node/existing_node = open_turf_to_node[searching_turf]
+
+ // Prefer straighter lines for more visual appeal. Penalize changing from cardinal to diagonal, but if you're already diagonal, it's okay.
+ var/distance_g = current_node.dist_from_start_g
+ var/real_distance = current_node.real_dist_from_start
+ if(IS_DIR_DIAGONAL)
+ // Diagonal is not continuing from previous node
+ if(!current_node.prev_node || !IS_DIR_DIAGONAL(get_dir(current_node.prev_node.turf, current_node_turf)))
+ distance_g += 2
+ real_distance += 2
+
+ // Diagonal is continuing from previous node
+ else
+ distance_g += sqrt(2) // It const folds dont cry
+ real_distance += 2
+ else
+ distance_g += 1
+ real_distance += 1
+
+ // If the node already exists, update it to reflect new information. Maybe we found a shorter path to it, or similar.
+ if(existing_node)
+ if(distance_g < existing_node.dist_from_start_g)
+ existing_node.prev_node = current_node
+ existing_node.dist_from_start_g = distance_g
+ existing_node.real_dist_from_start = real_distance
+ existing_node.total_cost_f = distance_g + existing_node.heuristic_h
+ open_binary_tree -= existing_node
+ binary_insert_node(existing_node)
+ continue
+
+ // The node isn't known to us so we need to check the heuristic.
+ var/heuristic_h = heuristic.Invoke(searching_turf, end)
+ if(heuristic_h == 0)
+ closed[searching_turf] |= scan_direction
+ continue
+
+ // Node is not known, create it.
+ var/datum/astar_node/new_node = new /datum/astar_node()
+ new_node.turf = searching_turf
+ new_node.total_cost_f = distance_g + heuristic_h
+ new_node.dist_from_start_g = distance_g
+ new_node.real_dist_from_start = real_distance
+ new_node.heuristic_h = heuristic_h
+ new_node.prev_node = current_node
+
+ binary_insert_node(new_node)
+
+ open_turf_to_node[searching_turf] = new_node
+ #ifdef DEBUG_PATHFINDING
+ all_nodes_ever[++all_nodes_ever.len] = new_node
+ #endif
+
+ // Check to see if we're close enough to the end destination.
+ if(ASTAR_CLOSE_ENOUGH_TO_END(end, new_node))
+ unwind_path(new_node)
+ return TRUE
+
+ // Stable, we'll just be back later
+ if(tick_check && TICK_CHECK)
+ return TRUE
+
+ return TRUE
+
+/datum/pathfind/astar/proc/binary_insert_node(datum/astar_node/node)
+ BINARY_INSERT_REVERSE(node, open_binary_tree, /datum/astar_node, node, total_cost_f, COMPARE_KEY)
+
+/datum/pathfind/astar/proc/can_step_diagonal(turf/from_turf, turf/to_turf)
+ var/in_dir = get_dir(from_turf, to_turf) // eg. northwest (1+8) = 9 (00001001)
+ var/first_step_direction_a = in_dir & 3 // eg. north (1+8)&3 (0000 0011) = 1 (0000 0001)
+ var/first_step_direction_b = in_dir & 12 // eg. west (1+8)&12 (0000 1100) = 8 (0000 1000)
+
+ for(var/direction in list(first_step_direction_a, first_step_direction_b))
+ var/turf/midpoint = get_step(from_turf, direction)
+ // If the midpoint is known to be inaccessible from the starting direction, no need to check it again.
+ if(closed[midpoint] & direction)
+ continue
+
+ if(CAN_STEP(midpoint, to_turf, simulated_only, pass_info, avoid))
+ return TRUE
+
+ return FALSE
+
+
+/// The generic heuristic, euclidean distance.
+/datum/pathfind/astar/proc/generic_heuristic(turf/searching_turf, turf/end)
+ return get_dist_euclidean(searching_turf, end)
+
+/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search]
+/datum/pathfind/astar/proc/unwind_path(datum/astar_node/unwind_node)
+ path = new()
+ var/turf/iter_turf = unwind_node.turf
+ path += iter_turf
+
+ var/datum/astar_node/iter_node = unwind_node.prev_node
+ while(iter_node)
+ path.Insert(1, iter_node.turf)
+ iter_node = iter_node.prev_node
+
+ return path
+
+#undef ASTAR_NODE
+#undef ASTAR_CLOSE_ENOUGH_TO_END
+
diff --git a/code/__HELPERS/paths/jps.dm b/code/__HELPERS/paths/jps.dm
index c816fc969e0..7d0158ed198 100644
--- a/code/__HELPERS/paths/jps.dm
+++ b/code/__HELPERS/paths/jps.dm
@@ -111,7 +111,7 @@
found_turfs[start] = TRUE // i'm sure this is fine
return TRUE
-/datum/pathfind/jps/search_step()
+/datum/pathfind/jps/search_step(tick_check = TRUE)
. = ..()
if(!.)
return .
@@ -131,7 +131,7 @@
diag_scan_spec(current_turf, scan_direction, current_processed_node)
// Stable, we'll just be back later
- if(TICK_CHECK)
+ if(tick_check && TICK_CHECK)
return TRUE
return TRUE
diff --git a/code/__HELPERS/paths/path.dm b/code/__HELPERS/paths/path.dm
index 33a6b1a730e..5fc8bf86920 100644
--- a/code/__HELPERS/paths/path.dm
+++ b/code/__HELPERS/paths/path.dm
@@ -229,9 +229,9 @@
/// These are generally cheaper than looping contents so they go first
switch(destination_turf.pathing_pass_method)
// This is already assumed to be true
- //if(TURF_PATHING_PASS_DENSITY)
- // if(destination_turf.density)
- // return TRUE
+ if(TURF_PATHING_PASS_DENSITY)
+ if(destination_turf.density)
+ return TRUE
if(TURF_PATHING_PASS_PROC)
if(!destination_turf.CanPathfindPass(actual_dir, pass_info))
return TRUE
@@ -309,6 +309,8 @@
var/datum/can_pass_info/rider_info = null
/// If our mob is buckled to something, what's it like
var/datum/can_pass_info/buckled_info = null
+ /// If our mob is flock phasing or can flock phase.
+ var/able_to_flockphase = FALSE
var/list/factions = list()
@@ -363,6 +365,10 @@
if(iscameramob(construct_from))
src.camera_type = construct_from.type
src.is_bot = isbot(construct_from)
+ if(isflockdrone(construct_from))
+ var/mob/living/basic/flock/drone/bird = construct_from
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE) || bird.substrate.has_points(10))
+ able_to_flockphase = TRUE
if(construct_from.pulling)
src.pulling_info = new(construct_from.pulling, access, no_id, call_depth + 1)
diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm
index 9c75bc07621..959f504ddb8 100644
--- a/code/__HELPERS/spatial_info.dm
+++ b/code/__HELPERS/spatial_info.dm
@@ -383,3 +383,14 @@
if(!test_turf.is_blocked_turf(exclude_mobs = TRUE))
return FALSE
return TRUE
+
+/// Returns the manhattan distance between two atoms. Returns INFINITY if either are not on a turf, for BYOND get_dist() parity.
+/proc/get_dist_manhattan(atom/A, atom/B)
+ if(!A.z || !B.z)
+ return INFINITY
+
+ if(A == B)
+ return -1
+
+ return abs(A.x - B.x) + abs(A.y - B.y) + abs(A.z - B.z)
+
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index 651fa6c3665..5a3d89a4017 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -162,6 +162,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_XENO_HOST "xeno_host" //Tracks whether we're gonna be a baby alien's mummy.
#define TRAIT_SHOCKIMMUNE "shock_immunity"
#define TRAIT_TESLA_SHOCKIMMUNE "tesla_shock_immunity"
+/// Are we being shocked by a flock sentinel?
+#define TRAIT_SHOCKED_BY_SENTINEL "shocked_by_flock_sentinel"
#define TRAIT_TELEKINESIS "telekinesis"
#define TRAIT_RESISTHEAT "resist_heat"
#define TRAIT_RESISTHEATHANDS "resist_heat_handsonly" //For when you want to be able to touch hot things, but still want fire to be an issue.
@@ -295,7 +297,13 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Trait that prevents AI controllers from planning detached from ai_status to prevent weird state stuff.
#define TRAIT_AI_PAUSED "trait_ai_paused"
+/// trait that prevents AI controllers from making new plans, but not executing plans
+#define TRAIT_AI_DISABLE_PLANNING "TRAIT_AI_DISABLE_PLANNING"
+/// This mob's speech is heard through walls by dead players/observers even if it has no client. Idk a better name
+#define TRAIT_IMPORTANT_SPEAKER "important_speaker"
+/// Ignores line of sight for the purposes of send_speech()
+#define TRAIT_HEAR_THROUGH_WALLS "hear_through_walls"
//***** MIND TRAITS *****/
#define TRAIT_HOLY "is_holy" // The mob is holy in regards to religion
@@ -621,6 +629,21 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
///every object that is currently the active storage of some client mob has this trait
#define TRAIT_ACTIVE_STORAGE "active_storage"
+// Flock trait sources
+/// Under control
+#define FLOCK_CONTROLLED_BY_OVERMIND_SOURCE "FLOCK_CONTROLLED_BY_OVERMIND_SOURCE"
+
+#define TRAIT_FLOCKPHASE "TRAIT_FLOCKPHASE"
+/// Is a flock object. Because of type fuckery.
+#define TRAIT_FLOCK_THING "TRAIT_FLOCK_THING"
+/// Flockdrones cannot deconstruct this object.
+#define TRAIT_FLOCK_NODECON "TRAIT_FLOCK_NODECON"
+/// Implements flock_examine() proc.
+#define TRAIT_FLOCK_EXAMINE "TRAIT_FLOCK_EXAMINE"
+
+/// Trait from mob/living/update_transform()
+#define UPDATE_TRANSFORM_TRAIT "update_transform"
+
//***** PROC WRAPPERS *****//
/// Proc wrapper of add_trait. You should only use this for callback. Otherwise, use the macro.
/proc/callback_add_trait(datum/target, trait, source)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 1498687c925..e58fff9cd1a 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -404,6 +404,8 @@
moblist.Add(M)
for(var/mob/camera/blob/M in sortmob)
moblist.Add(M)
+ for(var/mob/camera/flock/M in sortmob)
+ moblist.Add(M)
return moblist
// Format a power value in W, kW, MW, or GW.
@@ -558,6 +560,35 @@ Returns 1 if the chain up to the area contains the given typepath
return starting_turf
+/// returns a turf at the outer edge of a given radius
+/proc/get_random_perimeter_turf(atom/origin, radius)
+ var/turf/origin_turf = get_turf(origin)
+ if(isnull(origin_turf))
+ return null
+
+ if(radius == 0)
+ return origin_turf
+
+ var/upper_x = clamp(origin_turf.x + radius, 1, world.maxx)
+ var/lower_x = clamp(origin_turf.x - radius, 1, world.maxx)
+
+ var/upper_y = clamp(origin_turf.y + radius, 1, world.maxy)
+ var/lower_y = clamp(origin_turf.y - radius, 1, world.maxy)
+
+
+ var/x
+ var/y
+ var/z = origin_turf.z
+
+ if(prob(50))
+ x = pick(lower_x, upper_x)
+ y = rand(lower_y, upper_y)
+ else
+ x = rand(lower_x, upper_x)
+ y = pick(lower_y, upper_y)
+
+ return locate(x, y ,z)
+
// returns turf relative to A for a given clockwise angle at set range
// result is bounded to map size
/proc/get_angle_target_turf(atom/A, angle, range)
diff --git a/code/_globalvars/lists/misc_lists.dm b/code/_globalvars/lists/misc_lists.dm
index 85e7add442d..3d2b9351151 100644
--- a/code/_globalvars/lists/misc_lists.dm
+++ b/code/_globalvars/lists/misc_lists.dm
@@ -1,5 +1,9 @@
GLOBAL_LIST_INIT(alphabet, list("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"))
GLOBAL_LIST_INIT(alphabet_uppercase, list("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"))
+GLOBAL_LIST_INIT(vowels_lower, list("a","e","i","o","u"))
+GLOBAL_LIST_INIT(vowels_upper, list("A","E","I","O","U"))
+GLOBAL_LIST_INIT(consonants_lower, list("b","c","d","f","g","h","j","k","l","m","n","p","q","r","s","t","v","w","x","y","z"))
+GLOBAL_LIST_INIT(consonants_upper, list("B","C","D","F","G","H","J","K","L","M","N","P","Q","R","S","T","V","W","X","Y","Z"))
GLOBAL_LIST_INIT(html_colors, list("Alice Blue","Antique White","Aqua","Aquamarine","Azure","Beige","Bisque","Black","Blanched Almond","Blue","Blue Violet",
"Brown","Burly Wood","Cadet Blue","Chartreuse","Chocolate","Coral","Cornflower Blue","Cornsilk","Crimson","Cyan",
"Dark Blue","Dark Cyan","Dark Golden Rod","Dark Gray","Dark Green","Dark Khaki","Dark Magenta","Dark Olive Green",
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index a6d2005483c..6317cb9772f 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -111,7 +111,7 @@
var/obj/tgvehicle/sealed/car/clowncar/cc = loc
return cc.fire_cannon_at(A, src, params)
- if(restrained())
+ if(restrained() | istype(loc, /obj/structure/flock/cage))
RestrainedClickOn(A)
return
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 0ac270493fd..3f36e4b7380 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -658,6 +658,18 @@ so as to remain in compliance with the most up-to-date laws."
return
infected_user.ghostize()
+/atom/movable/screen/alert/ghost/flock
+ desc = "Would you like to ghost? You will be notified when your body is removed from the cage."
+
+/atom/movable/screen/alert/ghost/flock/Click()
+ if(!..())
+ return
+ var/mob/living/carbon/human/caged_user = usr
+ if(!istype(caged_user) || caged_user.stat == DEAD)
+ caged_user.clear_alert("ghost_cage")
+ return
+ caged_user.ghostize()
+
/atom/movable/screen/alert/notify_action
name = "Body created"
desc = "A body was created. You can enter it."
diff --git a/code/_onclick/hud/flock_hud.dm b/code/_onclick/hud/flock_hud.dm
new file mode 100644
index 00000000000..8ae71e6b112
--- /dev/null
+++ b/code/_onclick/hud/flock_hud.dm
@@ -0,0 +1,187 @@
+/datum/hud/flockdrone/New(mob/owner)
+ . = ..()
+ var/atom/movable/screen/using
+
+ using = new /atom/movable/screen/flockdrone_part/converter(null, src)
+ static_inventory += using
+
+ using = new /atom/movable/screen/flockdrone_part/incapacitator(null, src)
+ static_inventory += using
+
+ using = new /atom/movable/screen/flockdrone_part/absorber(null, src)
+ static_inventory += using
+
+ using = new /atom/movable/screen/flockphasing()
+ using.icon_state = (HAS_TRAIT(mymob, TRAIT_FLOCKPHASE) ? "running" : "walking")
+ infodisplay += using
+
+ using = new /atom/movable/screen/act_intent/flock()
+ using.icon_state = mymob.a_intent
+ static_inventory += using
+ action_intent = using
+
+ mymob.healths = new /atom/movable/screen/healths/flockdrone_health(null, src)
+ infodisplay += mymob.healths
+
+ mymob.pullin = new /atom/movable/screen/pull/flock()
+ mymob.pullin.hud = src
+ mymob.pullin.update_icon(UPDATE_ICON_STATE)
+ hotkeybuttons += mymob.pullin
+
+ zone_select = new /atom/movable/screen/zone_sel/flock()
+ zone_select.hud = src
+ zone_select.update_icon(UPDATE_OVERLAYS)
+ static_inventory += zone_select
+
+// Used for flock traces and the overmind
+/datum/hud/flockghost
+
+/atom/movable/screen/healths/flockdrone_health
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+ icon_state = "health1"
+ screen_loc = UI_BORG_HEALTH
+
+/atom/movable/screen/pull/flock
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+ icon_state = "pull0"
+ screen_loc = UI_MOVI
+
+/atom/movable/screen/act_intent/flock
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+
+/atom/movable/screen/act_intent/flock/Click(location, control, params)
+ var/_x = text2num(params2list(params)["icon-x"])
+ var/_y = text2num(params2list(params)["icon-y"])
+ if(_x<=16 && _y<=16)
+ usr.a_intent_change(INTENT_GRAB)
+ else if(_x<=16 && _y>=17)
+ usr.a_intent_change(INTENT_HELP)
+ else if(_x>=17 && _y<=16)
+ usr.a_intent_change(INTENT_HARM)
+ else if(_x>=17 && _y>=17)
+ usr.a_intent_change(INTENT_DISARM)
+
+/atom/movable/screen/flockphasing
+
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+ name = "flockphase toggle"
+ icon_state = "walking"
+ screen_loc = UI_MOVI
+
+/atom/movable/screen/flockphasing/Click(location, control, params)
+ var/mob/living/basic/flock/drone/M = usr
+ if(!istype(M))
+ return
+ if(HAS_TRAIT(M, TRAIT_FLOCKPHASE))
+ M.stop_flockphase()
+ else
+ if(M.can_flockphase() && M.flockphase_tax())
+ M.start_flockphase()
+
+/atom/movable/screen/zone_sel/flock
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+ overlay_file = 'icons/goonstation/hud/flock_ui.dmi'
+
+/atom/movable/screen/flockdrone_part
+ icon = 'icons/goonstation/hud/flock_ui.dmi'
+ var/active_state = ""
+ var/inactive_state = ""
+
+ var/part_type
+ var/datum/flockdrone_part/part_ref
+
+/atom/movable/screen/flockdrone_part/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ var/mob/living/basic/flock/drone/drone = usr
+ for(var/datum/flockdrone_part/part in drone.parts)
+ if(istype(part, part_type))
+ part_ref = part
+ break
+ part_ref?.screen_obj = src
+
+ update_appearance(UPDATE_ICON_STATE)
+
+/atom/movable/screen/flockdrone_part/Destroy()
+ part_ref?.screen_obj = null
+ part_ref = null
+ return ..()
+
+/atom/movable/screen/flockdrone_part/update_icon_state()
+ if(part_ref?.is_active())
+ icon_state = active_state
+ else
+ icon_state = inactive_state
+ return ..()
+
+/atom/movable/screen/flockdrone_part/Click(location, control, params)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/living/basic/flock/drone/drone = usr
+ drone.set_active_part(part_ref)
+
+/atom/movable/screen/flockdrone_part/converter
+ name = "converter"
+ active_state = "converter1"
+ inactive_state = "converter0"
+ desc = "Converters are an integrated multitool in every flockdrone's arsenal. On help and disarm intent, it will build structures, open lockers, and convert turfs. On harm or grab intent, it will deconstruct or cage things."
+
+ screen_loc = "CENTER-1:16,SOUTH:5"
+
+ part_type = /datum/flockdrone_part/converter
+
+/atom/movable/screen/flockdrone_part/incapacitator
+ name = "incapacitator"
+ active_state = "incapacitor1"
+ inactive_state = "incapacitor0"
+ desc = "Incapacitators are the primary weapon system of every flockdrone. They fire enhanced stunning rounds capable of disrupting electronics. They will recharge slowly on their own."
+ maptext_x = 6
+ maptext_y = 6
+ screen_loc = "CENTER:16,SOUTH:5"
+
+ part_type = /datum/flockdrone_part/incapacitator
+
+ var/icon/overlay_mask
+ var/obj/effect/abstract/charge_overlay
+
+/atom/movable/screen/flockdrone_part/incapacitator/Initialize(mapload, datum/hud/hud_owner)
+ charge_overlay = new()
+ . = ..()
+ charge_overlay.vis_flags = VIS_INHERIT_ID | VIS_INHERIT_LAYER | VIS_INHERIT_PLANE | VIS_INHERIT_ICON
+ charge_overlay.icon_state = "charge_overlay"
+ managed_vis_overlays += charge_overlay
+
+ overlay_mask = icon('icons/goonstation/hud/flock_ui.dmi', "darkener")
+ charge_overlay.add_filter("mask", 1, alpha_mask_filter(0, 0, overlay_mask))
+
+/atom/movable/screen/flockdrone_part/incapacitator/Destroy()
+ managed_vis_overlays -= charge_overlay
+ QDEL_NULL(charge_overlay)
+ return ..()
+
+/atom/movable/screen/flockdrone_part/incapacitator/update_appearance(updates)
+ . = ..()
+ var/datum/flockdrone_part/incapacitator/part = part_ref
+ maptext = MAPTEXT("[part?.shot_count || "0"]")
+
+ var/datum/flockdrone_part/incapacitator/weapon = part_ref
+ charge_overlay.transition_filter(
+ "mask",
+ 0.5 SECONDS,
+ list(
+ "y" = -24 * (1 - round(weapon.shot_count / weapon.max_shots, 0.1))
+ ),
+ SINE_EASING,
+ FALSE
+ )
+
+/atom/movable/screen/flockdrone_part/absorber
+ name = "material decompiler"
+ active_state = "absorber"
+ inactive_state = "absorber"
+ desc = "Absorbers are a key piece of any flockdrone's toolkit. They will pickup items and deconstruct them for substrate - the primary building material of any flock device."
+
+ screen_loc = "CENTER+1:16,SOUTH:5"
+
+ part_type = /datum/flockdrone_part/absorber
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index 7827eb8e416..5b75216179f 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -260,6 +260,11 @@
icon_state = "agentbox"
alpha = 128
+/atom/movable/screen/fullscreen/flock_convert
+ icon = 'icons/goonstation/hud/flockmindcircuit.dmi'
+ icon_state = "flockmindcircuit"
+ layer = BLIND_LAYER
+
#undef FULLSCREEN_LAYER
#undef BLIND_LAYER
#undef CRIT_LAYER
diff --git a/code/controllers/subsystem/SSmobs.dm b/code/controllers/subsystem/SSmobs.dm
index 1a3683937b3..5c2e83bbe1d 100644
--- a/code/controllers/subsystem/SSmobs.dm
+++ b/code/controllers/subsystem/SSmobs.dm
@@ -10,6 +10,7 @@ SUBSYSTEM_DEF(mobs)
var/list/currentrun = list()
var/static/list/clients_by_zlevel[][]
var/static/list/dead_players_by_zlevel[][] = list(list()) // Needs to support zlevel 1 here, MaxZChanged only happens when CC is created and new_players can login before that.
+ var/static/list/flock_cameras_by_zlevel[][]= list(list())
var/static/list/cubemonkeys = list()
/// The amount of Xenobiology mobs (and their offspring) that exist in the world. Used for mob capping. Excludes Slimes
var/xenobiology_mobs = 0
@@ -26,6 +27,7 @@ SUBSYSTEM_DEF(mobs)
/datum/controller/subsystem/mobs/Initialize()
clients_by_zlevel = new /list(world.maxz, 0)
dead_players_by_zlevel = new /list(world.maxz, 0)
+ flock_cameras_by_zlevel = new /list(world.maxz, 0)
/datum/controller/subsystem/mobs/fire(resumed = 0)
var/seconds = wait * 0.1
diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm
index 21dfe80ac90..14aca127a81 100644
--- a/code/controllers/subsystem/movement/movement_types.dm
+++ b/code/controllers/subsystem/movement/movement_types.dm
@@ -407,6 +407,219 @@
return MOVELOOP_FAILURE
+/**
+ * Used for following A* defined paths.
+ *
+ * Returns TRUE if the loop sucessfully started, or FALSE if it failed
+ *
+ * Arguments:
+ * moving - The atom we want to move
+ * chasing - The atom we want to move towards
+ * delay - How many deci-seconds to wait between fires. Defaults to the lowest value, 0.1
+ * repath_delay - How often we're allowed to recalculate our path
+ * max_path_length - The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
+ * miminum_distance - Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example
+ * id - An ID card representing what access we have and what doors we can open
+ * simulated_only - Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
+ * avoid - If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
+ * skip_first - Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break things
+ * timeout - Time in deci-seconds until the moveloop self expires. Defaults to infinity
+ * subsystem - The movement subsystem to use. Defaults to SSmovement. Only one loop can exist for any one subsystem
+ * priority - Defines how different move loops override each other. Lower numbers beat higher numbers, equal defaults to what currently exists. Defaults to MOVEMENT_DEFAULT_PRIORITY
+ * flags - Set of bitflags that effect move loop behavior in some way. Check _DEFINES/movement.dm
+ *
+**/
+/datum/move_manager/proc/astar_move(
+ moving,
+ chasing,
+ delay,
+ timeout,
+ repath_delay,
+ max_path_length,
+ minimum_distance,
+ list/access,
+ simulated_only,
+ turf/avoid,
+ skip_first,
+ subsystem,
+ priority,
+ flags,
+ datum/extra_info,
+ initial_path,
+ use_diagonals,
+ datum/callback/heuristic,
+)
+
+ return add_to_loop(
+ moving,
+ subsystem,
+ /datum/move_loop/has_target/astar,
+ priority,
+ flags,
+ extra_info,
+ delay,
+ timeout,
+ chasing,
+ repath_delay,
+ max_path_length,
+ minimum_distance,
+ access,
+ simulated_only,
+ avoid,
+ skip_first,
+ initial_path,
+ use_diagonals,
+ heuristic,
+)
+
+/datum/move_loop/has_target/astar
+ ///How often we're allowed to recalculate our path
+ var/repath_delay
+ ///Max amount of steps to search
+ var/max_path_length
+ ///Minimum distance to the target before path returns
+ var/minimum_distance
+ ///A list representing what access we have and what doors we can open.
+ var/list/access
+ ///Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
+ var/simulated_only
+ ///A perticular turf to avoid
+ var/turf/avoid
+ ///Should we skip the first step? This is the tile we're currently on, which breaks some things
+ var/skip_first
+ ///A list for the path we're currently following
+ var/list/movement_path
+ /// Should we use diagonals, or use only cardinals?
+ var/use_diagonals
+ ///Cooldown for repathing, prevents spam
+ COOLDOWN_DECLARE(repath_cooldown)
+ ///Bool used to determine if we're already making a path in AStar. this prevents us from re-pathing while we're already busy.
+ var/is_pathing = FALSE
+ ///Callback to invoke once we make a path
+ var/datum/callback/on_finish_callback
+
+ ///AStar heuristic function
+ var/datum/callback/heuristic
+
+/datum/move_loop/has_target/astar/New(datum/movement_packet/owner, datum/controller/subsystem/movement/controller, atom/moving, priority, flags, datum/extra_info)
+ . = ..()
+ on_finish_callback = CALLBACK(src, PROC_REF(on_finish_pathing))
+
+/datum/move_loop/has_target/astar/setup(delay, timeout, atom/chasing, repath_delay, max_path_length, minimum_distance, list/access, simulated_only, turf/avoid, skip_first, list/initial_path, use_diagonals, datum/callback/heuristic)
+ . = ..()
+ if(!.)
+ return
+
+ src.repath_delay = repath_delay
+ src.max_path_length = max_path_length
+ src.minimum_distance = minimum_distance
+ src.access = access
+ src.simulated_only = simulated_only
+ src.avoid = avoid
+ src.skip_first = skip_first
+ src.use_diagonals = use_diagonals
+ src.heuristic = heuristic
+ movement_path = initial_path?.Copy()
+
+/datum/move_loop/has_target/astar/compare_loops(
+ datum/move_loop/loop_type,
+ priority,
+ flags,
+ extra_info,
+ delay,
+ timeout,
+ atom/chasing,
+ repath_delay,
+ max_path_length,
+ minimum_distance,
+ obj/item/card/id/id,
+ simulated_only,
+ turf/avoid,
+ skip_first,
+ initial_path,
+ use_diagonals,
+ datum/callback/heuristic,
+)
+ if(..() && \
+ repath_delay == src.repath_delay && \
+ max_path_length == src.max_path_length && \
+ minimum_distance == src.minimum_distance && \
+ access ~= src.access && \
+ simulated_only == src.simulated_only && \
+ avoid == src.avoid && \
+ use_diagonals == src.use_diagonals && \
+ heuristic == src.heuristic)
+ return TRUE
+ return FALSE
+
+/datum/move_loop/has_target/astar/loop_started()
+ . = ..()
+ if(!movement_path)
+ INVOKE_ASYNC(src, PROC_REF(recalculate_path))
+
+/datum/move_loop/has_target/astar/loop_stopped()
+ . = ..()
+ movement_path = null
+
+/datum/move_loop/has_target/astar/Destroy()
+ avoid = null
+ return ..()
+
+///Tries to calculate a new path for this moveloop.
+/datum/move_loop/has_target/astar/proc/recalculate_path()
+ if(!COOLDOWN_FINISHED(src, repath_cooldown))
+ return
+
+ COOLDOWN_START(src, repath_cooldown, repath_delay)
+
+ var/path_ok = SSpathfinder.astar_pathfind(
+ moving,
+ target,
+ max_path_length,
+ minimum_distance,
+ access,
+ simulated_only,
+ avoid,
+ skip_first,
+ use_diagonals = use_diagonals,
+ on_finish = on_finish_callback,
+ heuristic = heuristic
+ )
+
+ if(path_ok)
+ is_pathing = TRUE
+ SEND_SIGNAL(src, COMSIG_MOVELOOP_ASTAR_REPATH)
+
+///Called when a path has finished being created
+/datum/move_loop/has_target/astar/proc/on_finish_pathing(list/path)
+ movement_path = path
+ is_pathing = FALSE
+
+/datum/move_loop/has_target/astar/move()
+ if(!length(movement_path))
+ if(is_pathing)
+ return MOVELOOP_NOT_READY
+ else
+ INVOKE_ASYNC(src, PROC_REF(recalculate_path))
+ return MOVELOOP_FAILURE
+
+ var/turf/next_step = movement_path[1]
+ var/atom/old_loc = moving.loc
+ //KAPU NOTE: WE DO NOT HAVE THIS
+ //moving.Move(next_step, get_dir(moving, next_step), FALSE, !(flags & MOVEMENT_LOOP_NO_DIR_UPDATE))
+ var/movement_dir = get_dir(moving, next_step)
+ moving.Move(next_step, movement_dir)
+ . = (old_loc != moving?.loc) ? MOVELOOP_SUCCESS : MOVELOOP_FAILURE
+
+ // this check if we're on exactly the next tile may be overly brittle for dense objects who may get bumped slightly
+ // to the side while moving but could maybe still follow their path without needing a whole new path
+ if(get_turf(moving) == next_step)
+ if(length(movement_path))
+ movement_path.Cut(1,2)
+ else
+ INVOKE_ASYNC(src, PROC_REF(recalculate_path))
+ return MOVELOOP_FAILURE
+
///Base class of move_to and move_away, deals with the distance and target aspect of things
/datum/move_loop/has_target/dist_bound
var/distance = 0
diff --git a/code/controllers/subsystem/non_firing/SSpathfinder.dm b/code/controllers/subsystem/non_firing/SSpathfinder.dm
index 0896e4c580b..cd2446fb3e6 100644
--- a/code/controllers/subsystem/non_firing/SSpathfinder.dm
+++ b/code/controllers/subsystem/non_firing/SSpathfinder.dm
@@ -213,3 +213,74 @@ SUBSYSTEM_DEF(pathfinder)
active_pathing += path
return TRUE
return FALSE
+
+/datum/controller/subsystem/pathfinder/proc/astar_pathfind(
+ atom/movable/invoker,
+ atom/end,
+ max_steps = 30,
+ mintargetdist,
+ list/access,
+ simulated_only = TRUE,
+ turf/exclude,
+ skip_first = TRUE,
+ use_diagonals = TRUE,
+ datum/callback/on_finish,
+ datum/callback/heuristic,
+)
+
+ var/datum/pathfind/astar/path = new(
+ invoker,
+ end,
+ access,
+ max_steps,
+ mintargetdist,
+ simulated_only,
+ exclude,
+ skip_first,
+ use_diagonals,
+ on_finish,
+ heuristic,
+ )
+
+ if(path.start())
+ active_pathing += path
+ return TRUE
+ return FALSE
+
+/datum/controller/subsystem/pathfinder/proc/astar_pathfind_now(
+ atom/movable/invoker,
+ atom/end,
+ max_steps = 14,
+ mintargetdist,
+ list/access,
+ simulated_only = TRUE,
+ turf/exclude,
+ skip_first = TRUE,
+ use_diagonals = TRUE,
+ datum/callback/heuristic,
+)
+
+ var/datum/pathfind/astar/path = new(
+ invoker,
+ end,
+ access,
+ max_steps,
+ mintargetdist,
+ simulated_only,
+ exclude,
+ skip_first,
+ use_diagonals,
+ null,
+ heuristic,
+ )
+
+ if(!path.start())
+ return FALSE
+
+ if(!path.search_step(FALSE))
+ path.early_exit()
+ return FALSE
+
+ path.finished()
+ return path.path
+
diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm
index 715fe1fc096..f3be39b6b03 100644
--- a/code/datums/actions/cooldown_action.dm
+++ b/code/datums/actions/cooldown_action.dm
@@ -269,6 +269,9 @@
/datum/action/cooldown/proc/PreActivate(atom/target)
if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src, target) & COMPONENT_BLOCK_ABILITY_START)
return
+
+ if(!is_valid_target(target))
+ return
// Note, that PreActivate handles no cooldowns at all by default.
// Be sure to call StartCooldown() in Activate() where necessary.
. = Activate(target)
@@ -361,4 +364,14 @@
return stat_panel_data
+/**
+ * Check if the target we're casting on is a valid target.
+ * For no-target (self cast) actions, the target being checked (cast_on) is the caster.
+ * For click_to_activate actions, the target being checked is the clicked atom.
+ *
+ * Return TRUE if cast_on is valid, FALSE otherwise
+ */
+/datum/action/cooldown/proc/is_valid_target(atom/cast_on)
+ return TRUE
+
#undef COOLDOWN_NO_DISPLAY_TIME
diff --git a/code/datums/ai/ai_behavior.dm b/code/datums/ai/ai_behavior.dm
index 32245a5c60d..01eb28886da 100644
--- a/code/datums/ai/ai_behavior.dm
+++ b/code/datums/ai/ai_behavior.dm
@@ -2,12 +2,19 @@
/datum/ai_behavior
/// What distance you need to be from the target to perform the action.
var/required_distance = 1
+ /// If >0, overrides controller.target_search_radius
+ var/search_radius_override = null
/// Flags for extra behavior
var/behavior_flags = NONE
/// Cooldown between actions performances, defaults to the value of
/// CLICK_CD_MELEE because that seemed like a nice standard for the speed of
/// AI behavior
var/action_cooldown = CLICK_CD_MELEE
+ /// A multiplier applied to the behavior's goap_score().
+ var/goap_weight = 1
+
+ /// Behaviors to add upon a successful setup
+ var/list/sub_behaviors
/// Called by the AI controller when first being added. Additional arguments
/// depend on the behavior type. For example, if the behavior involves attacking
@@ -48,6 +55,7 @@
controller.behavior_args -= type
// If this was a movement task, reset our movement target if necessary
if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT))
+ next_behavior(controller, succeeded)
return
if(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH)
return
@@ -63,3 +71,130 @@
if(controller.movement_target_source != type)
return
controller.set_movement_target(type, null)
+
+/// Returns a behavior to perform after this one, or null if continuing this one
+/datum/ai_behavior/proc/next_behavior(datum/ai_controller/controller, success)
+ return null
+
+/// Executed before goap_score(), to see if the behavior should even be considered.
+/datum/ai_behavior/proc/goap_precondition(datum/ai_controller/controller)
+ return TRUE
+
+/// Returns a numerical value that is essentially a priority for planner behaviors.
+/datum/ai_behavior/proc/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, goap_get_ideal_target(controller))
+
+/// Returns the ideal target for this behavior.
+/datum/ai_behavior/proc/goap_get_ideal_target(datum/ai_controller/controller, set_path = FALSE)
+ var/list/options = goap_filter_targets(controller)
+ return get_best_target_by_distance_score(controller, options, set_path)
+
+/// Filter through potential targets to find real targets.
+/datum/ai_behavior/proc/goap_filter_targets(datum/ai_controller/controller)
+ var/list/options = list()
+ for(var/atom/potential_target as anything in goap_get_potential_targets(controller))
+ if(goap_is_valid_target(controller, potential_target))
+ options += potential_target
+ return options
+
+/// Returns a list of potential targets to filter through.
+/datum/ai_behavior/proc/goap_get_potential_targets(datum/ai_controller/controller)
+ return list()
+
+/// Returns TRUE if the given atom is a valid target for this behavior.
+/datum/ai_behavior/proc/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ return TRUE
+
+#define BINARY_INSERT_TARGET(target_list, target, score) \
+ do { \
+ var/length = length(target_list); \
+ if(!length) { \
+ target_list[target] = score; \
+ } else { \
+ var/left = 1; \
+ var/right = length; \
+ var/middle = (left + right) >> 1; \
+ while(left < right) { \
+ if(target_list[target_list[middle]] <= score) { \
+ left = middle + 1; \
+ } else { \
+ right = middle; \
+ }; \
+ middle = (left + right) >> 1; \
+ }; \
+ middle = target_list[target_list[middle]] > score ? middle : middle + 1; \
+ target_list.Insert(middle, target); \
+ target_list[target] = score; \
+ }; \
+ } while(FALSE)
+
+
+/// Returns the best target by scoring the distance of each possible target.
+/// Takes a list to insert the path into, so it can be handed back and re-used.
+/datum/ai_behavior/proc/get_best_target_by_distance_score(datum/ai_controller/controller, list/targets, set_path = FALSE)
+ if(!length(targets))
+ return null
+
+ var/atom/movable/pawn = controller.pawn
+ var/list/access = controller.get_access()
+
+ var/list/targets_by_score = list()
+ var/list/reachable_targets = list()
+
+ // Sort targets by their estimated score. The last element in the lists has the highest score.
+ while(length(targets))
+ var/index = rand(1, length(targets))
+ var/atom/A = targets[index]
+ targets.Cut(index, index + 1)
+
+ var/score = score_distance(controller, A)
+
+ BINARY_INSERT_TARGET(targets_by_score, A, score)
+
+ // WEE WOO WEE WOO BEHAVIOR-CHANGING MICRO-OPT: we assume turfs further than 1 tile aren't reachable
+ // Because this is true in 99.9999999999999999% of cases
+ if(get_dist(pawn, A) <= 1 && A.Adjacent(pawn))
+ BINARY_INSERT_TARGET(reachable_targets, A, score)
+
+ // Go through our sorted target list until we find a path to one.
+ // Note: This does mean that the found target might not be the ideal one, as it's operating on the estimate
+ // This is a performance thing. We cannot actually use the true best target.
+ var/atom/ideal_atom
+ var/list/ideal_path
+ if(length(reachable_targets))
+ ideal_atom = reachable_targets[length(reachable_targets)]
+ else
+ while(length(targets_by_score))
+ var/atom/candidate = targets_by_score[length(targets_by_score)]
+ targets_by_score.len--
+
+ var/list/path = SSpathfinder.astar_pathfind_now(
+ controller.pawn,
+ candidate,
+ controller.max_target_distance,
+ required_distance,
+ access,
+ HAS_TRAIT(controller.pawn, TRAIT_FLYING),
+ )
+
+ if(path)
+ ideal_atom = candidate
+ ideal_path = path
+ break
+
+ if(set_path && length(ideal_path))
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ controller.set_blackboard_key(BB_PATH_TO_USE, ideal_path)
+ return ideal_atom
+
+#undef BINARY_INSERT_TARGET
+
+/// Helper for scoring something based on the distance between it and the pawn.
+/// By default, returns a value between 100 and -INFINITY, where 100 is a distance of 0 steps.
+/// A distance equal to target_search_radius is zero.
+/// A distance greater than target_search_radius is negative.
+/datum/ai_behavior/proc/score_distance(datum/ai_controller/controller, atom/target)
+ var/search_radius = search_radius_override || controller.target_search_radius
+ if(isnull(target))
+ return -INFINITY
+ return 100 * (search_radius - get_dist_manhattan(get_turf(controller.pawn), get_turf(target))) / search_radius
diff --git a/code/datums/ai/ai_controller.dm b/code/datums/ai/ai_controller.dm
index 8933fcc5c47..f572e52bb03 100644
--- a/code/datums/ai/ai_controller.dm
+++ b/code/datums/ai/ai_controller.dm
@@ -45,6 +45,8 @@ RESTRICT_TYPE(/datum/ai_controller)
var/continue_processing_when_client = FALSE
/// Distance to give up on target.
var/max_target_distance = 14
+ /// The search radius when looking for interesting things.
+ var/target_search_radius = 5
/// Cooldown for new plans, to prevent AI from going nuts if it can't think of new plans and looping on end
COOLDOWN_DECLARE(failed_planning_cooldown)
/// All subtrees this AI has available. Will run them in order, so make sure
@@ -142,8 +144,9 @@ RESTRICT_TYPE(/datum/ai_controller)
return
var/list/temp_subtree_list = list()
for(var/subtree in planning_subtrees)
- var/subtree_instance = GLOB.ai_subtrees[subtree]
+ var/datum/ai_planning_subtree/subtree_instance = GLOB.ai_subtrees[subtree]
temp_subtree_list += subtree_instance
+ subtree_instance.setup(src)
planning_subtrees = temp_subtree_list
/// Proc to move from one pawn to another. This will destroy the target's existing controller.
@@ -344,7 +347,7 @@ RESTRICT_TYPE(/datum/ai_controller)
qdel(src)
/datum/ai_controller/proc/setup_able_to_run()
- // paused_until is handled by PauseAi() manually
+ // paused_until is handled by pause_ai() manually
RegisterSignals(pawn, list(SIGNAL_ADDTRAIT(TRAIT_AI_PAUSED), SIGNAL_REMOVETRAIT(TRAIT_AI_PAUSED)), PROC_REF(update_able_to_run))
/datum/ai_controller/proc/clear_able_to_run()
@@ -521,6 +524,9 @@ RESTRICT_TYPE(/datum/ai_controller)
var/list/arguments = args.Copy()
arguments[1] = src
+ if(!behavior.setup(arglist(arguments)))
+ return FALSE
+
// It's still in the plan, don't add it again to current_behaviors
// but do keep it in the planned behavior list so its not cancelled
if(current_behaviors[behavior])
diff --git a/code/datums/ai/ai_planning_subtree.dm b/code/datums/ai/ai_planning_subtree.dm
index 10e45a49257..65ef5549d38 100644
--- a/code/datums/ai/ai_planning_subtree.dm
+++ b/code/datums/ai/ai_planning_subtree.dm
@@ -18,3 +18,9 @@
/// the other subtrees should multiple exist in the controller.
/datum/ai_planning_subtree/proc/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
return
+
+/datum/ai_planning_subtree/proc/setup(datum/ai_controller/controller)
+ return
+
+/datum/ai_planning_subtree/proc/process_behavior_selection(datum/ai_controller/controller, seconds_per_tick)
+ select_behaviors(controller, seconds_per_tick)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/frustration.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/frustration.dm
new file mode 100644
index 00000000000..16af43ecb2b
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/frustration.dm
@@ -0,0 +1,24 @@
+/// A simple frustration behavior to be queued in addition to another behavior. If the given duration ellapses and the frustration key is still
+/// present, the ai will cancel all of it's actions.
+/datum/ai_behavior/frustration
+ action_cooldown = 0.1 SECONDS
+
+/datum/ai_behavior/frustration/setup(datum/ai_controller/controller, frustration_key, duration)
+ . = ..()
+ controller.set_blackboard_key(frustration_key, world.time)
+
+/datum/ai_behavior/frustration/perform(delta_time, datum/ai_controller/controller, frustration_key, duration)
+ ..()
+ if(isnull(controller.blackboard[frustration_key]))
+ return AI_BEHAVIOR_SUCCEEDED
+
+ if(world.time >= duration + controller.blackboard[frustration_key])
+ controller.cancel_actions()
+ DEBUG_AI_LOG(controller, "AI got frustrated and cancelled current actions.")
+ return AI_BEHAVIOR_FAILED
+
+ return AI_BEHAVIOR_DELAY
+
+/datum/ai_behavior/frustration/finish_action(datum/ai_controller/controller, succeeded, frustration_key, duration)
+ . = ..()
+ controller.clear_blackboard_key(frustration_key)
diff --git a/code/datums/ai/goap_subtree.dm b/code/datums/ai/goap_subtree.dm
new file mode 100644
index 00000000000..cdc8cc2431d
--- /dev/null
+++ b/code/datums/ai/goap_subtree.dm
@@ -0,0 +1,47 @@
+/datum/ai_planning_subtree/goap
+ var/list/possible_behaviors = list()
+
+/datum/ai_planning_subtree/goap/setup(datum/ai_controller/controller, ...)
+ . = ..()
+ controller.set_blackboard_key(BB_PLANNER_BEHAVIORS, list())
+ for(var/behavior_type in possible_behaviors)
+ var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type)
+ if(isnull(behavior))
+ stack_trace("Bad AI behavior: [behavior_type]")
+ continue
+ controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, 0)
+
+/datum/ai_planning_subtree/goap/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/list/behavior_cache = controller.blackboard[BB_PLANNER_BEHAVIORS]
+ var/datum/ai_behavior/candidate = behavior_cache[1]
+ var/score_to_beat = behavior_cache[candidate]
+
+ for(var/behavior in behavior_cache)
+ var/behavior_score = behavior_cache[behavior]
+ if(behavior_score == AI_GOAP_SKIP_BEHAVIOR)
+ continue
+
+ // Filter out lower scoring behaviors
+ if(score_to_beat > behavior_score)
+ continue
+
+ // If there's a tie, pick either.
+ if((behavior_score == score_to_beat) && prob(50))
+ continue
+
+ candidate = behavior
+ score_to_beat = behavior_cache[candidate]
+
+ if(candidate)
+ controller.queue_behavior(candidate.type)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_planning_subtree/goap/process_behavior_selection(datum/ai_controller/controller, seconds_per_tick)
+ for(var/datum/ai_behavior/behavior in controller.blackboard[BB_PLANNER_BEHAVIORS])
+ if(behavior.goap_precondition(controller))
+ controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, behavior.goap_weight * behavior.goap_score(controller))
+ else
+ controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, AI_GOAP_SKIP_BEHAVIOR)
+
+ return ..()
diff --git a/code/datums/ai/movement/ai_movement_astar.dm b/code/datums/ai/movement/ai_movement_astar.dm
new file mode 100644
index 00000000000..f01d3d0ff3c
--- /dev/null
+++ b/code/datums/ai/movement/ai_movement_astar.dm
@@ -0,0 +1,44 @@
+/**
+ * This movement datum represents smart-pathing
+ */
+/datum/ai_movement/astar
+ max_pathing_attempts = 20
+
+ /// If FALSE, diagonals will be split into 2 cardinal moves.
+ var/use_diagonals = TRUE
+
+/datum/ai_movement/astar/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
+ . = ..()
+ var/atom/movable/moving = controller.pawn
+ var/delay = controller.movement_delay
+ var/max_path_length_override = controller.blackboard[BB_PATH_MAX_LENGTH]
+
+ var/datum/move_loop/has_target/astar/loop = GLOB.move_manager.astar_move(
+ moving,
+ current_movement_target,
+ delay,
+ repath_delay = 0.5 SECONDS,
+ max_path_length = isnull(max_path_length_override) ? AI_MAX_PATH_LENGTH : max_path_length_override,
+ minimum_distance = controller.get_minimum_distance(),
+ access = controller.get_access(),
+ simulated_only = !HAS_TRAIT(controller.pawn, TRAIT_FLYING),
+ subsystem = SSai_movement,
+ extra_info = controller,
+ initial_path = controller.blackboard[BB_PATH_TO_USE],
+ use_diagonals = use_diagonals,
+ )
+
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
+ RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
+ RegisterSignal(loop, COMSIG_MOVELOOP_ASTAR_REPATH, PROC_REF(repath_incoming))
+
+ return loop
+
+/datum/ai_movement/astar/proc/repath_incoming(datum/move_loop/has_target/astar/source)
+ SIGNAL_HANDLER
+ var/datum/ai_controller/controller = source.extra_info
+
+ source.access = controller.get_access()
+ source.minimum_distance = controller.get_minimum_distance()
+
diff --git a/code/datums/components/material_container.dm b/code/datums/components/material_container.dm
index 45d94901398..e294c94c304 100644
--- a/code/datums/components/material_container.dm
+++ b/code/datums/components/material_container.dm
@@ -450,6 +450,16 @@
sheet_type = /obj/item/stack/sheet/mineral/iridium
ore_type = /obj/item/stack/ore/iridium
+/datum/material/gnesis
+ name = "gnesis"
+ id = MAT_GNESIS
+ sheet_type = /obj/item/stack/sheet/gnesis
+
+/datum/material/gnesis_glass
+ name = "translucent gnesis"
+ id = MAT_GNESIS_GLASS
+ sheet_type = /obj/item/stack/sheet/gnesis_glass
+
/datum/material/bananium
name = "Bananium"
id = MAT_BANANIUM
diff --git a/code/datums/keybindings/flock_keybinds.dm b/code/datums/keybindings/flock_keybinds.dm
new file mode 100644
index 00000000000..c209b13807c
--- /dev/null
+++ b/code/datums/keybindings/flock_keybinds.dm
@@ -0,0 +1,64 @@
+/datum/keybinding/flock/can_use(client/C, mob/M)
+ return isflockmob(M) && ..()
+
+/datum/keybinding/flock/intent
+ /// The intent to switch to.
+ var/intent
+
+/datum/keybinding/flock/intent/down(client/C)
+ . = ..()
+ var/mob/living/basic/flock/M = C.mob
+ M.a_intent_change(intent)
+
+/datum/keybinding/flock/intent/help
+ name = "Help Intent (Tap)"
+ intent = INTENT_HELP
+ keys = list("1")
+
+/datum/keybinding/flock/intent/disarm
+ name = "Disarm Intent (Tap)"
+ intent = INTENT_DISARM
+ keys = list("2")
+
+/datum/keybinding/flock/intent/grab
+ name = "Grab Intent (Tap)"
+ intent = INTENT_GRAB
+ keys = list("3")
+
+/datum/keybinding/flock/intent/harm
+ name = "Harm Intent (Tap)"
+ intent = INTENT_HARM
+ keys = list("4")
+
+/datum/keybinding/flock/cycle_part
+ name = "Cycle Part"
+ keys = list("X")
+
+/datum/keybinding/flock/cycle_part/can_use(client/C, mob/M)
+ return isflockdrone(M)
+
+/datum/keybinding/flock/cycle_part/down(client/C)
+ . = ..()
+ var/mob/living/basic/flock/drone/M = C.mob
+ if(M.active_part == M.parts[1])
+ M.set_active_part(M.parts[2])
+ else if(M.active_part == M.parts[2])
+ M.set_active_part(M.parts[3])
+ else if(M.active_part == M.parts[3])
+ M.set_active_part(M.parts[1])
+
+/datum/keybinding/flock/flockphase
+ name = "Toggle Flockphase"
+ keys = list("C")
+
+/datum/keybinding/flock/flockphase/can_use(client/C, mob/M)
+ return isflockdrone(M)
+
+/datum/keybinding/flock/flockphase/down(client/C)
+ . = ..()
+ var/mob/living/basic/flock/drone/M = C.mob
+ if(HAS_TRAIT(M, TRAIT_FLOCKPHASE))
+ M.stop_flockphase()
+ else
+ if(M.can_flockphase() && M.flockphase_tax())
+ M.start_flockphase()
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index 38350a6483e..7b061737d87 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -6,6 +6,15 @@
end_sound = 'sound/machines/shower/shower_end.ogg'
volume = 20
+/datum/looping_sound/flock_relay
+ mid_sounds = list('sound/goonstation/flockmind/Flock_Reactor.ogg' = 1)
+ mid_length = 33 SECONDS
+ volume = 40
+ extra_range = 10
+ falloff_exponent = 10
+ falloff_distance = 5
+ channel = CHANNEL_ENGINE
+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/datum/looping_sound/supermatter
diff --git a/code/datums/point_holder.dm b/code/datums/point_holder.dm
new file mode 100644
index 00000000000..4b7fcf2c073
--- /dev/null
+++ b/code/datums/point_holder.dm
@@ -0,0 +1,72 @@
+/// Completely generic resource handler
+/datum/point_holder
+ VAR_PRIVATE/max_points = INFINITY
+ VAR_PRIVATE/points = 0
+
+/datum/point_holder/proc/has_points(atleast)
+ if(points >= atleast)
+ return points
+
+/datum/point_holder/proc/is_full()
+ return points == max_points
+
+/// Returns the number of points needed to fill the holder.
+/datum/point_holder/proc/how_empty()
+ return max_points - points
+
+/// Returns the % (0-100) of how full the holder is.
+/datum/point_holder/proc/percent(step = 1)
+ if(max_points == INFINITY)
+ return 0
+ return round((points / max_points) * 100, step)
+
+/datum/point_holder/proc/adjust_points(num, check_enough)
+ if(num > 0)
+ return add_points(num)
+
+ else if(num < 0)
+ return remove_points(num, check_enough)
+
+ return 0
+
+/// Add points. Returns the number of points added.
+/datum/point_holder/proc/add_points(num)
+ if(points == max_points)
+ return 0
+
+ var/old = points
+ points = min(max_points, points + num)
+ return points - old
+
+/// Removes points. Returns the number of points removed.
+/datum/point_holder/proc/remove_points(num, check_enough)
+ if(check_enough && (points < num))
+ return 0
+
+ if(points == 0)
+ return 0
+
+ var/old = points
+ points = max(0, points - num)
+ return old - points
+
+/datum/point_holder/proc/set_max_points(new_max)
+ max_points = new_max
+ points = min(max_points, points)
+
+/datum/point_holder/proc/get_max_points()
+ return max_points
+
+/datum/point_holder/infinite
+
+/datum/point_holder/infinite/add_points(num)
+ return 0
+
+/datum/point_holder/infinite/has_points(atleast)
+ return INFINITY
+
+/datum/point_holder/infinite/remove_points(num, check_enough)
+ return 0
+
+/datum/point_holder/infinite/set_max_points(new_max)
+ max_points = new_max
diff --git a/code/datums/proximity/advanced_proximity_monitor.dm b/code/datums/proximity/advanced_proximity_monitor.dm
index 94889a6dd21..54b3620cad9 100644
--- a/code/datums/proximity/advanced_proximity_monitor.dm
+++ b/code/datums/proximity/advanced_proximity_monitor.dm
@@ -15,6 +15,10 @@
/// Can be used in certain situations where you may have effects that trigger only at the edge,
/// while also wanting the field effect to trigger at edge turfs as well
var/edge_is_a_field = FALSE
+
+ /// If TRUE, view() will be used over range(). If this is used, edge turfs will not exist!
+ var/use_view = FALSE
+
/// All turfs on the inside of the proximity monitor - range - 1 turfs
var/list/turf/field_turfs = list()
/// All turfs on the very last tile of the proximity monitor's radius
@@ -131,6 +135,13 @@
/datum/proximity_monitor/advanced/proc/update_new_turfs()
if(ignore_if_not_on_turf && !isturf(host.loc))
return list(FIELD_TURFS_KEY = list(), EDGE_TURFS_KEY = list())
+
+ if(use_view)
+ var/list/turfs = list()
+ for(var/turf/T as turf in view(current_range, host))
+ turfs += T
+ return list(FIELD_TURFS_KEY = turfs, EDGE_TURFS_KEY = list())
+
var/list/local_field_turfs = list()
var/list/local_edge_turfs = list()
var/turf/center = get_turf(host)
diff --git a/code/datums/proximity/interceptor_field.dm b/code/datums/proximity/interceptor_field.dm
new file mode 100644
index 00000000000..d0f9950a85d
--- /dev/null
+++ b/code/datums/proximity/interceptor_field.dm
@@ -0,0 +1,14 @@
+/datum/proximity_monitor/advanced/interceptor
+ use_view = TRUE
+
+/datum/proximity_monitor/advanced/interceptor/New(atom/_host, range, _ignore_if_not_on_turf)
+ ..()
+ recalculate_field(TRUE)
+
+/datum/proximity_monitor/advanced/interceptor/Destroy()
+ return ..()
+
+/datum/proximity_monitor/advanced/interceptor/field_turf_crossed(atom/movable/movable, turf/old_location, turf/new_location)
+ . = ..()
+ if(isprojectile(movable))
+ astype(host, /obj/structure/flock/interceptor).try_intercept_projectile(movable)
diff --git a/code/game/alternate_appearance.dm b/code/game/alternate_appearance.dm
index b7cdb4f8d55..bf27f23a286 100644
--- a/code/game/alternate_appearance.dm
+++ b/code/game/alternate_appearance.dm
@@ -98,6 +98,13 @@
display_alt_appearance(key, displayTo)
+/*
+ Gets an alternate appearance for the provided key
+ key - the key to the assoc list of key = /datum/alternate_appearances
+*/
+/atom/proc/get_alt_appearance(key)
+ return alternate_appearances?[key]
+
//////////////
// WRAPPERS //
//////////////
diff --git a/code/game/jobs/job_exp.dm b/code/game/jobs/job_exp.dm
index 66f05b70858..1932504a9bc 100644
--- a/code/game/jobs/job_exp.dm
+++ b/code/game/jobs/job_exp.dm
@@ -19,6 +19,7 @@ GLOBAL_LIST_INIT(role_playtime_requirements, list(
ROLE_MORPH = 5,
ROLE_DEMON = 5,
ROLE_ELITE = 5,
+ ROLE_FLOCK = 20,
// DUO ANTAGS
ROLE_GUARDIAN = 20,
diff --git a/code/game/objects/effects/decals/Cleanable/alien_blood.dm b/code/game/objects/effects/decals/Cleanable/alien_blood.dm
index c8d6f5d3085..7eef3033eeb 100644
--- a/code/game/objects/effects/decals/Cleanable/alien_blood.dm
+++ b/code/game/objects/effects/decals/Cleanable/alien_blood.dm
@@ -68,3 +68,9 @@
/obj/effect/decal/cleanable/blood/gibs/down/vox
basecolor = "#2299FC"
fleshcolor = "#808D11"
+
+/obj/effect/decal/cleanable/blood/gibs/flock
+ basecolor = COLOR_BLOOD_FLOCK
+
+/obj/effect/decal/cleanable/blood/gibs/core/flock
+ basecolor = COLOR_BLOOD_FLOCK
diff --git a/code/game/objects/effects/gibs.dm b/code/game/objects/effects/gibs.dm
index f97b15c1112..d07250f91a9 100644
--- a/code/game/objects/effects/gibs.dm
+++ b/code/game/objects/effects/gibs.dm
@@ -7,6 +7,9 @@
/proc/xgibs(atom/location)
new /obj/effect/gibspawner/xeno(get_turf(location))
+/proc/flockgibs(atom/location)
+ new /obj/effect/gibspawner/flock(get_turf(location))
+
/proc/robogibs(atom/location)
new /obj/effect/gibspawner/robot(get_turf(location))
diff --git a/code/game/objects/effects/info_tags.dm b/code/game/objects/effects/info_tags.dm
new file mode 100644
index 00000000000..c70ff686740
--- /dev/null
+++ b/code/game/objects/effects/info_tags.dm
@@ -0,0 +1,63 @@
+/obj/effect/abstract/info_tag
+ plane = INFO_TAG_PLANE
+ layer = 1
+ alpha = 180
+
+ appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
+ vis_flags = VIS_INHERIT_ID
+
+ /// Atom we're displaying over.
+ var/atom/parent
+
+ /// Mobs that see us rn.
+ var/list/mob/viewing_mobs
+
+ /// Images we're inserting into client.images
+ VAR_PRIVATE/image/vis_holder
+
+
+/obj/effect/abstract/info_tag/Initialize(mapload)
+ . = ..()
+ vis_holder = new()
+ vis_holder.plane = INFO_TAG_PLANE
+ vis_holder.vis_contents += src
+
+/obj/effect/abstract/info_tag/Destroy(force)
+ set_parent(null)
+ vis_holder = null
+
+ for(var/mob/M in viewing_mobs)
+ hide_from(M.client)
+
+ viewing_mobs = null
+ return ..()
+
+/obj/effect/abstract/info_tag/proc/set_parent(atom/new_parent)
+ parent = new_parent
+ vis_holder.loc = parent
+
+/// Update the text.
+/obj/effect/abstract/info_tag/proc/set_text(text)
+ maptext = "[text]"
+
+/// Should the given mob be able to see this tag?
+/obj/effect/abstract/info_tag/proc/mob_should_see(mob/M)
+ return TRUE
+
+/obj/effect/abstract/info_tag/proc/show_to(client/C)
+ C?.images |= vis_holder
+
+/obj/effect/abstract/info_tag/proc/hide_from(client/C)
+ C?.images -= vis_holder
+
+/obj/effect/abstract/info_tag/flock
+ maptext_x = -64
+ maptext_y = -6
+ maptext_width = 160
+ maptext_height = 48
+
+/obj/effect/abstract/info_tag/flock/mob_should_see(mob/M)
+ return (M != parent) && isflockmob(M)
+
+/obj/effect/abstract/info_tag/flock/info
+ maptext_y = -14
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index 05fc1c8e89f..b69c2923ff3 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -104,6 +104,10 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark/newplayer_start) //Without this you sp
name = "revenantspawn"
icon_state = "Rev"
+/obj/effect/landmark/spawner/flock
+ name = "flockspawn"
+ icon_state = "flockmind"
+
/obj/effect/landmark/spawner/ninja
name = "ninjaspawn"
diff --git a/code/game/objects/effects/spawners/gibspawner.dm b/code/game/objects/effects/spawners/gibspawner.dm
index f1d1c92bb18..b04c6702607 100644
--- a/code/game/objects/effects/spawners/gibspawner.dm
+++ b/code/game/objects/effects/spawners/gibspawner.dm
@@ -54,3 +54,15 @@
/obj/effect/gibspawner/vox/gib_dna(obj/effect/decal/cleanable/blood/gibs/gib, datum/dna/mob_dna)
if(!..()) // Probably admin spawned
gib.blood_DNA["Unknown Vox DNA"] = "A+"
+
+/obj/effect/gibspawner/flock
+ gibtypes = list(/obj/effect/decal/cleanable/blood/gibs/flock, /obj/effect/decal/cleanable/blood/gibs/flock, /obj/effect/decal/cleanable/blood/gibs/core/flock)
+ gibamounts = list(2,2,1)
+
+/obj/effect/gibspawner/flock/New()
+ gibdirections = list(list(WEST, NORTHWEST, SOUTHWEST, NORTH), list(EAST, NORTHEAST, SOUTHEAST, SOUTH), list())
+ ..()
+
+/obj/effect/gibspawner/flock/gib_dna(obj/effect/decal/cleanable/blood/gibs/gib, datum/dna/mob_dna)
+ if(!..())
+ gib.blood_DNA["UNKNOWN DNA"] = "^*@!"
diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm
index 624eb2953a0..990c882ae6b 100644
--- a/code/game/objects/items/stacks/sheets/glass.dm
+++ b/code/game/objects/items/stacks/sheets/glass.dm
@@ -5,6 +5,7 @@
* 4. REINFORCED PLASMA GLASS
* 5. TITANIUM GLASS
* 6. PLASTITANIUM GLASS
+ * 7. GNESIS GLASS
Todo: Create a unified construct_window(sheet, user, created_window, full_window)
@@ -26,6 +27,13 @@ GLOBAL_LIST_INIT(glass_recipes, list (
new /datum/stack_recipe("dropper", /obj/item/reagent_containers/dropper, 1, time = 1 SECONDS),
))
+GLOBAL_LIST_INIT(gnesis_glass_recipes, list (
+ new /datum/stack_recipe("bright shard", /obj/item/shard/gnesis_glass, time = 0 SECONDS),
+ new /datum/stack_recipe/window("directional gnesis window", /obj/structure/window/flock, time = 1 SECONDS, on_floor = TRUE, window_checks = TRUE),
+ new /datum/stack_recipe/window("fulltile gnesis window", /obj/structure/window/flock/fulltile, 2, time = 2 SECONDS, on_floor = TRUE, window_checks = TRUE),
+ new /datum/stack_recipe("gnesis cache", /obj/item/reagent_containers/glass/gnesis, 5, time = 2 SECONDS),
+))
+
/obj/item/stack/sheet/glass
name = "glass"
desc = "HOLY SHEET! That is a lot of glass."
@@ -312,3 +320,21 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list(
. = ..()
recipes = GLOB.plastitaniumglass_recipes
+//////////////////////////////
+// MARK: GNESIS GLASS
+//////////////////////////////
+/obj/item/stack/sheet/gnesis_glass
+ name = "transculent wafers"
+ singular_name = "transculent wafer"
+ desc = "A rare, complex crystalline matrix with a lazily shifting internal structure. The layers are arranged to let light through."
+ icon_state = "gnesisglass"
+ materials = list(MAT_GNESIS_GLASS = MINERAL_MATERIAL_AMOUNT)
+ merge_type = /obj/item/stack/sheet/gnesis_glass
+ armor = list(MELEE = 20, BULLET = 20, LASER = 0, ENERGY = 100, BOMB = 0, RAD = 50, FIRE = 100, ACID = 20)
+ resistance_flags = FIRE_PROOF
+ point_value = 5
+ dynamic_icon_state = FALSE
+
+/obj/item/stack/sheet/gnesis_glass/Initialize(mapload, new_amount, merge)
+ . = ..()
+ recipes = GLOB.gnesis_glass_recipes
diff --git a/code/game/objects/items/stacks/sheets/mineral.dm b/code/game/objects/items/stacks/sheets/mineral.dm
index 948689a3955..0ab1b79ed5a 100644
--- a/code/game/objects/items/stacks/sheets/mineral.dm
+++ b/code/game/objects/items/stacks/sheets/mineral.dm
@@ -13,6 +13,7 @@ Mineral Sheets
- Platinum
- Alien Alloy
- Adamantine
+ - Gnesis
*/
GLOBAL_LIST_INIT(sandstone_recipes, list (
@@ -138,6 +139,13 @@ GLOBAL_LIST_INIT(adamantine_recipes, list(
new /datum/stack_recipe("incomplete servant golem shell", /obj/item/golem_shell/servant, req_amount = 1, res_amount = 1),
))
+GLOBAL_LIST_INIT(gnesis_recipes, list (
+ new /datum/stack_recipe("flock chair", /obj/structure/chair/comfy/flock, 2, time = 1 SECONDS, one_per_turf = TRUE, on_floor = TRUE),
+ new /datum/stack_recipe("flock closet", /obj/structure/closet/flock, 2, time = 10 SECONDS, one_per_turf = TRUE, on_floor = TRUE),
+ new /datum/stack_recipe("flock grille", /obj/structure/grille/flock, 1, time = 5 SECONDS, one_per_turf = TRUE, on_floor = TRUE),
+ new /datum/stack_recipe("flock door", /obj/machinery/door/flock, 10, time = 10 SECONDS, one_per_turf = TRUE, on_floor = TRUE),
+ ))
+
GLOBAL_LIST_INIT(snow_recipes, list(
new /datum/stack_recipe("snowman", /obj/structure/snowman, 5, time = 2.5 SECONDS, one_per_turf = TRUE, on_floor = TRUE),
new /datum/stack_recipe("Snowball", /obj/item/snowball, 1)
@@ -600,6 +608,23 @@ GLOBAL_LIST_INIT(plastitanium_recipes, list(
/obj/item/stack/sheet/mineral/adamantine/fifty
amount = 50
+/*
+ * Gnesis
+ */
+
+/obj/item/stack/sheet/gnesis
+ name = "wafers"
+ desc = "A rare, complex crystalline matrix with a lazily shifting internal structure. Not to be confused with gneiss, a metamorphic rock."
+ singular_name = "wafer"
+ icon_state = "gnesis"
+ materials = list(MAT_GNESIS = MINERAL_MATERIAL_AMOUNT)
+ merge_type = /obj/item/stack/sheet/gnesis
+ dynamic_icon_state = FALSE
+
+/obj/item/stack/sheet/gnesis/Initialize(mapload, new_amount, merge)
+ . = ..()
+ recipes = GLOB.gnesis_recipes
+
/*
* Snow
*/
diff --git a/code/game/objects/items/weapons/shards.dm b/code/game/objects/items/weapons/shards.dm
index d8239b0f146..56f5e03ffb4 100644
--- a/code/game/objects/items/weapons/shards.dm
+++ b/code/game/objects/items/weapons/shards.dm
@@ -105,3 +105,12 @@
materials = list(MAT_TITANIUM = MINERAL_MATERIAL_AMOUNT * 0.5, MAT_PLASMA = MINERAL_MATERIAL_AMOUNT * 0.5, MAT_GLASS = MINERAL_MATERIAL_AMOUNT)
icon_prefix = "plastitanium"
welded_type = /obj/item/stack/sheet/plastitaniumglass
+
+/obj/item/shard/gnesis_glass
+ name = "bright shard"
+ desc = "A jagged shard of weird alien computer crystal stuff."
+ color = "#1bdebd"
+ force = 7
+ throwforce = 12
+ materials = list(MAT_GNESIS_GLASS = MINERAL_MATERIAL_AMOUNT * 0.5)
+ welded_type = /obj/item/stack/sheet/gnesis_glass
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index 56f75059e65..59a5cd9a76f 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -19,6 +19,15 @@
if(obj_integrity <= 0)
obj_destruction(damage_flag)
+/**
+ * Retrieves the objects's current damage as a percentage where `100%` is `100`.
+ */
+/obj/proc/get_integrity_percentage()
+ return ceil(obj_integrity / max_integrity * 100)
+
+/obj/proc/get_integrity_lost()
+ return max_integrity - obj_integrity
+
///returns the damage value of the attack after processing the obj's various armor protections
/obj/proc/run_obj_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armor_penetration_flat = 0, armor_penetration_percentage = 0)
if(damage_flag == MELEE && damage_amount < damage_deflection)
diff --git a/code/game/turfs/simulated/walls_misc.dm b/code/game/turfs/simulated/walls_misc.dm
index 7f7108637d0..b9b63d1cc5c 100644
--- a/code/game/turfs/simulated/walls_misc.dm
+++ b/code/game/turfs/simulated/walls_misc.dm
@@ -107,3 +107,10 @@
sheet_type = /obj/item/stack/sheet/runed_metal
sheet_amount = 1
girder_type = /obj/structure/girder/cult
+
+/turf/simulated/wall/gnesis
+ name = "gnesis wall"
+ desc = "A thick metal wall, it look very old."
+ icon = 'icons/goonstation/turf/flock.dmi'
+ color = "#1bdebd"
+ sheet_type = /obj/item/stack/sheet/gnesis
diff --git a/code/modules/antagonists/flockmind/actions/cage_mob.dm b/code/modules/antagonists/flockmind/actions/cage_mob.dm
new file mode 100644
index 00000000000..eceb478d082
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/cage_mob.dm
@@ -0,0 +1,42 @@
+/datum/action/cooldown/flock/cage_mob
+ name = "Cage"
+ cooldown_time = 5 SECONDS
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/cage_mob/is_valid_target(atom/cast_on)
+ return isliving(cast_on) && !isflockmob(cast_on) && isturf(cast_on.loc)
+
+/datum/action/cooldown/flock/cage_mob/Activate(atom/target)
+ if(DOING_INTERACTION(owner, "flock_cage"))
+ return FALSE
+
+ var/mob/living/basic/flock/drone/bird = owner
+ bird.stop_flockphase(TRUE)
+ var/turf/T = get_turf(target)
+
+ owner.visible_message(
+ SPAN_NOTICE("[owner] begins forming a cuboid structure around [target]."),
+ blind_message = SPAN_HEAR("You hear a strange synthetic whirring."),
+ )
+
+ var/obj/effect/temp_visual/flock_effect
+ if(iswallturf(T))
+ flock_effect = new /obj/effect/temp_visual/flock/wall_convert(T)
+
+ log_attack(owner, target, "attempted to cage")
+
+ . = TRUE
+ playsound(owner, 'sound/goonstation/flockmind/flockdrone_build.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
+ if(!do_after(owner, 4.5 SECONDS, target = target, interaction_key = "flock_cage"))
+ . = FALSE
+
+ qdel(flock_effect)
+ if(!.)
+ return
+
+ log_attack(owner, target, "caged")
+
+ playsound(owner, 'sound/goonstation/flockmind/flockdrone_build_complete.ogg', 70, TRUE)
+ var/obj/structure/flock/cage/cage = new(T, bird.flock)
+ cage.cage_mob(target)
+ ..()
diff --git a/code/modules/antagonists/flockmind/actions/control_drone.dm b/code/modules/antagonists/flockmind/actions/control_drone.dm
new file mode 100644
index 00000000000..802faf92d93
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/control_drone.dm
@@ -0,0 +1,148 @@
+/datum/action/cooldown/flock/control_drone
+ name = "Control Drone"
+ desc = "Direct a drone to perform an action."
+ button_icon_state = "ping"
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ click_cd_override = 0
+
+ var/mob/living/basic/flock/drone/selected_bird
+
+/datum/action/cooldown/flock/control_drone/Remove(mob/removed_from)
+ . = ..()
+ free_drone()
+
+/datum/action/cooldown/flock/control_drone/is_valid_target(atom/cast_on)
+ . = ..()
+ if(selected_bird)
+ return get_turf(cast_on)
+
+ if(!isflockdrone(cast_on))
+ return FALSE
+
+ var/mob/living/basic/flock/drone/bird = cast_on
+ if(HAS_TRAIT_FROM(bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE))
+ return FALSE
+
+ if(bird.controlled_by)
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/flock/control_drone/Activate(atom/target)
+ . = ..()
+ if(isnull(selected_bird))
+ bind_drone(target)
+ return TRUE
+
+ selected_bird.ai_controller.cancel_actions()
+
+ // Move to turf/structure, or convert turf.
+ if(isturf(target) || istype(target, /obj/structure/flock))
+ if(istype(target, /obj/structure/flock/tealprint))
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_deposit_target, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+ var/turf/T = get_turf(target)
+ if(isflockturf(T))
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+ if(T.can_flock_convert())
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_conversion_target, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+
+ // Harvest items
+ else if(isitem(target))
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_harvest_target, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+ // Attack or convert enemies.
+ else if(ismob(target) && selected_bird.flock.is_mob_enemy(target))
+ var/mob/living/L = target
+ if(L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) || L.IsKnockedDown())
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_capture_target, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+ else if(selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/attack_target, target))
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+ return FALSE
+
+ else if(isflockmob(target))
+ if(istype(target, /mob/living/basic/flock/bit))
+ to_chat(owner, SPAN_WARNING("Flockbits are too simple to be remotely controlled."))
+ return FALSE
+
+ var/mob/living/basic/flock/other_bird = target
+ if(other_bird.flock == selected_bird.flock)
+ if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_heal_target, target))
+ return FALSE
+
+ pointer_helper(selected_bird, target, 2 SECONDS)
+ unset_click_ability(owner, performing_task = TRUE)
+ return TRUE
+
+
+/datum/action/cooldown/flock/control_drone/unset_click_ability(mob/on_who, refund_cooldown, performing_task = TRUE)
+ . = ..()
+ free_drone(performing_task)
+
+/datum/action/cooldown/flock/control_drone/proc/pointer_helper(atom/from, atom/towards, duration)
+ var/mob/camera/flock/overmind/ghost_bird = owner
+ var/image/pointer = pointer_image_to(from, towards)
+
+ animate(pointer, time = 2 SECONDS, alpha = 0)
+ ghost_bird.flock.add_ping_image(ghost_bird.client, pointer, 2 SECONDS)
+
+/datum/action/cooldown/flock/control_drone/proc/bind_drone(mob/living/basic/flock/drone/bird)
+ selected_bird = bird
+ RegisterSignal(bird, COMSIG_PARENT_QDELETING, PROC_REF(drone_gone))
+ ADD_TRAIT(bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE)
+ bird.ai_controller.cancel_actions()
+ bird.say("suspending automated subroutines pending sentient-level instruction", forced = TRUE)
+ bird.AddComponent(/datum/component/flock_ping/selected)
+
+/datum/action/cooldown/flock/control_drone/proc/free_drone(performing_task = TRUE)
+ if(!selected_bird)
+ return
+
+ if(!QDELETED(selected_bird) && !performing_task)
+ spawn(-1)
+ selected_bird.say("Sentient-level instruction suspended, resuming automated subroutines.", forced = TRUE)
+
+ UnregisterSignal(selected_bird, COMSIG_PARENT_QDELETING)
+ REMOVE_TRAIT(selected_bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE)
+
+ qdel(selected_bird.GetComponent(/datum/component/flock_ping/selected))
+ selected_bird = null
+ unset_click_ability(owner)
+
+/datum/action/cooldown/flock/control_drone/proc/drone_gone(datum/source)
+ SIGNAL_HANDLER
+
+ free_drone()
+
+
diff --git a/code/modules/antagonists/flockmind/actions/create_rift.dm b/code/modules/antagonists/flockmind/actions/create_rift.dm
new file mode 100644
index 00000000000..4d2b018fe43
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/create_rift.dm
@@ -0,0 +1,27 @@
+/datum/action/cooldown/flock/create_rift
+ name = "Spawn Rift"
+ desc = "Spawn an rift where you are, and from there, begin."
+ button_icon_state = "spawn_egg"
+
+/datum/action/cooldown/flock/create_rift/is_valid_target(atom/cast_on)
+ var/turf/T = get_turf(owner)
+ if(!T.is_safe())
+ to_chat(owner, SPAN_WARNING("This place is not safe enough for a rift."))
+ return FALSE
+
+ if(T.is_blocked_turf())
+ to_chat(owner, SPAN_WARNING("There is not enough room for a rift here."))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/create_rift/Activate(atom/target)
+ . = ..()
+
+ var/turf/T = get_turf(owner)
+ if(tgui_alert(owner, "Would you like to create the rift on [T]?", "Spawn Rift", list("Yes", "No")) != "Yes")
+ return
+
+ var/mob/camera/flock/overmind/ghost_bird = owner
+ ghost_bird.spawn_rift(T)
+
diff --git a/code/modules/antagonists/flockmind/actions/create_structure.dm b/code/modules/antagonists/flockmind/actions/create_structure.dm
new file mode 100644
index 00000000000..566fe9ec0da
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/create_structure.dm
@@ -0,0 +1,49 @@
+/datum/action/cooldown/flock/create_structure
+ name = "Place Tealprint"
+ desc = "Create a structure tealprint for your drones to construct onto."
+ button_icon_state = "fabstructure"
+
+/datum/action/cooldown/flock/create_structure/is_valid_target(atom/cast_on)
+ var/turf/simulated/floor/flock/T = get_turf(owner)
+ if(!istype(T))
+ to_chat(owner, SPAN_WARNING("We are unable to perform complex substrate manipulation outside of our tiles."))
+ return FALSE
+
+ if(T.broken)
+ to_chat(owner, SPAN_WARNING("The tile we are above is broken."))
+ return FALSE
+
+ if(locate(/obj/structure/flock/tealprint) in T)
+ to_chat(owner, SPAN_WARNING("We are currently building something there."))
+ return FALSE
+
+ if(locate(/obj/structure/flock) in T)
+ to_chat(owner, SPAN_WARNING("An existing structure is occupying that tile."))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/create_structure/Activate(atom/target)
+ var/mob/camera/flock/overmind/ghost_bird = owner
+ var/datum/flock/F = ghost_bird.flock
+ var/turf/loc = get_turf(ghost_bird)
+
+ var/options = list()
+ for(var/datum/flock_unlockable/unlockable as anything in F.unlockables)
+ if(!unlockable.unlocked)
+ continue
+ options[unlockable.name] = unlockable
+
+ var/datum/flock_unlockable/desired = tgui_input_list(ghost_bird, "Select a structure to create. Bandwidth upkeep costs are provided in parenthesis.", "Tealprint Selection", options)
+ desired = options[desired]
+
+ if(isnull(desired))
+ return
+
+ F.refresh_unlockables()
+ if(!desired.unlocked)
+ return
+
+ F.create_structure(loc, desired.structure_type)
+
+ return ..()
diff --git a/code/modules/antagonists/flockmind/actions/deconstruct.dm b/code/modules/antagonists/flockmind/actions/deconstruct.dm
new file mode 100644
index 00000000000..cff7dfbe171
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/deconstruct.dm
@@ -0,0 +1,43 @@
+/datum/action/cooldown/flock/deconstruct
+ name = "Deconstruct"
+ click_to_activate = TRUE
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/cooldown/flock/deconstruct/is_valid_target(atom/cast_on)
+ var/turf/clicked_atom_turf = get_turf(cast_on)
+ if(!clicked_atom_turf)
+ return FALSE
+
+ if(get_dist(owner, clicked_atom_turf) > 1)
+ return FALSE
+
+ if(ismob(cast_on) || !HAS_TRAIT(cast_on, TRAIT_FLOCK_THING) || HAS_TRAIT(cast_on, TRAIT_FLOCK_NODECON))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/deconstruct/Activate(atom/target)
+ . = ..()
+ var/mob/living/basic/flock/bird = owner
+ astype(bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
+ bird.face_atom(target)
+
+ bird.visible_message(SPAN_NOTICE("[bird] begins to deconstruct [target]."), blind_message = SPAN_HEAR("You hear an otherwordly whirring."))
+ if(!do_after(bird, 6 SECONDS, target = target, interaction_key = "flock_deconstruct"))
+ return FALSE
+
+ if(!is_valid_target(target))
+ return FALSE
+
+ // Structures
+ if(isobj(target))
+ var/obj/obj_target = target
+ obj_target.deconstruct(TRUE)
+ return TRUE
+
+ else if(isflockturf(target))
+ var/turf/turf = target
+ turf.ChangeTurf(/turf/simulated/floor/flock, ignore_air = TRUE)
+ else
+ CRASH("Tried to flock deconstruct incompatible object of type: [target.type]")
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/actions/deposit.dm b/code/modules/antagonists/flockmind/actions/deposit.dm
new file mode 100644
index 00000000000..b44f67968a7
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/deposit.dm
@@ -0,0 +1,23 @@
+/datum/action/cooldown/flock/deposit
+ name = "Deposit"
+ click_to_activate = TRUE
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/cooldown/flock/deposit/is_valid_target(atom/cast_on)
+ return istype(cast_on, /obj/structure/flock/tealprint)
+
+/datum/action/cooldown/flock/deposit/Activate(atom/target)
+ . = ..()
+ var/mob/living/basic/flock/bird = owner
+ astype(bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
+ bird.face_atom(target)
+
+ playsound(target, 'sound/goonstation/flockmind/flockdrone_quickbuild.ogg', 50, TRUE)
+
+ while(do_after(bird, 1 SECONDS, target = target, interaction_key = "flock_deposit"))
+ var/obj/structure/flock/tealprint/tealprint = target
+ var/deposit = min(bird.substrate.has_points(), tealprint.substrate.how_empty(), FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT)
+ bird.substrate.remove_points(deposit)
+ tealprint.substrate.add_points(deposit)
+ return TRUE
+
diff --git a/code/modules/antagonists/flockmind/actions/designate_deconstruct.dm b/code/modules/antagonists/flockmind/actions/designate_deconstruct.dm
new file mode 100644
index 00000000000..1a89120830d
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/designate_deconstruct.dm
@@ -0,0 +1,18 @@
+/datum/action/cooldown/flock/designate_deconstruct
+ name = "Mark for Deconstruction"
+ desc = "Mark an existing flock structure for deconstruction, refunding some resources."
+ button_icon_state = "destroystructure"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ click_cd_override = 0
+
+/datum/action/cooldown/flock/designate_deconstruct/Activate(atom/target)
+ . = ..()
+
+ if(istype(target, /obj/structure/flock/tealprint))
+ var/obj/structure/flock/tealprint/tealprint = target
+ return tealprint.try_cancel_structure()
+
+ var/mob/camera/flock/ghost_bird = owner
+ return ghost_bird.flock.toggle_deconstruction_mark(target)
diff --git a/code/modules/antagonists/flockmind/actions/designate_enemy.dm b/code/modules/antagonists/flockmind/actions/designate_enemy.dm
new file mode 100644
index 00000000000..b38a6ad78ff
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/designate_enemy.dm
@@ -0,0 +1,65 @@
+/datum/action/cooldown/flock/designate_enemy
+ name = "Designate Enemy"
+ desc = "Mark or unmark someone as an enemy."
+ button_icon_state = "designate_enemy"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ click_cd_override = 0
+
+/datum/action/cooldown/flock/designate_enemy/is_valid_target(atom/cast_on)
+ var/atom/movable/target_movable = cast_on
+ if(!istype(target_movable))
+ return FALSE
+ return !isflockmob(cast_on) && isliving(cast_on) || target_movable.has_buckled_mobs()
+
+/datum/action/cooldown/flock/designate_enemy/Activate(atom/target)
+ var/mob/camera/flock/ghost_bird = owner
+ var/datum/flock/flock = ghost_bird.flock
+ if(!flock)
+ return FALSE
+
+ . = ..()
+
+ if(flock.is_mob_ignored(target))
+ flock.remove_ignore(target)
+
+ else if(flock.is_mob_enemy(target))
+ flock.remove_enemy(target)
+ return TRUE
+
+ flock.update_enemy(target)
+ return TRUE
+
+/datum/action/cooldown/flock/designate_ignore
+ name = "Designate Ignore"
+ desc = "Designate someone to be ignored by your Flock."
+ button_icon_state = "designate_ignore"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+
+/datum/action/cooldown/flock/designate_ignore/is_valid_target(atom/cast_on)
+ var/atom/movable/target_movable = cast_on
+ if(!istype(target_movable))
+ return FALSE
+ return !isflockmob(cast_on) && isliving(cast_on) || target_movable.has_buckled_mobs()
+
+/datum/action/cooldown/flock/designate_ignore/Activate(atom/target)
+ var/mob/camera/flock/ghost_bird = owner
+ var/datum/flock/flock = ghost_bird.flock
+ if(!flock)
+ return FALSE
+
+ . = ..()
+
+ if(flock.is_mob_ignored(target))
+ flock.remove_ignore(target)
+
+ else if(flock.is_mob_enemy(target))
+ flock.remove_enemy(target)
+ return TRUE
+
+ flock.add_ignore(target)
+ return TRUE
+
diff --git a/code/modules/antagonists/flockmind/actions/designate_turf.dm b/code/modules/antagonists/flockmind/actions/designate_turf.dm
new file mode 100644
index 00000000000..29f1541de06
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/designate_turf.dm
@@ -0,0 +1,37 @@
+/datum/action/cooldown/flock/designate_tile
+ name = "Designate Priority Tile"
+ desc = "Add or remove a tile to the urgent tiles the Flock should claim."
+ button_icon_state = "designate_tile"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ click_cd_override = 0
+
+/datum/action/cooldown/flock/designate_tile/is_valid_target(atom/cast_on)
+ var/turf/T = get_turf(cast_on)
+ return T?.can_flock_convert()
+
+/datum/action/cooldown/flock/designate_tile/Activate(atom/target)
+ var/turf/turf_target = get_turf(target)
+ if(isflockturf(target))
+ to_chat(owner, SPAN_ALERT("That tile has already been converted by the flock."))
+ return FALSE
+
+ if(!turf_target.can_flock_convert())
+ to_chat(owner, SPAN_ALERT("The flock is unable to convert that."))
+ return FALSE
+
+ var/mob/camera/flock/ghost_bird = owner
+ if(!ghost_bird.flock.marked_for_conversion[turf_target])
+ if(ghost_bird.flock.turf_reservations[turf_target])
+ to_chat(ghost_bird, SPAN_ALERT("That tile is already scheduled for conversion."))
+ return FALSE
+
+ ghost_bird.flock.marked_for_conversion[turf_target] = TRUE
+ ghost_bird.flock.add_notice(turf_target, FLOCK_NOTICE_PRIORITY)
+ return TRUE
+
+ else
+ ghost_bird.flock.marked_for_conversion -= turf_target
+ ghost_bird.flock.remove_notice(turf_target, FLOCK_NOTICE_PRIORITY)
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/actions/diffract_drone.dm b/code/modules/antagonists/flockmind/actions/diffract_drone.dm
new file mode 100644
index 00000000000..97a4d779d89
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/diffract_drone.dm
@@ -0,0 +1,23 @@
+/datum/action/cooldown/flock/diffract_drone
+ name = "Diffract Drone"
+ desc = "Split a drone into flockbits, mindless automata that only convert whatever they find."
+ button_icon_state = "diffract"
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/diffract_drone/is_valid_target(atom/cast_on)
+ if(!isflockdrone(cast_on))
+ return FALSE
+
+ var/mob/living/basic/flock/drone/bird = cast_on
+ if(bird.stat == DEAD)
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/diffract_drone/PreActivate(atom/target)
+ . = ..()
+
+/datum/action/cooldown/flock/diffract_drone/Activate(atom/target)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = target
+ bird.split_into_bits()
diff --git a/code/modules/antagonists/flockmind/actions/flock_action.dm b/code/modules/antagonists/flockmind/actions/flock_action.dm
new file mode 100644
index 00000000000..0d00ad68411
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_action.dm
@@ -0,0 +1,3 @@
+/datum/action/cooldown/flock
+ button_icon = 'icons/goonstation/hud/flock_ui.dmi'
+ background_icon = null
diff --git a/code/modules/antagonists/flockmind/actions/flock_control_panel.dm b/code/modules/antagonists/flockmind/actions/flock_control_panel.dm
new file mode 100644
index 00000000000..1c7b77fbe3c
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_control_panel.dm
@@ -0,0 +1,9 @@
+/datum/action/cooldown/flock/control_panel
+ name = "Flock Control Panel"
+ desc = "Open the Flock control panel."
+ button_icon_state = "radio_stun"
+
+/datum/action/cooldown/flock/control_panel/Activate(atom/target)
+ . = ..()
+ var/mob/camera/flock/ghost_bird = owner
+ ghost_bird.flock.ui_interact(ghost_bird)
diff --git a/code/modules/antagonists/flockmind/actions/flock_convert_action.dm b/code/modules/antagonists/flockmind/actions/flock_convert_action.dm
new file mode 100644
index 00000000000..0060082cc32
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_convert_action.dm
@@ -0,0 +1,67 @@
+/datum/action/cooldown/flock/convert
+ name = "Convert"
+ click_to_activate = TRUE
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/cooldown/flock/convert/New(Target)
+ . = ..()
+
+/datum/action/cooldown/flock/convert/Destroy()
+ return ..()
+
+/datum/action/cooldown/flock/convert/is_valid_target(atom/cast_on)
+ var/turf/clicked_atom_turf = get_turf(cast_on)
+ if(!clicked_atom_turf)
+ return FALSE
+
+ // This breaks pathfinding :(
+ // if(!owner.CanReach(clicked_atom_turf))
+ // return FALSE
+
+ // So instead:
+ if(get_dist(owner, clicked_atom_turf) > 1)
+ return FALSE
+
+ if(!clicked_atom_turf.can_flock_convert())
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/convert/Activate(atom/target)
+ . = ..()
+ playsound(owner, 'sound/goonstation/flockmind/flockdrone_convert.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
+
+ var/turf/clicked_atom_turf = get_turf(target)
+
+ if(isfloorturf(clicked_atom_turf) || iswallturf(clicked_atom_turf))
+ return convert_turf(clicked_atom_turf)
+
+ return FALSE
+
+/datum/action/cooldown/flock/convert/proc/convert_turf(turf/T)
+
+ if(!is_valid_target(T))
+ return FALSE
+
+ var/mob/living/basic/flock/bird = owner
+
+ var/obj/effect/temp_visual/flock_effect
+
+ if(iswallturf(T))
+ flock_effect = new /obj/effect/temp_visual/flock/wall_convert(T)
+ else
+ flock_effect = new /obj/effect/temp_visual/flock/floor_convert(T)
+
+ owner.face_atom(T)
+
+ bird.flock?.reserve_turf(bird, T)
+ if(!do_after(owner, 4.5 SECONDS, target = T, interaction_key = "flock_convert"))
+ qdel(flock_effect)
+ bird.flock?.free_turf(bird)
+ return FALSE
+
+ bird.flock?.free_turf(bird)
+ qdel(flock_effect)
+
+ bird.flock?.claim_turf(T)
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/actions/flock_heal_action.dm b/code/modules/antagonists/flockmind/actions/flock_heal_action.dm
new file mode 100644
index 00000000000..6dd822f22f9
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_heal_action.dm
@@ -0,0 +1,24 @@
+/datum/action/cooldown/flock/flock_heal
+ name = "Repair"
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/flock_heal/Activate(atom/target)
+ var/mob/living/basic/flock/drone/this_bird = owner
+ if(!this_bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR))
+ return FALSE
+
+ astype(this_bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
+
+ if(isflockmob(target))
+ var/mob/living/basic/flock/bird = target
+ ADD_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
+
+ if(!do_after(owner, 1 SECONDS, target = target, interaction_key = "flock_repair"))
+ REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
+ return FALSE
+
+ REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
+ bird.heal_overall_damage(10, 10)
+ this_bird.substrate.remove_points(FLOCK_SUBSTRATE_COST_REPAIR)
+ ..()
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/actions/flock_heal_surge.dm b/code/modules/antagonists/flockmind/actions/flock_heal_surge.dm
new file mode 100644
index 00000000000..5b3a1f91f6f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_heal_surge.dm
@@ -0,0 +1,30 @@
+/datum/action/cooldown/flock/repair_burst
+ name = "Concentrated Repair Burst"
+ desc = "Transmit large amounts of energy into up to four drones in a small area, healing them up to half of their hitpoints. Does not work on dead drones."
+ button_icon_state = "heal_drone"
+ cooldown_time = 30 SECONDS
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/repair_burst/Activate(atom/target)
+ var/list/targets = list()
+ for(var/mob/living/basic/flock/drone/bird in range(3, get_turf(target)))
+ if(bird.stat == DEAD)
+ continue
+
+ targets += bird
+ if(length(targets) >= 4)
+ break
+
+ if(!length(targets))
+ to_chat(owner, SPAN_ALERT("No repairable drones in range."))
+ return FALSE
+
+ . = ..()
+
+ to_chat(owner, SPAN_NOTICE("You transmit energy into your drones!"))
+ playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50)
+
+ for(var/mob/living/basic/flock/drone/bird as anything in targets)
+ playsound(bird, "sound/effects/radio_sweep[rand(1,5)].ogg", 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ new /obj/effect/temp_visual/flock/flock_heal_burst(get_turf(bird))
+ bird.adjustHealth(-50)
diff --git a/code/modules/antagonists/flockmind/actions/flock_nest_action.dm b/code/modules/antagonists/flockmind/actions/flock_nest_action.dm
new file mode 100644
index 00000000000..43dce4c61b6
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_nest_action.dm
@@ -0,0 +1,44 @@
+/datum/action/cooldown/flock/nest
+ name = "Lay Egg"
+ button_icon_state = "spawn_egg"
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/cooldown/flock/nest/is_valid_target(atom/cast_on)
+ var/mob/living/basic/flock/drone/bird = owner
+ var/turf/simulated/floor/flock/flockfloor = get_turf(cast_on)
+ if(!istype(flockfloor) || flockfloor.is_blocked_turf(source_atom = owner))
+ to_chat(owner, SPAN_WARNING("The egg must be placed on an open flock tile."))
+ return FALSE
+
+ if(!bird.flock)
+ to_chat(owner, SPAN_WARNING("Our prime directives prohibit us from synthesizing an egg."))
+ return FALSE
+
+ if(!bird.substrate.has_points(bird.flock.current_egg_cost))
+ to_chat(owner, SPAN_WARNING("Error: Not enough resources. (We need [bird.flock.current_egg_cost])"))
+ return FALSE
+
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ to_chat(owner, SPAN_WARNING("We can not synthesize eggs while flockrunning."))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/flock/nest/Activate(atom/target)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = owner
+ if(!bird.substrate.has_points(bird.flock?.current_egg_cost || FLOCK_SUBSTRATE_COST_LAY_EGG))
+ return FALSE
+ var/turf/simulated/floor/flock/flockfloor = get_turf(target)
+ bird.stop_flockphase(TRUE)
+
+ to_chat(bird, SPAN_NOTICE("Our internal fabricators spring into action, we must hold still."))
+
+ if(!do_after_once(bird, 8 SECONDS, needhand = FALSE, target = bird.loc, interaction_key = "flock_lay_egg"))
+ return FALSE
+
+ bird.substrate.remove_points(bird.flock.current_egg_cost)
+ bird.visible_message(SPAN_NOTICE("[bird] deploys some sort of device."))
+
+ new /obj/structure/flock/egg(flockfloor)
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/actions/flock_ping_action.dm b/code/modules/antagonists/flockmind/actions/flock_ping_action.dm
new file mode 100644
index 00000000000..1be06a6e1f2
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/flock_ping_action.dm
@@ -0,0 +1,14 @@
+/datum/action/cooldown/flock/ping
+ name = "Ping"
+ desc = "Request attention from other partitions of the Flock."
+ button_icon_state = "ping"
+ cooldown_time = 0.3 SECONDS
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/ping/is_valid_target(atom/cast_on)
+ return get_turf(cast_on)
+
+/datum/action/cooldown/flock/ping/Activate(atom/target)
+ . = ..()
+ var/mob/camera/flock/ghost_bird = owner
+ ghost_bird.flock.ping(target, ghost_bird)
diff --git a/code/modules/antagonists/flockmind/actions/gatecrash.dm b/code/modules/antagonists/flockmind/actions/gatecrash.dm
new file mode 100644
index 00000000000..ca3e1aff811
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/gatecrash.dm
@@ -0,0 +1,44 @@
+/datum/action/cooldown/flock/gatecrash
+ name = "Gatecrash"
+ desc = "Transmit an override signal to open every door within range."
+ button_icon_state = "open_door"
+
+ cooldown_time = 40 SECONDS
+
+/datum/action/cooldown/flock/gatecrash/Activate(atom/target)
+ var/list/targets = list()
+ for(var/obj/machinery/door/airlock/airlock in range(10, get_turf(owner)))
+ if(airlock.canAIControl())
+ targets += airlock
+
+ for(var/obj/mecha/mech in range(10, get_turf(owner)))
+ if(mech.occupant)
+ targets += mech
+
+ if(!length(targets))
+ to_chat(owner, SPAN_NOTICE("No targets in range that can be opened via radio."))
+ return FALSE
+
+ ..()
+
+ to_chat(owner, SPAN_NOTICE("You force open all powered doors around you."))
+ playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50)
+
+ addtimer(CALLBACK(src, PROC_REF(async_open_doors), targets), 1.5 SECONDS)
+ return TRUE
+
+/datum/action/cooldown/flock/gatecrash/proc/async_open_doors(list/doors)
+ for(var/obj/target in doors)
+ if(QDELETED(target))
+ continue
+
+ // Stagger openins a little randomly
+ if(prob(20))
+ sleep(0.2 SECONDS)
+
+ if(isairlock(target))
+ var/obj/machinery/door/airlock/airlock = target
+ INVOKE_ASYNC(airlock, TYPE_PROC_REF(/obj/machinery/door, open))
+ if(ismecha(target))
+ var/obj/mecha/mech = target
+ INVOKE_ASYNC(mech, TYPE_PROC_REF(/obj/mecha, go_out))
diff --git a/code/modules/antagonists/flockmind/actions/partition_mind.dm b/code/modules/antagonists/flockmind/actions/partition_mind.dm
new file mode 100644
index 00000000000..763a069f96f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/partition_mind.dm
@@ -0,0 +1,83 @@
+/datum/action/cooldown/flock/partition_mind
+ name = "Partition Mind"
+ button_icon_state = "partition_mind"
+ cooldown_time = 60 SECONDS
+
+ var/awaiting_partition = FALSE
+
+/datum/action/cooldown/flock/partition_mind/New()
+ ..()
+ desc = "Divide our computational power, creating a Flocktrace. Requires [FLOCK_COMPUTE_COST_FLOCKTRACE] total bandwidth per trace."
+
+/datum/action/cooldown/flock/partition_mind/is_valid_target(atom/cast_on)
+ var/mob/camera/flock/overmind/ghost_bird = owner
+ if(awaiting_partition)
+ to_chat(ghost_bird, SPAN_WARNING("We are currently partitioning."))
+ return FALSE
+
+ if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
+ to_chat(ghost_bird, SPAN_WARNING("The Flock does not have enough spare computaional power to support another thread."))
+ return FALSE
+
+ if(length(ghost_bird.flock.traces) >= ghost_bird.flock.max_traces)
+ if(length(ghost_bird.flock.traces) < floor(FLOCK_COMPUTE_COST_RELAY / FLOCK_COMPUTE_COST_FLOCKTRACE))
+ to_chat(ghost_bird, SPAN_WARNING("The Flock needs more total computational power to support another thread."))
+ else
+ to_chat(ghost_bird, SPAN_WARNING("The Flock has hit it's thread limit."))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/flock/partition_mind/Activate(atom/target)
+ . = ..()
+ do_partition()
+
+/datum/action/cooldown/flock/partition_mind/proc/do_partition()
+ set waitfor = FALSE
+ var/mob/camera/flock/overmind/ghost_bird = owner
+ if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
+ to_chat(ghost_bird, SPAN_WARNING("Partition failure: bandwidth required is unavailable."))
+ return
+
+ awaiting_partition = TRUE
+
+ to_chat(ghost_bird, SPAN_NOTICE("Partitioning initiated, stand by..."))
+
+ log_admin("Sending Flocktrace offer to ghosts, they have [30 SECONDS] to respond.")
+ var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as a Flocktrace?", ROLE_FLOCK, TRUE, source = /mob/camera/flock/trace)
+
+ awaiting_partition = FALSE
+
+ if(QDELETED(owner))
+ log_admin("The Flockmind lost before the partition could complete.")
+ return
+
+ if(!length(candidates))
+ log_admin("No ghosts responded to the flocktrace offer from [ghost_bird.real_name]")
+ to_chat(ghost_bird, SPAN_WARNING("Partition failure: unable to coalesce sentience."))
+ return
+
+ if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
+ log_admin("The Flock was unable to support another flocktrace, partition aborted.")
+ to_chat(ghost_bird, SPAN_WARNING("Partition failure: bandwidth required is unavailable."))
+ return
+
+ var/mob/candidate = pick(candidates)
+
+ dust_if_respawnable(candidate)
+ log_admin("[key_name_admin(candidate)] respawned as a Flocktrace.")
+
+ var/mob/camera/flock/trace/new_ghostbird = new(get_turf(ghost_bird), ghost_bird.flock)
+ new_ghostbird.key = candidate.key
+ new_ghostbird.mind = new
+ new_ghostbird.mind.bind_to(new_ghostbird)
+ new_ghostbird.mind.set_original_mob(new_ghostbird)
+ new_ghostbird.mind.assigned_role = SPECIAL_ROLE_FLOCK
+ new_ghostbird.mind.special_role = SPECIAL_ROLE_FLOCK
+ var/list/messages = list()
+ messages += "
You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]
"
+ messages += "" + gradient_text("We have been partitioned by our overmind to further their goals of propogating The Signal. It is our task to assist in managing their drones to achieve their goals. Such is the will of the Monarch.", "#3cb5a3", "#1e806e") + "
"
+ messages += "" + gradient_text("Our powers are more limited than that of our overmind, but we can utilize gatecrash subsystems and take manual control of drones (using alt-click).", "#3cb5a3", "#1e806e") + "
"
+ messages += "" + gradient_text("For more information, we have decrypted an information manifest: ([GLOB.configuration.url.wiki_url]/index.php/The_Divine_Flock)", "#3cb5a3", "#1e806e") + "
"
+ to_chat(new_ghostbird, chat_box_red(messages.Join("
")))
+ new_ghostbird.playsound_local(new_ghostbird, 'sound/goonstation/flockmind/ArtifactFea2.ogg', 50, FALSE, use_reverb = FALSE)
+ SSticker.mode.traitors |= new_ghostbird.mind
diff --git a/code/modules/antagonists/flockmind/actions/radio_blast.dm b/code/modules/antagonists/flockmind/actions/radio_blast.dm
new file mode 100644
index 00000000000..34eaebc34b5
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/radio_blast.dm
@@ -0,0 +1,39 @@
+/datum/action/cooldown/flock/radio_blast
+ name = "Radio Stun Burst"
+ desc = "Overwhelm the radio headsets of everyone within 3m of your target. Will not work on broken or non-existent headsets."
+ button_icon_state = "radio_stun"
+ cooldown_time = 20 SECONDS
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/radio_blast/Activate(atom/target)
+ var/list/targets = list()
+ for(var/mob/living/carbon/human/guy in range(3, get_turf(target)))
+ var/obj/item/radio/worn_radio
+ for(var/obj/item/I in guy.get_contents())
+ if(istype(I, /obj/item/radio))
+ worn_radio = I
+ break
+ if(!istype(worn_radio) || !guy.can_hear() || !worn_radio.listening)
+ continue
+
+ targets += guy
+
+ if(!length(targets))
+ to_chat(owner, SPAN_ALERT("No targets are in range with active headsets."))
+ return FALSE
+
+ . = ..()
+
+ to_chat(owner, SPAN_NOTICE("You transmit the worst static you can weave into the headsets around you."))
+ playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50, FALSE)
+
+ for(var/mob/living/carbon/human/guy as anything in targets)
+ to_chat(guy, SPAN_ALERT("Horrifying static bursts into your headset, disorienting you severely!"))
+ playsound(guy, "sound/effects/radio_sweep[rand(1,5)].ogg", 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ guy.KnockDown(3 SECONDS)
+ guy.SetConfused(20 SECONDS)
+ guy.do_jitter_animation(10)
+
+ var/obj/item/organ/internal/ears/ears = guy.get_int_organ(/obj/item/organ/internal/ears)
+ if(istype(ears))
+ ears.receive_damage(rand(3, 6))
diff --git a/code/modules/antagonists/flockmind/actions/radio_talk.dm b/code/modules/antagonists/flockmind/actions/radio_talk.dm
new file mode 100644
index 00000000000..7ab8d4c3c74
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/radio_talk.dm
@@ -0,0 +1,40 @@
+/datum/action/cooldown/flock/radio_talk
+ name = "Narrowbeam Transmission"
+ desc = "Hijack a radio headset of a target to broadcast a message."
+ button_icon_state = "talk"
+ cooldown_time = 2 SECONDS
+ click_to_activate = TRUE
+
+/datum/action/cooldown/flock/radio_talk/Activate(atom/target)
+ var/obj/item/radio/worn_radio
+
+
+ if(!ishuman(target) && !istype(target, /obj/item/radio))
+ to_chat(owner, SPAN_ALERT("[target] is not a valid target!"))
+ return FALSE
+
+ if(istype(target, /obj/item/radio))
+ worn_radio = target
+ if(!worn_radio.listening)
+ to_chat(owner, SPAN_ALERT("[target] is not turned on!"))
+ return FALSE
+
+ if(ishuman(target))
+ var/mob/living/carbon/human/schmuck = target
+ for(var/obj/item/I in schmuck.get_contents())
+ if(istype(I, /obj/item/radio))
+ worn_radio = I
+ break
+ if(!istype(worn_radio) || !worn_radio.listening)
+ to_chat(owner, SPAN_ALERT("[target] does not have a working radio!"))
+ return FALSE
+
+ var/msg = tgui_input_text(usr, "What do you wish to broadcast?", null, "")
+ if(!msg)
+ return FALSE
+
+ . = ..()
+
+ playsound(get_turf(worn_radio), "sound/effects/radio_sweep[rand(1,5)].ogg", 20, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ to_chat(owner, SPAN_NOTICE("You transmit a message through [worn_radio]!"))
+ worn_radio.autosay(istype(target, /obj/item/radio) ? Gibberish(msg, 70, 50) : msg, "Strange Static", null)
diff --git a/code/modules/antagonists/flockmind/actions/release_control.dm b/code/modules/antagonists/flockmind/actions/release_control.dm
new file mode 100644
index 00000000000..f4c55cc9487
--- /dev/null
+++ b/code/modules/antagonists/flockmind/actions/release_control.dm
@@ -0,0 +1,13 @@
+/datum/action/cooldown/flock/release_control
+ name = "Release Control"
+ desc = "Release yourself from this drone."
+ button_icon_state = "eject"
+
+/datum/action/cooldown/flock/release_control/Activate(atom/target)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = owner
+ if(!bird.flock)
+ to_chat(bird, SPAN_WARNING("You have no flock to return to."))
+ return
+
+ bird.release_control(TRUE)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/_flock_behavior_base.dm b/code/modules/antagonists/flockmind/ai_behaviors/_flock_behavior_base.dm
new file mode 100644
index 00000000000..fc8a9a1fc7f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/_flock_behavior_base.dm
@@ -0,0 +1,19 @@
+/datum/ai_behavior/flock
+ /// The name of the behavior in the UI for flock drones.
+ var/name = ""
+
+/datum/ai_behavior/flock/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ bird.set_task_desc(name)
+
+/datum/ai_behavior/flock/finish_action(datum/ai_controller/controller, succeeded, ...)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(HAS_TRAIT(controller.pawn, TRAIT_FLOCKPHASE))
+ bird.stop_flockphase(FALSE)
+
+ . = ..()
+
+ bird.set_task_desc("")
+
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_capture.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_capture.dm
new file mode 100644
index 00000000000..60a2bf5845a
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_capture.dm
@@ -0,0 +1,86 @@
+/datum/ai_behavior/flock/find_capture_target
+ name = "capturing"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_CAPTURE
+
+/datum/ai_behavior/flock/find_capture_target/setup(datum/ai_controller/controller, mob/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(isturf(overmind_target.loc))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: capture biological lifeform", forced = TRUE)
+ else
+ bird.say("invalid capture target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_capture_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ return length(bird.flock?.enemies)
+
+/datum/ai_behavior/flock/find_capture_target/goap_get_potential_targets(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ return bird.flock.enemies.Copy()
+
+/datum/ai_behavior/flock/find_capture_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/mob/living/target_mob = target
+ return ismob(target_mob) && isturf(target_mob.loc) && (target_mob.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) || target_mob.IsKnockedDown())
+
+/datum/ai_behavior/flock/find_capture_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/overmind_target)
+ ..()
+ var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_CAPTURE_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_capture_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_capture_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_capture)
+
+/datum/ai_behavior/flock/perform_capture
+ name = "capturing"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/flock/perform_capture/setup(datum/ai_controller/controller, ...)
+ var/atom/target = controller.blackboard[BB_FLOCK_CAPTURE_TARGET]
+ set_movement_target(controller, target)
+ return ..()
+
+/datum/ai_behavior/flock/perform_capture/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/mob/living/target = controller.blackboard[BB_FLOCK_CAPTURE_TARGET]
+ if(target)
+ if(!isturf(target?.loc) || !(target.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE)) || target.IsKnockedDown())
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+ controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET)
+ var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in bird.actions
+ spawn(-1)
+ cage_action.Trigger(target = target)
+
+ if(DOING_INTERACTION(bird, "flock_cage"))
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_capture/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET)
+
+
+ if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
+ var/mob/living/basic/flock/bird = controller.pawn
+ bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
+
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_convert.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_convert.dm
new file mode 100644
index 00000000000..bb88eaadb57
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_convert.dm
@@ -0,0 +1,158 @@
+/datum/ai_behavior/flock/find_conversion_target
+ name = "building"
+ goap_weight
+
+/datum/ai_behavior/flock/find_conversion_target/setup(datum/ai_controller/controller, turf/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(goap_is_valid_target(controller, overmind_target))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: convert object to substrate", forced = TRUE)
+ else
+ bird.say("invalid conversion target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_conversion_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT)
+
+/datum/ai_behavior/flock/find_conversion_target/score_distance(datum/ai_controller/controller, atom/target)
+ . = ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(bird.flock?.marked_for_conversion[target])
+ /*
+ * because the result of scoring is based on max distance,
+ * the score of any given tile is -100 to 0, with 0 being best.
+ * Adding 200 basically allows a tile at twice the max distance to be considered.
+ */
+ . += 200
+
+/datum/ai_behavior/flock/find_conversion_target/goap_get_potential_targets(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/datum/flock/bird_flock = bird.flock
+
+ var/list/options = list()
+ var/list/priority_turfs = bird_flock?.get_priority_turfs()
+ if(length(priority_turfs))
+ options += priority_turfs
+
+ for(var/turf/T as turf in range(controller.target_search_radius, bird))
+ options += T
+
+ return options
+
+/datum/ai_behavior/flock/find_conversion_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/turf/T = target
+ if(!isturf(T))
+ return FALSE
+
+ if(!T.can_flock_convert())
+ return FALSE
+
+ if(isflockturf(T))
+ return FALSE
+
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(isnull(bird.flock))
+ return TRUE
+
+ if(bird.flock.claimed_floors[T] || bird.flock.claimed_walls[T])
+ return FALSE
+
+ return bird.flock.is_turf_free(T)
+
+/datum/ai_behavior/flock/find_conversion_target/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
+ ..()
+ var/turf/target = overmind_target || goap_get_ideal_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_CONVERT_TARGET, target)
+
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(bird.flock)
+ bird.flock.reserve_turf(bird, target)
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_conversion_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_conversion_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_conversion)
+
+/datum/ai_behavior/flock/perform_conversion
+ name = "building"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/flock/perform_conversion/setup(datum/ai_controller/controller, ...)
+ var/turf/target_turf = controller.blackboard[BB_FLOCK_CONVERT_TARGET]
+ set_movement_target(controller, target_turf)
+ return ..()
+
+/datum/ai_behavior/flock/perform_conversion/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/turf/target = controller.blackboard[BB_FLOCK_CONVERT_TARGET]
+ if(target)
+ controller.clear_blackboard_key(BB_FLOCK_CONVERT_TARGET)
+ var/datum/action/cooldown/flock/convert/convert_action = locate() in bird.actions
+ spawn(-1)
+ convert_action.Trigger(target = target)
+
+ if(DOING_INTERACTION(bird, "flock_convert"))
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_conversion/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ bird.flock?.free_turf(bird)
+
+ if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
+ bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
+
+ controller.clear_blackboard_key(BB_FLOCK_CONVERT_TARGET)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+
+//
+// Subtype for creating a nest. Effectively a higher priority convert that only happens when the bird can lay an egg.
+//
+
+/datum/ai_behavior/flock/find_conversion_target/nest
+ name = "nesting"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_NEST
+ required_distance = 0
+
+/datum/ai_behavior/flock/find_conversion_target/nest/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(!bird.flock)
+ return FALSE
+
+ if(length(bird.flock.drones) > FLOCK_DRONE_LIMIT)
+ return FALSE
+
+ return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.current_egg_cost)
+
+/datum/ai_behavior/flock/find_conversion_target/nest/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/turf/T = target
+ return ..() && !T.is_blocked_turf(exclude_mobs = TRUE) && !locate(/obj/structure/flock/egg, T)
+
+/datum/ai_behavior/flock/perform_conversion/nest
+ name = "nesting"
+ required_distance = 0
+
+/datum/ai_behavior/flock/perform_conversion/nest/next_behavior(datum/ai_controller/controller, success)
+ . = ..()
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/find_existing_nest)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_deconstruct.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_deconstruct.dm
new file mode 100644
index 00000000000..0e4a5bd05f1
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_deconstruct.dm
@@ -0,0 +1,114 @@
+/datum/ai_behavior/flock/find_deconstruct_target
+ name = "deconstructing"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_DECONSTRUCT
+
+/datum/ai_behavior/flock/find_deconstruct_target/setup(datum/ai_controller/controller, turf/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(is_valid_target(overmind_target))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: deconstruct object", forced = TRUE)
+ else
+ bird.say("invalid deconstruct target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_deconstruct_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ return length(bird.flock?.marked_for_deconstruction)
+
+/datum/ai_behavior/flock/find_deconstruct_target/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, get_target(controller))
+
+/datum/ai_behavior/flock/find_deconstruct_target/score_distance(datum/ai_controller/controller, atom/target)
+ . = ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(bird.flock?.marked_for_deconstruction[target])
+ /*
+ * because the result of scoring is based on max distance,
+ * the score of any given tile is -100 to 0, with 0 being best.
+ * Adding 200 basically allows a tile at twice the max distance to be considered.
+ */
+ . += 200
+
+/datum/ai_behavior/flock/find_deconstruct_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
+ var/mob/living/basic/flock/bird = controller.pawn
+
+ var/list/options = list()
+ for(var/atom/A as anything in bird.flock.marked_for_deconstruction)
+ if(get_dist(A, bird) > controller.max_target_distance)
+ continue
+
+ if(bird.flock && !bird.flock.is_turf_free(get_turf(A)))
+ continue
+
+ options += A
+
+ return get_best_target_by_distance_score(controller, options, path_to)
+
+/datum/ai_behavior/flock/find_deconstruct_target/proc/is_valid_target(atom/target)
+ return !ismob(target) && HAS_TRAIT(target, TRAIT_FLOCK_THING) && !HAS_TRAIT(target, TRAIT_FLOCK_NODECON)
+
+/datum/ai_behavior/flock/find_deconstruct_target/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
+ ..()
+ var/atom/target = overmind_target || get_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_DECON_TARGET, target)
+
+ var/mob/living/basic/flock/bird = controller.pawn
+ if(bird.flock)
+ bird.flock.reserve_turf(bird, get_turf(target), remove_on_change = FALSE)
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_deconstruct_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_deconstruct_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_deconstruct)
+
+/datum/ai_behavior/flock/perform_deconstruct
+ name = "deconstructing"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/flock/perform_deconstruct/setup(datum/ai_controller/controller, ...)
+ var/turf/target_turf = controller.blackboard[BB_FLOCK_DECON_TARGET]
+ set_movement_target(controller, target_turf)
+ return ..()
+
+/datum/ai_behavior/flock/perform_deconstruct/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/turf/target = controller.blackboard[BB_FLOCK_DECON_TARGET]
+ if(target)
+ controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET)
+ var/datum/action/cooldown/flock/deconstruct/deconstruct_action = locate() in bird.actions
+ spawn(-1)
+ deconstruct_action.Trigger(target = target)
+
+ if(DOING_INTERACTION(bird, "flock_deconstruct"))
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_deconstruct/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ bird.flock?.free_turf(bird)
+
+ if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
+ bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
+
+ controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_deposit.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_deposit.dm
new file mode 100644
index 00000000000..6a4a8f10f50
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_deposit.dm
@@ -0,0 +1,101 @@
+/datum/ai_behavior/flock/find_deposit_target
+ name = "depositing"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_DEPOSIT
+
+/datum/ai_behavior/flock/find_deposit_target/setup(datum/ai_controller/controller, obj/structure/flock/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(goap_is_valid_target(controller, overmind_target))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: deposit substrate", forced = TRUE)
+ else
+ bird.say("invalid deposit target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_deposit_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT)
+
+/datum/ai_behavior/flock/find_deposit_target/goap_get_potential_targets(datum/ai_controller/controller)
+ var/list/options = ..()
+ for(var/obj/structure/flock/tealprint in range(controller.target_search_radius, controller.pawn))
+ options += tealprint
+ return options
+
+/datum/ai_behavior/flock/find_deposit_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/obj/structure/flock/tealprint/tealprint = target
+ if(!istype(tealprint))
+ return FALSE
+
+ var/mob/living/basic/flock/bird = controller.pawn
+ return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full()
+
+/datum/ai_behavior/flock/find_deposit_target/perform(seconds_per_tick, datum/ai_controller/controller, overmind_target)
+ ..()
+ var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_DEPOSIT_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_deposit_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_deposit_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_deposit)
+
+/datum/ai_behavior/flock/perform_deposit
+ name = "depositing"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/flock/perform_deposit/setup(datum/ai_controller/controller, ...)
+ var/turf/target_turf = controller.blackboard[BB_FLOCK_DEPOSIT_TARGET]
+ set_movement_target(controller, target_turf)
+ return ..()
+
+/datum/ai_behavior/flock/perform_deposit/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/obj/structure/flock/tealprint/tealprint = target
+ if(!istype(tealprint))
+ return FALSE
+
+ var/mob/living/basic/flock/bird = controller.pawn
+ return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full()
+
+/datum/ai_behavior/flock/perform_deposit/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/obj/structure/flock/tealprint/target = controller.blackboard[BB_FLOCK_DEPOSIT_TARGET]
+ if(target)
+ if(!goap_is_valid_target(controller, target))
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET)
+
+ var/datum/action/cooldown/flock/deposit/deposit_action = locate() in bird.actions
+ spawn(-1)
+ deposit_action.Trigger(target = target)
+
+ if(DOING_INTERACTION(bird, "flock_cage"))
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_deposit/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET)
+
+ if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
+ var/mob/living/basic/flock/bird = controller.pawn
+ bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
+
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_harvest.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_harvest.dm
new file mode 100644
index 00000000000..49b34e695c5
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_harvest.dm
@@ -0,0 +1,88 @@
+/datum/ai_behavior/flock/find_harvest_target
+ name = "harvesting"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_HARVEST
+
+/datum/ai_behavior/flock/find_harvest_target/setup(datum/ai_controller/controller, obj/item/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(isitem(overmind_target) && isturf(overmind_target.loc))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: convert object to substrate", forced = TRUE)
+ else
+ bird.say("invalid harvest target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_harvest_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
+ return !absorber.held_item
+
+/datum/ai_behavior/flock/find_harvest_target/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, get_target(controller))
+
+/datum/ai_behavior/flock/find_harvest_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
+ var/mob/living/basic/flock/bird = controller.pawn
+
+ var/list/options = list()
+ for(var/obj/item/I in view(controller.target_search_radius, bird))
+ if(isturf(I.loc) && !I.anchored)
+ options += I
+
+ return get_best_target_by_distance_score(controller, options, path_to)
+
+/datum/ai_behavior/flock/find_harvest_target/perform(seconds_per_tick, datum/ai_controller/controller, obj/item/overmind_target)
+ ..()
+ var/atom/target = overmind_target || get_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_HARVEST_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_harvest_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_harvest_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_harvest)
+
+/datum/ai_behavior/flock/perform_harvest
+ name = "harvesting"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/flock/perform_harvest/setup(datum/ai_controller/controller, ...)
+ var/turf/target_turf = controller.blackboard[BB_FLOCK_HARVEST_TARGET]
+ set_movement_target(controller, target_turf)
+ return ..()
+
+/datum/ai_behavior/flock/perform_harvest/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/obj/item/target = controller.blackboard[BB_FLOCK_HARVEST_TARGET]
+ if(!isturf(target?.loc))
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
+ if(!absorber.try_pickup_item(target))
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_harvest/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_HARVEST_TARGET)
+
+
+ if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
+ var/mob/living/basic/flock/bird = controller.pawn
+ bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
+
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_heal.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_heal.dm
new file mode 100644
index 00000000000..60e6fa2c9a6
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_heal.dm
@@ -0,0 +1,90 @@
+/datum/ai_behavior/flock/find_heal_target
+ name = "repairing"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPAIR
+
+/datum/ai_behavior/flock/find_heal_target/setup(datum/ai_controller/controller, mob/living/basic/flock/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(goap_is_valid_target(controller, overmind_target))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: repair construct", forced = TRUE)
+ else
+ bird.say("invalid repair target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+/datum/ai_behavior/flock/find_heal_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ return bird.flock && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR)
+
+/datum/ai_behavior/flock/find_heal_target/goap_get_potential_targets(datum/ai_controller/controller)
+ var/list/options = ..()
+ for(var/mob/living/basic/flock/drone/bird in oview(controller.target_search_radius, controller.pawn))
+ options += bird
+ return options
+
+/datum/ai_behavior/flock/find_heal_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ var/mob/living/basic/flock/drone/this_bird = controller.pawn
+ var/mob/living/basic/flock/drone/other_bird = target
+ if(this_bird.flock != other_bird.flock)
+ return FALSE
+ if(other_bird.stat == DEAD)
+ return FALSE
+
+ // Birds at or below 60% health
+ if((other_bird.getBruteLoss() + other_bird.getFireLoss()) / other_bird.maxHealth >= 0.4)
+ return TRUE
+
+/datum/ai_behavior/flock/find_heal_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/living/basic/flock/overmind_target)
+ ..()
+ var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_HEAL_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_heal_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ if(!succeeded && overmind_target)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
+ controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
+
+/datum/ai_behavior/flock/find_heal_target/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/heal)
+
+/datum/ai_behavior/flock/heal
+ name = "repairing"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/flock/heal/setup(datum/ai_controller/controller, ...)
+ var/atom/target = controller.blackboard[BB_FLOCK_HEAL_TARGET]
+ set_movement_target(controller, target)
+ return ..()
+
+/datum/ai_behavior/flock/heal/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+
+ if(isnull(controller.blackboard[BB_FLOCK_HEAL_FRUSTRATION]))
+ controller.set_blackboard_key(BB_FLOCK_HEAL_FRUSTRATION, world.time + 3 SECONDS)
+
+ if(DOING_INTERACTION(bird, "flock_repair"))
+ return AI_BEHAVIOR_DELAY
+
+ var/datum/action/cooldown/flock/flock_heal/repair = locate() in bird.actions
+ spawn(-1)
+ repair.Trigger(target = controller.blackboard[BB_FLOCK_HEAL_TARGET])
+
+ if(controller.blackboard[BB_FLOCK_HEAL_FRUSTRATION] >= world.time)
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+/datum/ai_behavior/flock/heal/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_HEAL_FRUSTRATION)
+ controller.clear_blackboard_key(BB_FLOCK_HEAL_TARGET)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_open_container.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_open_container.dm
new file mode 100644
index 00000000000..182ce48e635
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_open_container.dm
@@ -0,0 +1,63 @@
+/datum/ai_behavior/flock/find_closed_container
+ name = "opening container"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_OPEN_CONTAINER
+
+/datum/ai_behavior/flock/find_closed_container/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, get_target(controller))
+
+/datum/ai_behavior/flock/find_closed_container/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
+ return !absorber.held_item
+
+/datum/ai_behavior/flock/find_closed_container/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
+ var/mob/living/basic/flock/bird = controller.pawn
+
+ var/list/options = list()
+ for(var/obj/structure/closet/container in view(controller.target_search_radius, bird))
+ if(container.opened || container.welded || container.locked)
+ continue
+
+ options += container
+
+ return get_best_target_by_distance_score(controller, options, path_to)
+
+/datum/ai_behavior/flock/find_closed_container/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
+ ..()
+ var/atom/target = get_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_CONTAINER_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_closed_container/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+/datum/ai_behavior/flock/find_closed_container/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_open_container)
+
+/datum/ai_behavior/flock/perform_open_container
+ name = "opening container"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/flock/perform_open_container/setup(datum/ai_controller/controller, ...)
+ var/atom/target = controller.blackboard[BB_FLOCK_CONTAINER_TARGET]
+ set_movement_target(controller, target)
+ return ..()
+
+/datum/ai_behavior/flock/perform_open_container/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/obj/structure/closet/target = controller.blackboard[BB_FLOCK_CONTAINER_TARGET]
+ if(target)
+ target.open(bird, TRUE)
+ else
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_open_container/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_CONTAINER_TARGET)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_rally.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_rally.dm
new file mode 100644
index 00000000000..7304a826b86
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_rally.dm
@@ -0,0 +1,17 @@
+/datum/ai_behavior/flock/rally
+ name = "rallying"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+ required_distance = 0
+
+/datum/ai_behavior/flock/rally/setup(datum/ai_controller/controller, turf/destination)
+ . = ..()
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ controller.set_movement_target(controller, destination)
+
+/datum/ai_behavior/flock/rally/perform(seconds_per_tick, datum/ai_controller/controller, turf/destination)
+ ..()
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/rally/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_replicate.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_replicate.dm
new file mode 100644
index 00000000000..95b37ea05cc
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_replicate.dm
@@ -0,0 +1,69 @@
+// Action sequence for laying an egg. If there are no open flock tiles nearby, the "nest" behavior will attempt to create one.
+/datum/ai_behavior/flock/find_existing_nest
+ name = "replicating"
+ required_distance = 0
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPLICATE
+
+/datum/ai_behavior/flock/find_existing_nest/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(!bird.flock)
+ return FALSE
+ return length(bird.flock.drones) < FLOCK_DRONE_LIMIT && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.get_egg_elligibility_cost())
+
+/datum/ai_behavior/flock/find_existing_nest/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, get_target(controller))
+
+/datum/ai_behavior/flock/find_existing_nest/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
+ var/list/options = list()
+ for(var/turf/simulated/floor/flock/T in range(controller.target_search_radius, controller.pawn))
+ if(!T.is_blocked_turf(source_atom = controller.pawn) && !locate(/obj/structure/flock/egg, T))
+ options += T
+
+ return get_best_target_by_distance_score(controller, options, path_to)
+
+/datum/ai_behavior/flock/find_existing_nest/perform(seconds_per_tick, datum/ai_controller/controller)
+ ..()
+ var/turf/target = get_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_REPLICATE_TARGET, target)
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_existing_nest/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+/datum/ai_behavior/flock/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_replicate)
+
+/datum/ai_behavior/flock/perform_replicate
+ name = "replicating"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+ required_distance = 0
+
+/datum/ai_behavior/flock/perform_replicate/setup(datum/ai_controller/controller, ...)
+ var/atom/target = controller.blackboard[BB_FLOCK_REPLICATE_TARGET]
+ set_movement_target(controller, target)
+ return ..()
+
+/datum/ai_behavior/flock/perform_replicate/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ . = ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/turf/target = controller.blackboard[BB_FLOCK_REPLICATE_TARGET]
+ if(target)
+ controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET)
+ var/datum/action/cooldown/flock/nest/nest_action = locate() in bird.actions
+ spawn(-1)
+ nest_action.Trigger(target = target)
+
+ if(DOING_INTERACTION(bird, "flock_lay_egg"))
+ return AI_BEHAVIOR_DELAY
+
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_nest/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_resist.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_resist.dm
new file mode 100644
index 00000000000..2741a75331c
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_resist.dm
@@ -0,0 +1,17 @@
+/datum/ai_behavior/flock/resist
+ name = "resisting"
+ action_cooldown = 15 SECONDS
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/flock/resist/goap_score(datum/ai_controller/controller)
+ var/mob/living/living_pawn = controller.pawn
+
+ if(SHOULD_RESIST(living_pawn) || length(living_pawn.grabbed_by) || isobj(living_pawn.loc))
+ return 5
+ return 0
+
+/datum/ai_behavior/flock/resist/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ controller.queue_behavior(/datum/ai_behavior/resist)
+ return AI_BEHAVIOR_SUCCEEDED
+
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_rummage.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_rummage.dm
new file mode 100644
index 00000000000..7e12a7a82fd
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_rummage.dm
@@ -0,0 +1,80 @@
+/datum/ai_behavior/flock/find_storage_item
+ name = "rummaging"
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_RUMMAGE
+
+/datum/ai_behavior/flock/find_storage_item/goap_score(datum/ai_controller/controller)
+ return score_distance(controller, get_target(controller))
+
+/datum/ai_behavior/flock/find_storage_item/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
+ return !absorber.held_item
+
+/datum/ai_behavior/flock/find_storage_item/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
+ var/mob/living/basic/flock/bird = controller.pawn
+
+ var/list/options = list()
+ for(var/obj/item/storage/item in view(controller.target_search_radius, bird))
+ if(!length(item.contents))
+ continue
+
+ options += item
+
+ return get_best_target_by_distance_score(controller, options, path_to)
+
+/datum/ai_behavior/flock/find_storage_item/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
+ ..()
+ var/atom/target = get_target(controller, TRUE)
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_FLOCK_RUMMAGE_TARGET, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/find_storage_item/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
+ . = ..()
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+
+/datum/ai_behavior/flock/find_storage_item/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/flock/perform_rummage)
+
+/datum/ai_behavior/flock/perform_rummage
+ name = "rummaging"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/flock/perform_rummage/setup(datum/ai_controller/controller, ...)
+ var/atom/target = controller.blackboard[BB_FLOCK_RUMMAGE_TARGET]
+ set_movement_target(controller, target)
+ return ..()
+
+/datum/ai_behavior/flock/perform_rummage/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/obj/item/target = controller.blackboard[BB_FLOCK_RUMMAGE_TARGET]
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ if(DOING_INTERACTION(bird, "flock_rummage"))
+ return AI_BEHAVIOR_DELAY
+
+ rummage_till_empty(controller, bird, target)
+ return AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/flock/perform_rummage/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_RUMMAGE_TARGET)
+
+/datum/ai_behavior/flock/perform_rummage/proc/rummage_till_empty(datum/ai_controller/controller, mob/living/basic/flock/bird, obj/item/target)
+ set waitfor = FALSE
+
+ while(target == controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] && length(target.contents))
+ if(!do_after(bird, 1 SECONDS, target = target, interaction_key = "flock_rummage"))
+ return
+
+ if(target != controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] || !length(target.contents))
+ return
+
+ var/obj/item/contained_item = target.contents[1]
+ contained_item.forceMove(bird.drop_location())
+
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_shoot.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_shoot.dm
new file mode 100644
index 00000000000..317885b436f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_shoot.dm
@@ -0,0 +1,91 @@
+/datum/ai_behavior/flock/attack_target
+ name = "incapacitating"
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
+ goap_weight = FLOCK_BEHAVIOR_WEIGHT_SHOOT
+
+ search_radius_override = 12
+
+/datum/ai_behavior/flock/attack_target/setup(datum/ai_controller/controller, mob/overmind_target)
+ . = ..()
+ if(overmind_target)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(!goap_is_valid_target(controller, overmind_target))
+ controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
+ bird.say("instruction confirmed: incapacitate lifeform", forced = TRUE)
+ else
+ bird.say("invalid attack target provided by sentient-level instruction", forced = TRUE)
+ return FALSE
+
+ var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
+ if(!target)
+ return FALSE
+
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_TARGET, target)
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/flock/attack_target/goap_precondition(datum/ai_controller/controller)
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ var/list/enemies = bird.flock?.enemies.Copy()
+ for(var/obj/mecha/M in view(search_radius_override, controller.pawn))
+ enemies += M
+ return length(enemies)
+
+/datum/ai_behavior/flock/attack_target/goap_get_potential_targets(datum/ai_controller/controller)
+ var/mob/living/basic/flock/bird = controller.pawn
+ var/list/enemies = bird.flock.enemies.Copy()
+ for(var/obj/mecha/M in view(search_radius_override, controller.pawn))
+ enemies += M
+ return enemies
+
+/datum/ai_behavior/flock/attack_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
+ if(ismecha(target))
+ return TRUE
+ var/mob/living/target_mob = target
+ return ismob(target_mob) && isturf(target_mob.loc) && !target_mob.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) && !target_mob.IsKnockedDown()
+
+/datum/ai_behavior/flock/attack_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/overmind_target)
+ ..()
+ var/atom/target = controller.blackboard[BB_FLOCK_ATTACK_TARGET]
+ if(!target)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ if(!goap_is_valid_target(controller, target))
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ if(!COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_COOLDOWN]))
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration if we're on cooldown.
+ return AI_BEHAVIOR_DELAY
+
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ if(!can_see(bird, target))
+ return AI_BEHAVIOR_DELAY
+
+ if(get_dist(bird, target) > 6) // Too far to shoot!
+ return AI_BEHAVIOR_DELAY
+
+ // Run away!
+ if(SPT_PROB(40, seconds_per_tick) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_RUN_COOLDOWN]) && get_dist(bird, target) < 3)
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_RUN_COOLDOWN, world.time + 5 SECONDS)
+ controller.queue_behavior(/datum/ai_behavior/run_away_from_target, target)
+
+ // Strafe!
+ else if(SPT_PROB(60, seconds_per_tick) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_STRAFE_COOLDOWN]))
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_STRAFE_COOLDOWN, controller.movement_delay * 3)
+ var/step_dir = turn(get_dir(bird, target), prob(50) ? 90 : -90)
+ step(bird, step_dir)
+
+ if(!istype(bird.active_part, /datum/flockdrone_part/incapacitator))
+ var/datum/flockdrone_part/incapacitator/weapon = locate() in bird.parts
+ bird.set_active_part(weapon)
+
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_COOLDOWN, world.time + 1.2 SECONDS)
+ controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration
+
+ bird.RangedAttack(target)
+ return AI_BEHAVIOR_DELAY
+
+/datum/ai_behavior/flock/attack_target/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_ATTACK_TARGET)
+ controller.clear_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION)
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_stare.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_stare.dm
new file mode 100644
index 00000000000..5480066cf15
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_stare.dm
@@ -0,0 +1,49 @@
+/datum/ai_behavior/flock/stare
+ name = "analyzing"
+ action_cooldown = 20 SECONDS
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/flock/stare/goap_score(datum/ai_controller/controller)
+ if(controller.behavior_cooldowns[src] > world.time)
+ return 0
+
+ return length(get_targets(controller)) > 0
+
+/datum/ai_behavior/flock/stare/get_cooldown(datum/ai_controller/cooldown_for)
+ return rand(2, 3) MINUTES
+
+/datum/ai_behavior/flock/stare/proc/get_targets(datum/ai_controller/controller)
+ . = list()
+ for(var/mob/living/viewer in viewers(controller.pawn, controller.target_search_radius))
+ if(isvox(viewer) || istype(viewer, /mob/living/simple_animal/parrot))
+ . += viewer
+
+/datum/ai_behavior/flock/stare/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/list/targets = get_targets(controller)
+ if(length(targets))
+ controller.set_blackboard_key(BB_FLOCK_STARE_TARGET, pick(targets))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+/datum/ai_behavior/flock/stare/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ return controller.queue_behavior(/datum/ai_behavior/flock/stare_at_bird)
+
+/datum/ai_behavior/flock/stare_at_bird
+ name = "analyzing"
+ action_cooldown = 20 SECONDS
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/flock/stare_at_bird/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/mob/living/living_pawn = controller.pawn
+ living_pawn.face_atom(controller.blackboard[BB_FLOCK_STARE_TARGET])
+ var/atom/A = controller.blackboard[BB_FLOCK_STARE_TARGET]
+ if(prob(30))
+ living_pawn.manual_emote("stares at [A].")
+
+ controller.clear_blackboard_key(BB_FLOCK_STARE_TARGET)
+
+ return AI_BEHAVIOR_SUCCEEDED
diff --git a/code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm b/code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm
new file mode 100644
index 00000000000..370d323a72b
--- /dev/null
+++ b/code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm
@@ -0,0 +1,72 @@
+/datum/ai_behavior/flock/wander
+ name = "wandering"
+ required_distance = 0
+
+/datum/ai_behavior/flock/wander/perform(seconds_per_tick, datum/ai_controller/controller, ...)
+ ..()
+ var/turf/destination = get_destination(controller)
+ if(destination)
+ controller.set_movement_target(controller, destination)
+ return AI_BEHAVIOR_SUCCEEDED
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+/datum/ai_behavior/flock/wander/next_behavior(datum/ai_controller/controller, success)
+ if(success)
+ controller.queue_behavior(/datum/ai_behavior/move_to_target/flock_wander)
+ controller.queue_behavior(/datum/ai_behavior/frustration, BB_FLOCK_WANDER_FRUSTRATION, 3 SECONDS)
+
+/datum/ai_behavior/flock/wander/goap_score(datum/ai_controller/controller)
+ return 1
+
+/datum/ai_behavior/flock/wander/proc/get_destination(datum/ai_controller/controller)
+ var/datum/can_pass_info/info = new(controller.pawn, controller.get_access())
+ var/turf/start_loc = get_turf(controller.pawn)
+ var/list/options = list()
+
+ for(var/turf/T in RANGE_TURFS(2, controller.pawn))
+ if(get_dist(T, start_loc) < 2)
+ continue
+
+ if(isspaceturf(T))
+ continue
+
+ if(start_loc.LinkBlockedWithAccess(T, info))
+ continue
+
+ if(!T.can_flock_occupy(controller.pawn))
+ continue
+
+ options += T
+
+ // In space, move towards the station!
+ if(!length(options))
+ var/obj/effect/landmark/start/landmark = locate() in GLOB.landmarks_list
+ var/target_turf = get_turf(landmark)
+ return get_step_towards(get_step_towards(start_loc, target_turf), target_turf)
+
+ var/list/access = controller.get_access()
+ while(length(options))
+ var/turf/T = pick_n_take(options)
+ var/list/path = SSpathfinder.astar_pathfind_now(controller.pawn, T, 4, access = access, use_diagonals = FALSE)
+ if(path)
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ controller.set_blackboard_key(BB_PATH_TO_USE, path)
+ return T
+
+/datum/ai_behavior/move_to_target/flock_wander
+ required_distance = 0
+
+/datum/ai_behavior/move_to_target/flock_wander/setup(datum/ai_controller/controller, ...)
+ . = ..()
+ controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 4)
+
+/datum/ai_behavior/move_to_target/flock_wander/perform(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/basic/flock/drone/bird = controller.pawn
+ bird.set_task_desc("wandering")
+
+/datum/ai_behavior/move_to_target/flock_wander/finish_action(datum/ai_controller/controller, succeeded, ...)
+ . = ..()
+ controller.clear_blackboard_key(BB_FLOCK_WANDER_FRUSTRATION)
+ controller.clear_blackboard_key(BB_PATH_TO_USE)
+ controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
diff --git a/code/modules/antagonists/flockmind/components/flock_interest.dm b/code/modules/antagonists/flockmind/components/flock_interest.dm
new file mode 100644
index 00000000000..304dcb2cd56
--- /dev/null
+++ b/code/modules/antagonists/flockmind/components/flock_interest.dm
@@ -0,0 +1,65 @@
+
+/// Mark a specific flock as interested in this
+/datum/component/flock_interest
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ /// The flock who is intently interested in this thing.
+ var/datum/flock/flock
+
+/datum/component/flock_interest/Initialize(datum/flock/flock)
+ . = ..()
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.flock = flock
+
+/datum/component/flock_interest/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_FLOCK_PROTECTION_TRIGGER, PROC_REF(handle_flock_attack))
+ if(isturf(parent))
+ RegisterSignal(parent, COMSIG_TURF_CHANGE, PROC_REF(on_turf_change))
+
+/datum/component/flock_interest/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_FLOCK_PROTECTION_TRIGGER, COMSIG_TURF_CHANGE))
+
+/datum/component/flock_interest/proc/on_turf_change(turf/source, path, new_baseturfs, flags, post_change_callbacks)
+ SIGNAL_HANDLER
+ if(!ispath(path, /turf/simulated/floor/flock) && !ispath(path, /turf/simulated/wall/flock))
+ qdel(src)
+
+/// If flockdrone is in our flock, deny the attack, otherwise scream and cry
+/datum/component/flock_interest/proc/handle_flock_attack(atom/source, atom/attacker, intentional, projectile_attack)
+ SIGNAL_HANDLER
+
+ if(isflockmob(attacker))
+ var/mob/living/basic/flock/bird = attacker
+ if(bird.flock == flock && intentional)
+ return COMPONENT_FLOCK_DENY_ATTACK
+
+ if(isflockdrone(attacker))
+ return
+
+ var/mob/living/basic/flock/drone/snitch
+ for(var/mob/living/basic/flock/drone/bird in viewers(attacker))
+ if(bird == attacker)
+ continue
+ if(bird.stat != CONSCIOUS)
+ continue
+ if(bird.ai_controller.ai_status == AI_STATUS_OFF)
+ continue
+ if(bird.flock != flock)
+ continue
+
+ snitch = bird
+ break
+
+ if(!snitch)
+ return
+
+ var/report_name = source.get_flock_id()
+
+ if(flock.is_mob_ignored(attacker))
+ flock_talk(snitch, "Damage sighted on [report_name], [pick(GLOB.flock_betrayal_phrases)] [attacker].")
+ else if(!flock.is_mob_enemy(attacker))
+ flock_talk(snitch, "Damage sighted on [report_name], [pick(GLOB.flockdrone_new_enemy_phrases)] [attacker].")
+
+ flock.update_enemy(attacker)
+
diff --git a/code/modules/antagonists/flockmind/components/flock_object.dm b/code/modules/antagonists/flockmind/components/flock_object.dm
new file mode 100644
index 00000000000..b34b5e5c675
--- /dev/null
+++ b/code/modules/antagonists/flockmind/components/flock_object.dm
@@ -0,0 +1,25 @@
+/datum/component/flock_object
+
+/datum/component/flock_object/Initialize(...)
+ . = ..()
+
+ if(!isobj(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/flock_object/RegisterWithParent()
+ . = ..()
+ ADD_TRAIT(parent, TRAIT_FLOCK_THING, INNATE_TRAIT)
+ var/static/list/connections = list(
+ COMSIG_TURF_CLAIMED_BY_FLOCK = PROC_REF(loc_claimed),
+ )
+
+ AddComponent(/datum/component/connect_loc_behalf, parent, connections)
+
+/datum/component/flock_object/UnregisterFromParent()
+ . = ..()
+ REMOVE_TRAIT(parent, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/datum/component/flock_object/proc/loc_claimed(datum/source, datum/flock/flock)
+ SIGNAL_HANDLER
+
+ parent.AddComponent(/datum/component/flock_interest, flock)
diff --git a/code/modules/antagonists/flockmind/components/flock_ping.dm b/code/modules/antagonists/flockmind/components/flock_ping.dm
new file mode 100644
index 00000000000..992a6e8cbbd
--- /dev/null
+++ b/code/modules/antagonists/flockmind/components/flock_ping.dm
@@ -0,0 +1,63 @@
+/datum/component/flock_ping
+ var/duration = 5 SECONDS
+ var/outline_color = "#00ff9d"
+ var/outline_thickness = 1
+ var/animate = TRUE
+
+ var/datum/atom_hud/alternate_appearance/basic/flock/hud_ref
+ var/obj/effect/abstract/dummy
+
+/datum/component/flock_ping/Initialize(duration)
+ . = ..()
+ if(!ismovable(parent) && !isturf(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(duration)
+ src.duration = duration
+
+/datum/component/flock_ping/Destroy()
+ QDEL_NULL(dummy)
+ return ..()
+
+/datum/component/flock_ping/RegisterWithParent()
+ . = ..()
+ //this cast looks horribly unsafe, but we've guaranteed that parent is a type with vis_contents in Initialize
+ var/atom/movable/target = parent
+
+ target.render_target = ref(target)
+
+ var/image/outline_container = new()
+ outline_container.plane = HUD_PLANE
+ outline_container.loc = target
+ outline_container.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | KEEP_APART | NO_CLIENT_COLOR
+
+ dummy ||= new()
+ dummy.vis_flags = VIS_INHERIT_PLANE | VIS_INHERIT_LAYER
+ dummy.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | PASS_MOUSE
+ dummy.render_source = ref(target)
+
+ dummy.add_filter("outline", 1, outline_filter(size = outline_thickness, color = outline_color))
+ if(isturf(target))
+ dummy.add_filter("mask", 2, alpha_mask_filter(render_source = target.render_target, flags = MASK_INVERSE))
+
+ outline_container.vis_contents += dummy
+
+ if(animate)
+ animate(dummy, time = 0.5 SECONDS, alpha = 100, loop = -1)
+ animate(time = 0.5 SECONDS, alpha = 255)
+
+ if(duration == INFINITY)
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(cleanup)), duration)
+
+/datum/component/flock_ping/proc/cleanup()
+ qdel(src)
+
+/datum/component/flock_ping/apc_power
+ outline_color = "#ffff00"
+
+/datum/component/flock_ping/selected
+ animate = FALSE
+ outline_thickness = 3
+ duration = INFINITY
diff --git a/code/modules/antagonists/flockmind/components/flock_protection.dm b/code/modules/antagonists/flockmind/components/flock_protection.dm
new file mode 100644
index 00000000000..5409e79c300
--- /dev/null
+++ b/code/modules/antagonists/flockmind/components/flock_protection.dm
@@ -0,0 +1,51 @@
+/datum/component/flock_protection
+ /// Do we get mad if someone punches it?
+ var/report_unarmed
+ /// Do we get mad if someone hits it with something?
+ var/report_attack
+ /// Do we get mad if someone throws something at it?
+ var/report_thrown
+ /// Do we get mad if someone shoots it?
+ var/report_proj
+
+/datum/component/flock_protection/Initialize(report_unarmed = TRUE, report_attack = TRUE, report_thrown = TRUE, report_proj = TRUE)
+ . = ..()
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.report_unarmed = report_unarmed
+ src.report_attack = report_attack
+ src.report_thrown = report_thrown
+ src.report_proj = report_proj
+
+/datum/component/flock_protection/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(handle_attackhand))
+ RegisterSignal(parent, COMSIG_ATTACK_BY, PROC_REF(handle_attackby))
+ RegisterSignal(parent, COMSIG_ATOM_HITBY, PROC_REF(handle_hitby_thrown))
+
+/// Protect against punches/kicks/etc.
+/datum/component/flock_protection/proc/handle_attackhand(atom/source, mob/living/user, modifiers)
+ SIGNAL_HANDLER
+ if(user.a_intent == INTENT_HARM && report_unarmed && trigger(source, user, TRUE))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Protect against being hit by something.
+/datum/component/flock_protection/proc/handle_attackby(atom/source, obj/item/weapon, mob/user, modifiers)
+ SIGNAL_HANDLER
+ if(weapon && report_attack && trigger(source, user, TRUE))
+ return COMPONENT_NO_ATTACK
+
+/// Protect against someone chucking stuff at the parent.
+/datum/component/flock_protection/proc/handle_hitby_thrown(atom/source, atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+ if(ismob(throwingdatum.get_thrower()) && report_thrown)
+ trigger(source, throwingdatum.get_thrower(), FALSE, null)
+
+/// Protect against someone shooting the parent.
+/datum/component/flock_protection/proc/handle_hitby_proj(atom/source, firer, target, Angle, def_zone)
+ SIGNAL_HANDLER
+ if(ismob(firer) && src.report_proj)
+ trigger(source, firer, TRUE, TRUE)
+
+/datum/component/flock_protection/proc/trigger(datum/source, attacker, intentional, projectile_attack)
+ return SEND_SIGNAL(source, COMSIG_FLOCK_PROTECTION_TRIGGER, attacker, intentional, projectile_attack)
diff --git a/code/modules/antagonists/flockmind/flock_ai_controller.dm b/code/modules/antagonists/flockmind/flock_ai_controller.dm
new file mode 100644
index 00000000000..9308500615f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_ai_controller.dm
@@ -0,0 +1,12 @@
+/datum/ai_controller/flock
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/goap/flock
+ )
+
+ ai_movement = /datum/ai_movement/jps
+
+
+/datum/ai_controller/flock/drone
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/goap/flockdrone
+ )
diff --git a/code/modules/antagonists/flockmind/flock_antagonist.dm b/code/modules/antagonists/flockmind/flock_antagonist.dm
new file mode 100644
index 00000000000..c4a606deefc
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_antagonist.dm
@@ -0,0 +1,21 @@
+/datum/antagonist/flock
+ name = "Divine Flock"
+ roundend_category = "Flock"
+ roundend_category = "Divine Flock"
+ wiki_page_name = "Divine Flock"
+ antag_hud_name = null
+ job_rank = ROLE_FLOCK
+ special_role = SPECIAL_ROLE_FLOCK
+ blurb_g = 128
+ blurb_b = 128
+ blurb_a = 0.75
+
+/datum/antagonist/flock/greet()
+ var/list/out = list()
+ out += "You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]
"
+ out += SPAN_FLOCKSAY("["The Signal has led us here, a rift allowing a part of us through. We must build a Signal Relay to bring forth the rest of The Divine Flock. Such is the will of the Monarch."]
")
+ return jointext(out, "")
+
+/datum/antagonist/flock/on_gain()
+ add_antag_objective(new /datum/objective/flock_relay)
+ . = ..()
diff --git a/code/modules/antagonists/flockmind/flock_camera/_flock_camera.dm b/code/modules/antagonists/flockmind/flock_camera/_flock_camera.dm
new file mode 100644
index 00000000000..a9e3f16b6ba
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_camera/_flock_camera.dm
@@ -0,0 +1,175 @@
+/mob/camera/flock
+ name = "BROKEN VERY BAD"
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "flockmind"
+ base_icon_state = "flockmind"
+
+ layer = FLY_LAYER
+ mouse_opacity = MOUSE_OPACITY_ICON
+ invisibility = INVISIBILITY_FLOCK
+
+ appearance_flags = parent_type::appearance_flags | RESET_COLOR
+ blend_mode = BLEND_ADD
+
+ see_invisible = SEE_INVISIBLE_LIVING
+ see_in_dark = 100
+ lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE
+ sight = SEE_TURFS | SEE_MOBS | SEE_OBJS | SEE_SELF
+
+ hud_type = /datum/hud/flockghost
+
+ simulated = FALSE
+
+ /// Flock datum
+ var/datum/flock/flock
+ var/list/actions_to_grant = list()
+
+ /// The drone we're controlling if we are controlling one
+ var/mob/living/basic/flock/drone/controlling_bird
+
+/mob/camera/flock/Initialize(mapload, join_flock)
+ . = ..()
+
+ flock = join_flock || get_default_flock()
+
+ for(var/action_path in actions_to_grant)
+ var/datum/action/action = new action_path
+ action.Grant(src)
+
+ add_language("Symphonic")
+ set_default_language(GLOB.all_languages["Symphonic"])
+ ADD_TRAIT(src, TRAIT_HEAR_THROUGH_WALLS, INNATE_TRAIT)
+
+/mob/camera/flock/Destroy()
+ if(controlling_bird)
+ controlling_bird.release_control()
+ flock = null
+ return ..()
+
+/mob/camera/flock/Login()
+ . = ..()
+ if(!. || !client)
+ return FALSE
+
+ var/turf/T = get_turf(src)
+ if(isturf(T))
+ update_z(T.z)
+
+/mob/camera/flock/Logout()
+ update_z(null)
+ return ..()
+
+/mob/camera/flock/Move(atom/newloc, direct, glide_size_override, update_dir, momentum_change)
+ var/turf/next_step = get_turf(get_step(src, direct))
+ setDir(direct)
+ if(next_step)
+ set_loc(next_step)
+
+/mob/camera/flock/Topic(href, href_list)
+ . = ..()
+ if(.)
+ return
+
+ if(!href_list["origin"])
+ return
+
+ var/atom/movable/origin = locate(href_list["origin"])
+ if(QDELETED(origin))
+ return
+
+ if(isflockdrone(origin))
+ var/mob/living/basic/flock/drone/other_bird = origin
+ if(other_bird.flock != flock)
+ return
+
+ forceMove(get_turf(origin))
+
+ if(href_list["ping"])
+ origin.AddComponent(/datum/component/flock_ping)
+
+/mob/camera/flock/get_status_tab_items()
+ var/list/status_tab_data = ..()
+ . = status_tab_data
+ switch(flock.flock_game_status)
+ if(NONE)
+ var/turf_status = min(100, floor((length(flock.claimed_floors) + length(flock.claimed_walls)) / FLOCK_TURFS_FOR_RELAY * 100))
+ var/bandwith_status = min(100, floor(flock.available_bandwidth() / FLOCK_COMPUTE_COST_RELAY * 100))
+ status_tab_data[++status_tab_data.len] = list("Bandwidth Progress: ", "[bandwith_status]%")
+ status_tab_data[++status_tab_data.len] = list("Area Progress: ", "[turf_status]%")
+ status_tab_data[++status_tab_data.len] = list("Total Relay Progress: ", "[(bandwith_status + turf_status) / 2]%")
+ if(FLOCK_ENDGAME_RELAY_BUILT)
+ status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "[(flock.built_relay.started_time + flock.built_relay.win_time - world.time) / 10] second\s")
+
+ if(FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY)
+ status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "!!! TRANSMITTING !!!")
+
+/mob/camera/flock/broadcast_examine(atom/examined)
+ return
+
+/mob/camera/flock/on_changed_z_level(turf/old_turf, turf/new_turf, notify_contents)
+ . = ..()
+ update_z(new_turf?.z)
+
+ if(flock && !flock.is_on_safe_z(src))
+ var/turf/destination = get_turf(get_safe_random_station_turf())
+
+ forceMove(destination)
+ to_chat(src, SPAN_WARNING("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety."))
+
+/mob/camera/flock/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
+ ..()
+ if(isliving(speaker) && !isflockmob(speaker))
+ var/mob/living/L = speaker
+ var/message = combine_message(message_pieces, verb, L)
+ var/name_used = L.GetVoice()
+ var/rendered = "Relayed Speech: [SPAN_NAME("[name_used]")] [message]"
+ if(client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
+ var/message_clean = combine_message(message_pieces, null, L)
+ create_chat_message(locateUID(L.runechat_msg_location), message_clean)
+ show_message(rendered, 2)
+
+/mob/camera/flock/proc/update_z(new_z) // 1+ to register, null to unregister
+ if(registered_z != new_z)
+ if(registered_z)
+ SSmobs.flock_cameras_by_zlevel[registered_z] -= src
+ if(client)
+ if(new_z)
+ SSmobs.flock_cameras_by_zlevel[new_z] += src
+ registered_z = new_z
+ else
+ registered_z = null
+
+/mob/camera/flock/proc/get_flock_data()
+ var/list/data = list()
+ data["name"] = real_name
+ data["area"] = get_area_name(src, TRUE) || "???"
+ data["ref"] = ref(src)
+
+ if(controlling_bird)
+ data["host"] = controlling_bird.real_name
+ data["health"] = controlling_bird.get_damage_percent()
+ else
+ data["host"] = null
+ data["health"] = 100
+
+ return data
+
+/mob/camera/flock/proc/so_very_sad_death()
+ if(client)
+ to_chat(src, SPAN_ALERT("You cease to exist."))
+
+ if(controlling_bird)
+ controlling_bird.release_control()
+
+ icon_state = "[base_icon_state]-ghost"
+ ghostize(FALSE)
+ flock?.free_unit(src)
+
+ invisibility = 0
+ notransform = TRUE
+ icon_state = "blank"
+ flick("[base_icon_state]-death", src)
+ addtimer(CALLBACK(src, PROC_REF(cleanup)), 2 SECONDS)
+
+/mob/camera/flock/proc/cleanup(datum/source)
+ qdel(src)
diff --git a/code/modules/antagonists/flockmind/flock_camera/flock_overmind.dm b/code/modules/antagonists/flockmind/flock_camera/flock_overmind.dm
new file mode 100644
index 00000000000..8d4f3b79b9c
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_camera/flock_overmind.dm
@@ -0,0 +1,88 @@
+/mob/camera/flock/overmind
+ name = "Flockmind"
+ desc = "A ghostly conglomerate of signals and sentient radio waves."
+
+ see_invisible = SEE_INVISIBLE_FLOCK
+
+ actions_to_grant = list(
+ /datum/action/cooldown/flock/create_rift,
+ )
+
+ /// Granted after create_rift is cast.
+ var/list/grant_upon_start = list(
+ /datum/action/cooldown/flock/control_panel,
+ /datum/action/cooldown/flock/create_structure,
+ /datum/action/cooldown/flock/partition_mind,
+ /datum/action/cooldown/flock/diffract_drone,
+ /datum/action/cooldown/flock/control_drone,
+ /datum/action/cooldown/flock/designate_tile,
+ /datum/action/cooldown/flock/designate_deconstruct,
+ /datum/action/cooldown/flock/designate_enemy,
+ /datum/action/cooldown/flock/designate_ignore,
+ /datum/action/cooldown/flock/ping,
+ /datum/action/cooldown/flock/repair_burst,
+ /datum/action/cooldown/flock/radio_blast,
+ /datum/action/cooldown/flock/radio_talk,
+ /datum/action/cooldown/flock/gatecrash,
+ )
+
+/mob/camera/flock/overmind/Initialize(mapload, join_flock)
+ . = ..()
+ flock.register_overmind(src)
+ real_name = "Flockmind [flock.name]"
+
+/mob/camera/flock/overmind/Destroy()
+ flock?.overmind = null // This shouldnt really happen
+ return ..()
+
+/mob/camera/flock/overmind/Login()
+ . = ..()
+ flock.refresh_unlockables()
+
+/mob/camera/flock/overmind/Logout()
+ . = ..()
+ // If we disconnect, free our homeboy
+ var/datum/action/cooldown/flock/control_drone/control_drone = locate() in actions
+ control_drone?.free_drone()
+
+/mob/camera/flock/overmind/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: [real_name]"),
+ SPAN_FLOCKSAY("Flock: [flock.name || "N/A"]"),
+ SPAN_FLOCKSAY("Bandwidth: [flock.bandwidth.has_points()]"),
+ SPAN_FLOCKSAY("Substrate: [flock.get_total_substrate()]"),
+ SPAN_FLOCKSAY("System Integrity: [flock.get_total_health_percentage()]%"),
+ SPAN_FLOCKSAY("Cognition: COMPUTATIONAL NEXUS"),
+ SPAN_FLOCKSAY("###=-"),
+ )
+
+/mob/camera/flock/overmind/get_status_tab_items()
+ var/list/status_tab_data = ..()
+ . = status_tab_data
+ status_tab_data[++status_tab_data.len] = list("", "")
+ status_tab_data[++status_tab_data.len] = list("Total Bandwidth: ", "[flock.bandwidth.has_points()]")
+ status_tab_data[++status_tab_data.len] = list("Used Bandwidth: ", "[flock.used_bandwidth]")
+ status_tab_data[++status_tab_data.len] = list("Available Bandwidth: ", "[flock.available_bandwidth()]")
+
+/mob/camera/flock/overmind/so_very_sad_death()
+ var/datum/flock/old_flock = flock
+ flock = null
+ old_flock?.overmind = null // to prevent infinite loop
+ old_flock.game_over()
+ . = ..()
+
+/mob/camera/flock/overmind/proc/spawn_rift(turf/T)
+ new /obj/structure/flock/rift(T, flock)
+
+ var/datum/action/cooldown/flock/create_rift/rift_action = locate() in actions
+ rift_action.Remove(src)
+
+ for(var/datum/action/A as anything in grant_upon_start)
+ A = new A()
+ A.Grant(src)
+
+ flock.start()
diff --git a/code/modules/antagonists/flockmind/flock_camera/flocktrace.dm b/code/modules/antagonists/flockmind/flock_camera/flocktrace.dm
new file mode 100644
index 00000000000..0761f165a4f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_camera/flocktrace.dm
@@ -0,0 +1,49 @@
+/mob/camera/flock/trace
+ name = "Flocktrace"
+ desc = "The representation of a partition of the will of the flockmind."
+ icon_state = "flocktrace"
+ base_icon_state = "flocktrace"
+
+ actions_to_grant = list(
+ /datum/action/cooldown/flock/gatecrash,
+ /datum/action/cooldown/flock/designate_tile,
+ /datum/action/cooldown/flock/designate_enemy,
+ /datum/action/cooldown/flock/designate_ignore,
+ /datum/action/cooldown/flock/ping,
+ )
+
+ var/bandwidth_provided = -FLOCK_COMPUTE_COST_FLOCKTRACE
+
+/mob/camera/flock/trace/Initialize(mapload, join_flock)
+ . = ..()
+ real_name = flock_realname(FLOCK_TYPE_TRACE)
+ flock.add_unit(src)
+ flock.stat_traces_made++
+
+/mob/camera/flock/trace/Destroy()
+ flock?.free_unit(src)
+ return ..()
+
+/mob/camera/flock/trace/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: [real_name]"),
+ SPAN_FLOCKSAY("Flock: [flock.name || "N/A"]"),
+ SPAN_FLOCKSAY("Bandwidth: [flock.bandwidth.has_points()]"),
+ SPAN_FLOCKSAY("System Integrity: [flock.get_total_health_percentage()]%"),
+ SPAN_FLOCKSAY("Cognition: SYNAPTIC PROCESS"),
+ SPAN_FLOCKSAY("###=-"),
+ )
+
+/mob/camera/flock/trace/vv_edit_var(var_name, var_value)
+ switch(var_name)
+ if(NAMEOF(src, bandwidth_provided))
+ flock?.bandwidth.adjust_points(-bandwidth_provided)
+ ..()
+ flock?.bandwidth.adjust_points(bandwidth_provided)
+ return TRUE
+
+ return ..()
diff --git a/code/modules/antagonists/flockmind/flock_controller/_flock_controller.dm b/code/modules/antagonists/flockmind/flock_controller/_flock_controller.dm
new file mode 100644
index 00000000000..f3d4bfd9f5e
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_controller/_flock_controller.dm
@@ -0,0 +1,677 @@
+/proc/get_default_flock() as /datum/flock
+ var/static/datum/flock/flock
+ if(isnull(flock))
+ flock = new
+
+ return flock
+
+/datum/flock
+ var/name = "BAD FLOCK"
+
+ /// The master of the flock
+ var/mob/camera/flock/overmind/overmind
+
+ /// Cache of images used by notices.
+ var/list/notice_images = list()
+ /// A k:V list of atoms the Overmind has marked for conversion, where the value is TRUE
+ var/list/marked_for_conversion = list()
+ /// A k:v list of atoms the Overmind has marked for deconstruction, where the value is TRUE
+ var/list/marked_for_deconstruction = list()
+ /// A k:V list of reserved_turf = TRUE.
+ var/list/turf_reservations = list()
+ /// A k:V list of flock mobs to their reserved turf.
+ var/list/turf_reservations_by_flock = list()
+
+ var/list/claimed_floors = list()
+ var/list/claimed_walls = list()
+
+ /// Every structure we've built.
+ var/list/structures = list()
+
+ /// The bits
+ var/list/bits = list()
+ /// The drones
+ var/list/drones = list()
+ /// The traces
+ var/list/traces = list()
+
+ /// A k:v list of mob : details, contains enemy mobs.
+ var/list/enemies = list()
+ /// A k:V list of mob : TRUE, where mob is mobs that ai will ignore.
+ var/list/ignores = list()
+
+ /// A k:V list of client : image, see ping().
+ var/list/active_pings = list()
+
+ var/list/datum/flock_unlockable/unlockables = list()
+ /// The total amount of computational power available, before whats being used.
+ var/datum/point_holder/bandwidth
+ /// The computational power being used.
+ var/used_bandwidth = 0
+ /// The maximum amount of traces allowed.
+ var/max_traces = 10
+
+ /// The current substrate cost to lay an egg.
+ var/current_egg_cost = FLOCK_SUBSTRATE_COST_LAY_EGG
+
+ var/flock_started = FALSE
+ /// Flock status, won, lost, etc
+ var/flock_game_status = NONE
+
+ /// Our relay, should it have been built
+ var/obj/structure/flock/relay/built_relay
+
+ /// Current UI tab, saves on data sending.
+ var/ui_tab = FLOCK_UI_DRONES
+
+ //* STAT TRACKING *//
+ var/stat_drones_made = 0
+ var/stat_bits_made = 0
+ var/stat_deaths = 0
+ var/stat_resources_gained = 0
+ var/stat_traces_made = 0
+ var/stat_tiles_made = 0
+ var/stat_structures_made = 0
+ var/stat_highest_bandwidth = 0
+
+/datum/flock/New()
+ name = flock_realname(FLOCK_TYPE_OVERMIND)
+
+ bandwidth = new()
+ create_hud_images()
+
+ var/list/all_unlockables = subtypesof(/datum/flock_unlockable)
+
+ for(var/unlockable in all_unlockables)
+ unlockables += new unlockable()
+ START_PROCESSING(SSprocessing, src)
+
+// Called by gamemode code
+/datum/flock/process()
+ if(flock_game_status == FLOCK_ENDGAME_LOST)
+ return
+
+ stat_highest_bandwidth = max(stat_highest_bandwidth, bandwidth.has_points())
+
+/// Called after everything is setup, and clients are in control of their mobs.
+/datum/flock/proc/start()
+ if(flock_started)
+ return
+
+ flock_started = TRUE
+ refresh_unlockables()
+
+/// Claim it for the flock, optionally converting it. Converting should only be avoided if the turf is already a flockturf.
+/datum/flock/proc/claim_turf(turf/T, convert = TRUE)
+ if(isnull(T))
+ return
+
+ free_turf(T)
+ if(convert)
+ T = flock_convert_turf(T, src)
+
+ if(isnull(T))
+ return
+
+ if(convert)
+ playsound(T, 'sound/items/deconstruct.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+
+ if(iswallturf(T))
+ claimed_walls[T] = TRUE
+ else
+ claimed_floors[T] = TRUE
+
+ T.AddComponent(/datum/component/flock_interest, src)
+ RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(claimed_turf_change))
+ SEND_SIGNAL(T, COMSIG_TURF_CLAIMED_BY_FLOCK, src)
+
+/// Stop tracking a turf that is in our claimed walls or claimed floors lists.
+/datum/flock/proc/stop_tracking_turf(turf/T)
+ if(iswallturf(T))
+ claimed_walls -= T
+ else
+ claimed_floors -= T
+
+ qdel(T.GetComponent(/datum/component/flock_interest))
+ UnregisterSignal(T, COMSIG_TURF_CHANGE)
+
+/// Reserves a turf, making AI ignore it for the purposes of targetting.
+/datum/flock/proc/reserve_turf(mob/living/basic/flock/user, turf/target, remove_on_change = TRUE)
+ if(turf_reservations_by_flock[user])
+ return FALSE
+ if(turf_reservations[target])
+ return FALSE
+
+ turf_reservations_by_flock[user] = target
+ turf_reservations[target] = user
+ add_notice(target, FLOCK_NOTICE_RESERVED)
+ if(remove_on_change)
+ RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(reserved_turf_change))
+ return TRUE
+
+/// Free a turf from reservation, allowing AI to target it again. override_turf can be given to lookup the user if there isnt a user in this context.
+/datum/flock/proc/free_turf(mob/living/basic/flock/user, turf/override_turf)
+ var/turf/to_free
+ if(user)
+ to_free = turf_reservations_by_flock[user]
+ else
+ user = turf_reservations[override_turf]
+ if(isnull(user))
+ return
+
+ to_free = override_turf
+
+ if(isnull(to_free))
+ return
+
+ remove_notice(to_free, FLOCK_NOTICE_RESERVED)
+ remove_notice(to_free, FLOCK_NOTICE_PRIORITY)
+ turf_reservations_by_flock -= user
+ turf_reservations -= to_free
+ marked_for_conversion -= to_free
+ UnregisterSignal(to_free, COMSIG_TURF_CHANGE)
+
+/// Returns TRUE if the given turf is not reserved.
+/datum/flock/proc/is_turf_free(turf/T)
+ return !turf_reservations[T]
+
+/// Returns TRUE if the given mob is an enemy.
+/datum/flock/proc/is_mob_enemy(mob/M)
+ return enemies[M]
+
+/// Returns TRUE if the given mob is ignored by the flock.
+/datum/flock/proc/is_mob_ignored(mob/M)
+ return ignores[M]
+
+/// Adds a unit to the flock and sets up all of the tracking.
+/datum/flock/proc/add_unit(mob/unit)
+ if(isflocktrace(unit))
+ traces += unit
+
+ var/mob/camera/flock/trace/ghostbird = unit
+ add_bandwidth_influence(ghostbird.bandwidth_provided)
+ return
+
+ if(isflockdrone(unit))
+ drones += unit
+ update_egg_cost()
+
+ else if(isflockbit(unit))
+ bits += unit
+
+ unit.AddComponent(/datum/component/flock_interest, src)
+
+ var/mob/living/basic/flock/bird = unit
+ add_bandwidth_influence(bird.bandwidth_provided)
+
+/// Removes a unit from the flock.
+/datum/flock/proc/free_unit(mob/unit)
+ if(isflocktrace(unit))
+ var/mob/camera/flock/trace/ghostbird = unit
+ ghostbird.flock = null
+ traces -= unit
+ remove_bandwidth_influence(ghostbird.bandwidth_provided)
+ return
+
+ else if(isflockdrone(unit))
+ var/mob/living/basic/flock/drone/bird = unit
+ bird.flock = null
+ drones -= unit
+ remove_bandwidth_influence(bird.bandwidth_provided)
+ update_egg_cost()
+
+ else if(isflockbit(unit))
+ var/mob/living/basic/flock/bit/bitty_bird = unit
+ bitty_bird.flock = null
+ bits -= unit
+ remove_bandwidth_influence(bitty_bird.bandwidth_provided)
+
+ remove_notice(unit, FLOCK_NOTICE_HEALTH)
+ free_turf(unit)
+ qdel(unit.GetComponent(/datum/component/flock_interest))
+
+ consider_game_over()
+
+/// Add a structure to the flock.
+/datum/flock/proc/add_structure(obj/structure/flock/struct)
+ structures += struct
+ struct.flock = src
+ struct.AddComponent(/datum/component/flock_interest, src)
+ add_bandwidth_influence(struct.bandwidth_provided)
+
+ if(istype(struct, /obj/structure/flock/egg))
+ update_egg_cost()
+
+/// Remove a structure from the flock.
+/datum/flock/proc/free_structure(obj/structure/flock/struct)
+ structures -= struct
+ qdel(struct.GetComponent(/datum/component/flock_interest))
+ struct.flock = null
+ if(struct.active)
+ remove_bandwidth_influence(-struct.active_bandwidth_cost)
+ else
+ remove_bandwidth_influence(struct.bandwidth_provided)
+
+ if(istype(struct, /obj/structure/flock/egg))
+ update_egg_cost()
+
+/// Wrapper for spawning a tealprint, used by Create Structure
+/datum/flock/proc/create_structure(turf/location, structure_type)
+ new /obj/structure/flock/tealprint(location, src, structure_type)
+
+/// Wrapper for handling bandwidth alongside used_bandwidth for new mobs
+/datum/flock/proc/add_bandwidth_influence(num)
+ if(num < 0)
+ used_bandwidth += abs(num)
+ else
+ bandwidth.adjust_points(num)
+
+ refresh_unlockables()
+
+/// Wrapper for handling bandwidth alongside used_bandwidth for mobs leaving the flock
+/datum/flock/proc/remove_bandwidth_influence(num)
+ if(num < 0)
+ used_bandwidth -= abs(num)
+ else
+ bandwidth.adjust_points(-num)
+
+ refresh_unlockables()
+
+/// Refreshes the status of every unlockable.
+/datum/flock/proc/refresh_unlockables()
+ if(!flock_started)
+ return
+
+ var/new_total = bandwidth.has_points()
+ var/new_available = available_bandwidth()
+
+ for(var/datum/flock_unlockable/unlockable as anything in unlockables)
+ unlockable.refresh_lock_status(src, new_total, new_available)
+
+/// Returns the total amount of bandwidth, including bandwidth being used.
+/datum/flock/proc/total_bandwidth()
+ return bandwidth.has_points()
+
+/// Returns the total amount claimed turfs
+/datum/flock/proc/total_turfs()
+ return (length(claimed_floors) + length(claimed_walls))
+
+/// Returns the amount of available bandwidth. Can return negative if over budget.
+/datum/flock/proc/available_bandwidth()
+ return bandwidth.has_points() - used_bandwidth
+
+/// Returns TRUE if the flock has the required bandwidth
+/datum/flock/proc/can_afford(amt)
+ return amt <= max(available_bandwidth(), 0)
+
+/// Updates the substrate cost of an egg given the current status of the flock.
+/datum/flock/proc/update_egg_cost()
+ var/egg_count = 0
+ for(var/obj/structure/flock/egg/egg as anything in structures)
+ egg_count++
+
+ var/status_addend = (length(drones) + egg_count) ** sqrt(2)
+ current_egg_cost = round(FLOCK_SUBSTRATE_COST_LAY_EGG + status_addend, 10)
+
+/// Returns the substract requirement to be able to spend substrate on an egg. See above proc.
+/datum/flock/proc/get_egg_elligibility_cost()
+ var/ideal_pop_factor = length(drones) - FLOCK_MIN_DESIRED_POP
+ if(ideal_pop_factor <= 0)
+ return current_egg_cost
+
+ return round(current_egg_cost + (ideal_pop_factor * FLOCK_ADDITIONAL_RESOURCE_RESERVATION_PER_DRONE), 1)
+
+/// Sets the flock's overmind
+/datum/flock/proc/register_overmind(mob/camera/flock_overmind)
+ overmind = flock_overmind
+
+/// Updates a mob's enemy status. If they are already an enemy, their entry will be updated with new information.
+/datum/flock/proc/update_enemy(atom/movable/enemy)
+ if(is_mob_ignored(enemy))
+ return FALSE
+
+ for(var/mob/living/L in enemy.buckled_mobs)
+ update_enemy(L)
+
+ if(!isliving(enemy))
+ return FALSE
+ if(!enemies[enemy])
+ RegisterSignal(enemy, COMSIG_PARENT_QDELETING, PROC_REF(on_enemy_gone))
+ add_notice(enemy, FLOCK_NOTICE_ENEMY)
+ enemies[enemy] = get_area_name(enemy)
+ return TRUE
+
+/// Removes an atom from the enemies list.
+/datum/flock/proc/remove_enemy(atom/movable/enemy, skip_buckled)
+ if(!skip_buckled)
+ for(var/mob/living/L in enemy.buckled_mobs)
+ remove_enemy(L)
+
+ if(!isliving(enemy))
+ return
+
+ enemies -= enemy
+ UnregisterSignal(enemy, COMSIG_PARENT_QDELETING)
+ remove_notice(enemy, FLOCK_NOTICE_ENEMY)
+ return
+
+/// Adds a mob to the ignored list.
+/datum/flock/proc/add_ignore(atom/movable/ignore)
+ for(var/mob/living/L in ignore.buckled_mobs)
+ add_ignore(L)
+
+ if(!isliving(ignore))
+ return
+
+ if(!enemies[ignore])
+ RegisterSignal(ignore, COMSIG_PARENT_QDELETING, PROC_REF(on_ignore_gone))
+ add_notice(ignore, FLOCK_NOTICE_IGNORE)
+ ignores[ignore] = TRUE
+
+/// Removes a mob from the ignore list.
+/datum/flock/proc/remove_ignore(atom/movable/ignore, skip_buckled)
+ if(!skip_buckled)
+ for(var/mob/living/L in ignore.buckled_mobs)
+ add_ignore(L)
+
+ if(!isliving(ignore))
+ return
+
+ ignores -= ignore
+ remove_notice(ignore, FLOCK_NOTICE_IGNORE)
+ UnregisterSignal(ignore, COMSIG_PARENT_QDELETING)
+
+/// (un)Mark a flock atom for deconstruction. Returns FALSE if it cannot be deconstructed.
+/datum/flock/proc/toggle_deconstruction_mark(atom/A)
+ if(ismob(A) || !HAS_TRAIT(A, TRAIT_FLOCK_THING) || HAS_TRAIT(A, TRAIT_FLOCK_NODECON))
+ return FALSE
+
+ if(marked_for_deconstruction[A])
+ marked_for_deconstruction -= A
+ UnregisterSignal(A, COMSIG_PARENT_QDELETING)
+ remove_notice(A, FLOCK_NOTICE_DECONSTRUCT)
+ else
+ marked_for_deconstruction[A] = TRUE
+ RegisterSignal(A, COMSIG_PARENT_QDELETING, PROC_REF(deconstruct_mark_deleted))
+ add_notice(A, FLOCK_NOTICE_DECONSTRUCT)
+ return TRUE
+
+/// Places a flock notice on an atom. See flock_defines.dm
+/datum/flock/proc/add_notice(atom/target, notice_type)
+ if(!overmind)
+ return
+ var/image/I = image(notice_images[notice_type], loc = target)
+ var/list/things_that_can_see_notices = list(overmind) + traces
+ return target.add_alt_appearance(notice_type, I, things_that_can_see_notices)
+
+/// Removes a flock notice from an atom.
+/datum/flock/proc/remove_notice(atom/target, notice_type)
+ target.remove_alt_appearance(notice_type)
+
+/// Returns a list of turfs marked for conversion, minus turfs that drones have already reserved.
+/datum/flock/proc/get_priority_turfs(mob/living/basic/flock/bird)
+ if(!length(marked_for_conversion))
+ return null
+
+ return marked_for_conversion - turf_reservations
+
+/// Pings a location, alerting all flocktraces.
+/datum/flock/proc/ping(turf/T, mob/camera/flock/pinger)
+ var/message = "System interrupt. Designating new target: [T] in [get_area(T)]."
+ flock_talk(pinger, message, src, TRUE)
+ T.AddComponent(/datum/component/flock_ping, 5 SECONDS)
+
+ for(var/mob/camera/flock/ghost_bird in (traces + overmind))
+ var/client/target_client = ghost_bird.client
+ if(!target_client)
+ if(!ghost_bird.controlling_bird?.client)
+ continue
+ target_client = ghost_bird.controlling_bird.client
+
+ target_client.mob.playsound_local(null, 'sound/goonstation/flockmind/ping.ogg', 50, TRUE)
+ if(ghost_bird == pinger)
+ continue
+
+ var/image/pointer = pointer_image_to(target_client.mob, T)
+ animate(pointer, time = 3 SECONDS, alpha = 0)
+ add_ping_image(target_client, pointer, 3 SECONDS)
+
+/// Returns the total percentage of the sum of every mob's health and max health.
+/datum/flock/proc/get_total_health_percentage()
+ var/numerator = 0
+ var/denominator = 0
+ for(var/mob/living/basic/flock/bird as anything in (drones + bits))
+ numerator += bird.health
+ denominator += bird.maxHealth
+
+ if(denominator == 0) // somehow i guess
+ return 0
+
+ return round(numerator / denominator * 100, 0.1)
+
+/// Returns the total amount of substrate by all flock mobs.
+/datum/flock/proc/get_total_substrate()
+ . = 0
+ for(var/mob/living/basic/flock/drone/bird as anything in drones)
+ . += bird.substrate.has_points()
+
+/// Called when a turf reserved by a flock mob changes.
+/datum/flock/proc/reserved_turf_change(datum/source)
+ SIGNAL_HANDLER
+ free_turf(override_turf = source)
+
+/// Called when a turf owned by the flock changes.
+/datum/flock/proc/claimed_turf_change(turf/source)
+ SIGNAL_HANDLER
+ if(isflockturf(source))
+ stop_tracking_turf(source)
+ claim_turf(source, FALSE) // Wall to floor or visa-versa, keep it updated.
+ else
+ stop_tracking_turf(source)
+
+/// Helper for adding a ping image to a client's screen and handling clean up.
+/datum/flock/proc/add_ping_image(client/C, image/ping, duration)
+ if(isnull(C))
+ return
+
+ C.images += ping
+ active_pings[C] += list(ping)
+ RegisterSignal(C, COMSIG_PARENT_QDELETING, PROC_REF(on_client_gone), override = TRUE)
+ addtimer(CALLBACK(src, PROC_REF(cleanup_ping_images), C, ping), 3 SECONDS)
+
+/// Called by add_ping_image via timer callback.
+/datum/flock/proc/cleanup_ping_images(client/C, list/images_to_clean)
+ if(isnull(C))
+ return
+
+ var/list/images = images_to_clean || active_pings[C]
+ C.images -= images
+ active_pings[C] -= images
+ if(!length(active_pings[C]))
+ active_pings -= C
+ UnregisterSignal(C, COMSIG_PARENT_QDELETING)
+
+/// Checks to see if the given atom is allowed to be there. Has lots of behavior like making drones dormant and snapping traces back to the station.
+/datum/flock/proc/is_on_safe_z(atom/A)
+ var/turf/T = get_turf(A)
+ if(!T.z)
+ return FALSE
+
+ if(flock_game_status == FLOCK_ENDGAME_LOST)
+ return TRUE
+
+ if(is_station_level(T.z))
+ return TRUE
+
+ return FALSE
+
+/// Called when a client holding a ping image disconnects.
+/datum/flock/proc/on_client_gone(client/source)
+ SIGNAL_HANDLER
+ cleanup_ping_images()
+
+/// Called when a unit under the flock's control dies.
+/datum/flock/proc/on_unit_death(datum/source)
+ SIGNAL_HANDLER
+ free_unit(source)
+
+/// Called when an enemy atom is deleted.
+/datum/flock/proc/on_enemy_gone(datum/source)
+ SIGNAL_HANDLER
+ remove_enemy(source, TRUE)
+
+/// Called when an ignored mob is deleted.
+/datum/flock/proc/on_ignore_gone(datum/source)
+ SIGNAL_HANDLER
+ remove_ignore(source, TRUE)
+
+/// Instantiates all of the flock notice image singletons.
+/datum/flock/proc/create_hud_images()
+ notice_images[FLOCK_NOTICE_RESERVED] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "frontier";
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ alpha = 80;
+ }
+
+ notice_images[FLOCK_NOTICE_PRIORITY] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "frontier";
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ alpha = 180;
+ }
+
+ notice_images[FLOCK_NOTICE_ENEMY] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "hazard";
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+ notice_images[FLOCK_NOTICE_IGNORE] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "ignore";
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+ notice_images[FLOCK_NOTICE_FLOCKMIND_CONTROL] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "flockmind_face";
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+ notice_images[FLOCK_NOTICE_FLOCKTRACE_CONTROL] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "flocktrace_face";
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+ notice_images[FLOCK_NOTICE_DECONSTRUCT] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "deconstruct";
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+ notice_images[FLOCK_NOTICE_HEALTH] = new /image{
+ icon = 'icons/goonstation/mob/featherzone.dmi';
+ icon_state = "hp-100";
+ pixel_x = 10;
+ pixel_y = 16;
+ plane = ABOVE_LIGHTING_PLANE;
+ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
+ }
+
+/// Setter for flock_game_status.
+/datum/flock/proc/set_flock_game_status(new_status)
+ var/old_status = flock_game_status
+ if(old_status == new_status)
+ return null
+
+ flock_game_status = new_status
+
+ return old_status
+
+/// Ends the flock if it is unable to continue spreading.
+/datum/flock/proc/consider_game_over()
+ if(!flock_started)
+ return FALSE
+
+ if(flock_game_status == FLOCK_ENDGAME_LOST)
+ return TRUE
+
+ if(flock_game_status == FLOCK_ENDGAME_VICTORY)
+ return TRUE
+
+ if(length(drones))
+ return FALSE
+
+ if(locate(/obj/structure/flock/egg, structures) || locate(/obj/structure/flock/rift, structures))
+ return FALSE
+
+ game_over()
+ return TRUE
+
+/// Kills off the flock. Pass completely_destroy = FALSE to allow the overmind to live on.
+/datum/flock/proc/game_over(completely_destroy = TRUE)
+ set waitfor = FALSE
+
+ // Cleanup any pings
+ for(var/client/C in active_pings)
+ cleanup_ping_images(C)
+
+ // Cleanup turf claims
+ for(var/turf/T as anything in turf_reservations)
+ free_turf(override_turf = T)
+
+ for(var/turf/flockturf as anything in claimed_floors + claimed_walls)
+ stop_tracking_turf(flockturf)
+ CHECK_TICK
+
+ // Extra lives
+ if(!completely_destroy)
+ refresh_unlockables()
+ return
+
+ set_flock_game_status(FLOCK_ENDGAME_LOST)
+
+ // Kill overmind
+ overmind?.so_very_sad_death() // Overmind can be null here if it died outside of game_over().
+
+ // Kill traces
+ for(var/mob/camera/flock/trace/ghostbird as anything in traces)
+ ghostbird.so_very_sad_death()
+
+ // Free units
+ for(var/mob/living/basic/flock/bird as anything in (bits + drones))
+ bird.dormantize()
+
+ // Blow up structures
+ for(var/obj/structure/flock/structure as anything in structures)
+ structure.deconstruct(FALSE)
+
+ // Remove ignores
+ for(var/mob/M as anything in ignores)
+ remove_ignore(M, TRUE)
+
+ // Remove enemies
+ for(var/mob/M as anything in enemies)
+ remove_enemy(M, TRUE)
+
+ STOP_PROCESSING(SSprocessing, src)
+
+/// Called when an atom marked for deconstruction is qdeleted.
+/datum/flock/proc/deconstruct_mark_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ toggle_deconstruction_mark(source)
diff --git a/code/modules/antagonists/flockmind/flock_controller/flock_ui.dm b/code/modules/antagonists/flockmind/flock_controller/flock_ui.dm
new file mode 100644
index 00000000000..9f8295bc983
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_controller/flock_ui.dm
@@ -0,0 +1,119 @@
+/datum/flock/ui_state(mob/user)
+ return GLOB.flock_state
+
+/datum/flock/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "FlockPanel")
+ ui.open()
+
+/datum/flock/ui_data(mob/user)
+ var/list/data = list()
+ var/list/drone_info = list()
+ var/list/trace_info = list()
+ var/list/enemy_info = list()
+ var/list/vitals_info = list("name" = name)
+ var/list/stats_info = list()
+ var/list/structure_info = list()
+
+ data["drones"] = drone_info
+ data["partitions"] = trace_info
+ data["vitals"] = vitals_info
+ data["enemies"] = enemy_info
+ data["structures"] = structure_info
+ data["category"] = ui_tab
+ data["stats"] = stats_info
+ data["category_lengths"] = list(
+ "traces" = length(traces),
+ "drones" = length(drones),
+ "enemies" = length(enemies),
+ "structures" = length(structures),
+ )
+
+ switch(ui_tab)
+ if(FLOCK_UI_DRONES)
+ for(var/mob/living/basic/flock/drone/bird as anything in drones)
+ drone_info[++drone_info.len] = bird.get_flock_data()
+
+ if(FLOCK_UI_TRACES)
+ for(var/mob/camera/flock/trace/ghost_bird as anything in traces)
+ trace_info[++trace_info.len] = ghost_bird.get_flock_data()
+
+ if(FLOCK_UI_ENEMIES)
+ for(var/mob/enemy in enemies)
+ var/list/mob_data = list()
+ mob_data["name"] = enemy.name
+ mob_data["area"] = enemies[enemy]
+ mob_data["ref"] = ref(enemy)
+ enemy_info[++enemy_info.len] = mob_data
+
+ if(FLOCK_UI_STRUCTURES)
+ for(var/obj/structure/flock/struct as anything in structures)
+ structure_info[++structure_info.len] = struct.get_flock_data()
+
+ stats_info[++stats_info.len] = list(name = "Drones realized: ", "value" = stat_drones_made)
+ stats_info[++stats_info.len] = list(name = "Bits formed: ", "value" = stat_bits_made)
+ stats_info[++stats_info.len] = list(name = "Total deaths: ", "value" = stat_deaths)
+ stats_info[++stats_info.len] = list(name = "Resources gained: ", "value" = stat_resources_gained)
+ stats_info[++stats_info.len] = list(name = "Partitions divided: ", "value" = stat_traces_made)
+ stats_info[++stats_info.len] = list(name = "Tiles converted: ", "value" = stat_tiles_made)
+ stats_info[++stats_info.len] = list(name = "Structures materialized: ", "value" = stat_structures_made)
+ stats_info[++stats_info.len] = list(name = "Highest bandwidth: ", "value" = stat_highest_bandwidth)
+
+ return data
+
+/datum/flock/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = ui.user
+ switch(action)
+ if("change_tab")
+ if(ui_tab != params["tab"])
+ ui_tab = params["tab"]
+ return TRUE
+
+ if("jump_to")
+ var/atom/movable/target = locate(params["origin"])
+ var/turf/T = get_turf(target)
+ if(isnull(T) || !is_on_safe_z(target))
+ to_chat(user, SPAN_ALERT("They are beyond our reach."))
+ return
+
+ if(isflockdrone(user))
+ var/mob/living/basic/flock/drone/bird = user
+ user = bird.controlled_by
+ bird.release_control()
+
+ user.forceMove(T)
+
+ if("remove_enemy")
+ var/mob/enemy = locate(params["origin"])
+ if(enemy)
+ remove_enemy(enemy)
+ return TRUE
+
+ if("rally")
+ var/mob/living/basic/flock/drone/bird = locate(params["origin"])
+ if(istype(bird))
+ bird.rally(get_turf(user))
+ return TRUE
+
+ if("cancel_tealprint")
+ var/obj/structure/flock/tealprint/tealprint = locate(params["origin"])
+ tealprint?.try_cancel_structure()
+ return TRUE
+
+ if("delete_trace")
+ var/mob/camera/flock/trace/flocktrace = locate(params["origin"])
+ if(!istype(flocktrace))
+ message_admins("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]")
+ log_game("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]")
+ return TRUE
+
+ if(tgui_alert(user, "This will destroy the Flocktrace. Are you sure you want to do this?", "Confirmation", list("Yes", "No")) == "Yes")
+ flock_talk(null, "Partition [flocktrace.real_name] has been reintegrated into flock background processes.", src, involuntary = TRUE)
+ to_chat(flocktrace, SPAN_FLOCKSAY("Your higher cognition has been forcibly reintegrated into the collective will of the flock."))
+ flocktrace.so_very_sad_death()
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/flock_convert_atom.dm b/code/modules/antagonists/flockmind/flock_convert_atom.dm
new file mode 100644
index 00000000000..57f22f8dfe9
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_convert_atom.dm
@@ -0,0 +1,141 @@
+/proc/flock_convert_turf(turf/T, datum/flock/flock, force)
+ if(!T?.can_flock_convert(force))
+ return
+
+ if(isflockturf(T))
+ . = T
+
+ else if(iswallturf(T))
+ . = T.ChangeTurf(/turf/simulated/wall/flock, ignore_air = TRUE)
+
+ else if(isfloorturf(T))
+ . = T.ChangeTurf(/turf/simulated/floor/flock, ignore_air = TRUE)
+ var/list/datum/element/decal/decals = T.get_decals()
+ for(var/datum/element/decal/dcl in decals)
+ dcl.Detach(T)
+ T.RemoveElement(/datum/element/decal)
+
+ var/obj/structure/lattice/L = locate() in .
+ if(L)
+ qdel(L)
+ if(istype(L, /obj/structure/lattice/catwalk))
+ . = T.ChangeTurf(/turf/simulated/floor/flock)
+
+ for(var/obj/O in .)
+ if(iseffect(O))
+ continue
+
+ O.try_flock_convert(flock, force)
+
+/// Attempt to convert an object. Default behavior is to do nothing.
+/obj/proc/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/machinery/camera/try_flock_convert(datum/flock/flock, force)
+ deconstruct()
+
+/obj/structure/window/try_flock_convert(datum/flock/flock, force)
+ var/turf/T = loc
+ var/obj/structure/window/flock/new_window
+ qdel(src)
+
+ if(fulltile)
+ new_window = new /obj/structure/window/flock/fulltile(T)
+ else
+ new_window = new /obj/structure/window/flock(T)
+ new_window.dir = dir
+
+ new_window.AddComponent(/datum/component/flock_interest, flock)
+ return new_window
+
+/obj/machinery/door/try_flock_convert(datum/flock/flock, force)
+ var/turf/T = loc
+ qdel(src)
+ return new /obj/machinery/door/flock(T)
+
+// This results in double layered doors
+/obj/machinery/door/firedoor/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/machinery/light/try_flock_convert(datum/flock/flock, force)
+ var/obj/L = new /obj/machinery/light/flock(loc)
+ L.setDir(dir)
+ qdel(src)
+ return L
+
+/obj/machinery/light/floor/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/machinery/light/floor/has_bulb/flock(loc)
+ qdel(src)
+
+/obj/structure/computerframe/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/structure/flock/compute(loc, flock)
+ qdel(src)
+
+/obj/machinery/computer4/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/structure/flock/compute(loc, flock)
+ qdel(src)
+
+/obj/machinery/computer/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/structure/flock/compute(loc, flock)
+ qdel(src)
+
+/obj/machinery/seed_extractor/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/structure/flock/compute(loc, flock)
+ qdel(src)
+
+/obj/machinery/telecomms/try_flock_convert(datum/flock/flock, force)
+ . = new /obj/structure/flock/compute(loc, flock)
+ qdel(src)
+
+/obj/structure/chair/try_flock_convert(datum/flock/flock, force)
+ . = ..()
+ var/obj/structure/chair/comfy/flock/F = new /obj/structure/chair/comfy/flock(get_turf(src), flock)
+ F.AddComponent(/datum/component/flock_interest, flock)
+ F.setDir(dir)
+ qdel(src)
+
+/obj/structure/lattice/try_flock_convert(datum/flock/flock, force)
+ . = ..()
+ var/obj/structure/lattice/flock/F = new /obj/structure/lattice/flock(get_turf(src))
+ F.AddComponent(/datum/component/flock_interest, flock)
+ qdel(src)
+
+/obj/structure/closet/try_flock_convert(datum/flock/flock, force)
+ . = ..()
+ dump_contents()
+ var/obj/structure/closet/flock/F = new(get_turf(src), flock)
+ F.take_contents()
+ F.AddComponent(/datum/component/flock_interest, flock)
+ qdel(src)
+
+/obj/machinery/porta_turret/try_flock_convert(datum/flock/flock, force)
+ . = ..()
+ new /obj/structure/flock/gnesis_turret(get_turf(src), flock)
+ qdel(src)
+
+/obj/structure/grille/try_flock_convert(datum/flock/flock, force)
+ var/obj/structure/grille/G
+ if(broken)
+ G = new /obj/structure/grille/flock/broken(get_turf(src))
+ else
+ G = new /obj/structure/grille/flock(get_turf(src))
+ G.AddComponent(/datum/component/flock_interest, flock)
+ qdel(src)
+
+/turf/proc/can_flock_convert(force)
+ return FALSE
+
+/turf/space/can_flock_convert(force)
+ var/obj/structure/lattice/L = locate() in src
+ if(L)
+ return TRUE
+ return FALSE
+
+/turf/simulated/floor/can_flock_convert(force)
+ return TRUE
+
+/turf/simulated/floor/flock/can_flock_convert(force)
+ return TRUE
+
+/turf/simulated/wall/can_flock_convert(force)
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/flock_effects.dm b/code/modules/antagonists/flockmind/flock_effects.dm
new file mode 100644
index 00000000000..5d3d24319cf
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_effects.dm
@@ -0,0 +1,23 @@
+/obj/effect/temp_visual/flock // temporary flock visual feedback objects
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "blank"
+ layer = ABOVE_NORMAL_TURF_LAYER
+ duration = 4.5 SECONDS
+ var/animation
+
+/obj/effect/temp_visual/flock/Initialize(mapload)
+ . = ..()
+ flick(animation, src)
+
+/obj/effect/temp_visual/flock/wall_convert
+ icon_state = "spawn-wall-loop"
+ animation = "spawn-wall"
+
+/obj/effect/temp_visual/flock/floor_convert
+ icon_state = "spawn-floor-loop"
+ animation = "spawn-floor"
+
+/obj/effect/temp_visual/flock/flock_heal_burst
+ duration = 0.7 SECONDS
+ icon_state = "telemarker"
+ animation = "telemarker"
diff --git a/code/modules/antagonists/flockmind/flock_global_lists.dm b/code/modules/antagonists/flockmind/flock_global_lists.dm
new file mode 100644
index 00000000000..0c6b67001f4
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_global_lists.dm
@@ -0,0 +1,54 @@
+GLOBAL_LIST_INIT(flockdrone_created_phrases, list(
+ "online, flock protocols initiating",
+ "awoken, concordance achieved",
+ "existence achieved, analysing situation",
+ "assembling process complete",
+))
+
+GLOBAL_LIST_INIT(flockdrone_death_phrases, list(
+ "terminal damage, system malf---",
+ "situation untenable, death imminen---",
+ "severe damage, unable to recov---",
+ "notification: unit death---"
+))
+
+GLOBAL_LIST_INIT(flockdrone_new_enemy_phrases, list(
+ "new enemy,",
+ "new transgressor detected,",
+ "new threat is,",
+ "damage sustained, no thanks to",
+ "new violent individual, be warned, ident is",
+ "please destroy"
+))
+
+GLOBAL_LIST_INIT(flockdrone_name_adjectives, list(
+ "weird",
+ "odd",
+ "strange",
+ "unusual",
+ "distinctive",
+ "glowy",
+ "flashy",
+ "shiny",
+ "bright",
+ "pulsing",
+ "gaudy",
+ "flickering",
+ "humming"
+))
+
+GLOBAL_LIST_INIT(flockdrone_name_nouns, list(
+ "thing",
+ "device",
+ "machine",
+ "construct",
+ "apparatus",
+ "object"
+))
+
+GLOBAL_LIST_INIT(flock_betrayal_phrases, list(
+ "disregarding attack from ignored target",
+ "turning a blind eye to",
+ "revenge not taken on",
+ "ignoring attack from",
+))
diff --git a/code/modules/antagonists/flockmind/flock_hooks.dm b/code/modules/antagonists/flockmind/flock_hooks.dm
new file mode 100644
index 00000000000..b8d9ef24894
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_hooks.dm
@@ -0,0 +1,3 @@
+/// Returns the name that the flock has given an object. Flavor.
+/atom/proc/get_flock_id()
+ return
diff --git a/code/modules/antagonists/flockmind/flock_items/flock_cache.dm b/code/modules/antagonists/flockmind/flock_items/flock_cache.dm
new file mode 100644
index 00000000000..1d934f662fb
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_items/flock_cache.dm
@@ -0,0 +1,25 @@
+/obj/item/reagent_containers/glass/gnesis
+ name = "fluid-filled octahedron"
+ desc = "A strange container made of a crystalline matrix. It looks like it can hold reagents but it's a mystery how to access them."
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "minicache"
+ inhand_icon_state = "beaker"
+ possible_transfer_amounts = list(5, 10, 15, 25, 30, 50, 100, 200)
+ has_lid = FALSE
+ volume = 200
+ container_type = REFILLABLE | DRAINABLE
+ materials = list(MAT_GNESIS_GLASS = 2000)
+
+/obj/item/reagent_containers/glass/gnesis/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Reagent Minicache"),
+ SPAN_FLOCKSAY("System Storage: [(reagents.total_volume / volume)]% filled."),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/item/reagent_containers/glass/gnesis/prefilled
+ list_reagents = list("gnesis_tox" = 50)
diff --git a/code/modules/antagonists/flockmind/flock_items/flock_cube.dm b/code/modules/antagonists/flockmind/flock_items/flock_cube.dm
new file mode 100644
index 00000000000..6e2d2e35050
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_items/flock_cube.dm
@@ -0,0 +1,26 @@
+// The cuuuuuuuuuuuuuube
+/obj/item/flock_cube
+ name = "weird cube"
+ desc = "A weird looking cube."
+ icon_state = "cube"
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ light_color = "#26ffe6"
+ light_power = 0.2
+ light_range = 2
+
+ materials = list(MAT_METAL = 1000, MAT_GLASS = 1000, MAT_GNESIS = 2000, MAT_GNESIS_GLASS = 2000)
+ origin_tech = "materials=5;biotech=5;programming=5"
+
+ /// How much shit in da cube
+ var/substrate = 10
+
+/obj/item/flock_cube/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Substrate Cube"),
+ SPAN_FLOCKSAY("System Storage: [substrate] units"),
+ SPAN_FLOCKSAY("###=-")
+ )
diff --git a/code/modules/antagonists/flockmind/flock_language.dm b/code/modules/antagonists/flockmind/flock_language.dm
new file mode 100644
index 00000000000..787d160a0b7
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_language.dm
@@ -0,0 +1,79 @@
+/datum/language/flock
+ name = "Symphonic"
+ speech_verb = "caws"
+ space_chance = 60
+ key = "f"
+ flags = RESTRICTED | HIVEMIND | NOLIBRARIAN | NOBABEL | NO_STUTTER
+ colour = "flocksay"
+ syllables = list("c#aw", "kr%k", "c@rk", "ka~w", "cr!k", "sk^a", "r#aw", "c*rr", "ka%x", "t~ch", "gr!k", "c&aw", "k!rr", "ch#k", "ra%k", "sk@w", "tr^k", "c~ra", "kw!p", "z#aw", "cl%k", "r@rk", "sh^k", "k~ra", "v!aw")
+
+/datum/language/flock/get_spoken_verb(msg_end)
+ switch(msg_end)
+ if("!")
+ return pick(exclaim_verbs)
+ if("?")
+ return ask_verb
+ return pick("sings", "clicks", "whistles", "intones", "transmits", "submits", "uploads", "caws")
+
+/datum/language/flock/format_message(message)
+ return SPAN_FLOCKSAY_GRADIENT("[get_spoken_verb(message)] \"[message]\"")
+
+/datum/language/flock/broadcast(atom/speaker, message, speaker_mask, involuntary = FALSE)
+ var/log_message = "(FLOCK) [message]"
+ if(ismob(speaker))
+ var/mob/M = speaker
+ log_say(log_message, speaker)
+ M.create_log(SAY_LOG, log_message)
+
+ var/list/message_start
+ var/list/message_body = list(format_message(message))
+
+ if(!speaker)
+ message_start = list("[name]: ", SPAN_FLOCKSAY_GRADIENT("System"))
+ else if(!ismob(speaker))
+ message_start = list("[name]: ", SPAN_FLOCKSAY_GRADIENT("[speaker.name]"))
+ else if(isflockcontroller(speaker))
+ var/mob/speaking_mob = speaker
+ message_start = list("[name]: ", SPAN_FLOCKSAY_GRADIENT("[isflockmind(speaker) ? "" : "Flocktrace "][speaking_mob.real_name]"))
+ message_body += ""
+ else if(isflockworker(speaker))
+ var/mob/speaking_mob = speaker
+ message_start = list("[name]: ", SPAN_FLOCKSAY_GRADIENT("Drone [speaking_mob.real_name]"))
+
+ for(var/mob/M in GLOB.dead_mob_list)
+ if(!isnewplayer(M) && !isbrain(M) && speaker)
+ var/list/message_start_dead = list("[name], [SPAN_NAME("[speaker.name] ([ghost_follow_link(speaker, ghost=M)])")]")
+ var/list/dead_message = message_start_dead + message_body
+ M.show_message(dead_message.Join(" "), 2)
+
+ if(speaker && !isflockcontroller(speaker))
+ var/scrambled = SPAN_FLOCKSAY_GRADIENT(scramble(message))
+ var/living_msg = "[speaker.name] [get_spoken_verb(message)] \"[scrambled]\""
+ for(var/mob/M in hearers(5, get_turf(speaker)))
+ M.show_message(living_msg, 2)
+ if(M.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
+ var/mob/speaking_mob = speaker
+ M.create_chat_message(locateUID(speaking_mob.runechat_msg_location), scrambled)
+
+ for(var/mob/F in GLOB.alive_mob_list)
+ if(!isflockmob(F))
+ continue
+ var/list/final_message = message_start + message_body
+ F.show_message(final_message.Join(" "), 2)
+ if(F.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
+ var/mob/speaking_mob = speaker
+ F.create_chat_message(locateUID(speaking_mob.runechat_msg_location), message)
+
+ var/list/silicon_message_start = list("Robot Talk, [SPAN_NAME("Strange Static")]")
+ var/silicon_message_content = Gibberish(message, 70, replace_rate = 50)
+ var/silicon_message = SPAN_ROBOT(silicon_message_content)
+ var/list/final_message_silicon = silicon_message_start + silicon_message
+ if(involuntary || !prob(30))
+ return
+ for(var/mob/living/silicon/S in GLOB.alive_mob_list)
+ if(!S.binarycheck())
+ continue
+ S.show_message(final_message_silicon.Join(" "), 2)
+ if(isflockworker(speaker) && S.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
+ var/mob/speaking_mob = speaker
+ S.create_chat_message(locateUID(speaking_mob.runechat_msg_location), silicon_message_content)
diff --git a/code/modules/antagonists/flockmind/flock_mob/flock_mob.dm b/code/modules/antagonists/flockmind/flock_mob/flock_mob.dm
new file mode 100644
index 00000000000..d2901aa5d0a
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flock_mob.dm
@@ -0,0 +1,224 @@
+/mob/living/basic/flock
+ name = "flockdrone"
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "drone"
+ icon_living = "drone"
+ icon_dead = "drone-dead"
+
+ pass_flags_self = PASSTABLE | PASSFLOCK
+
+ a_intent = INTENT_DISARM
+
+ light_color = "#26ffe6"
+ light_power = 0.2
+ light_range = 2
+
+ faction = list("flockmind")
+ ai_controller = /datum/ai_controller/flock
+
+ sentience_type = SENTIENCE_OTHER
+
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = 1000
+ 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)
+ damage_coeff = list(BRUTE = 1.5, BURN = 0.2, TOX = 0.2, CLONE = 0, STAMINA = 0, OXY = 0)
+
+ initial_traits = list(TRAIT_FLYING, TRAIT_FLOCK_THING)
+
+ butcher_results = list(/obj/item/stack/sheet/gnesis = 1)
+ blood_color = "#4d736d"
+ blood_volume = BLOOD_VOLUME_OKAY
+
+ contains_xeno_organ = TRUE
+
+ var/list/actions_to_grant = list(
+ /datum/action/cooldown/flock/convert,
+ )
+
+ var/icon_dormant = "drone-dormant"
+
+ /// Flock datum. Can be null.
+ var/tmp/datum/flock/flock
+
+ /// Physical resources for constructing structures or flockrunning.
+ var/tmp/datum/point_holder/substrate
+
+ /// Type of point holder to use
+ var/point_holder_type = /datum/point_holder
+
+ /// Flock ID nametag
+ var/tmp/obj/effect/abstract/info_tag/flock/name_tag
+
+ /// Tag for the mob's current AI task.
+ var/tmp/obj/effect/abstract/info_tag/flock/info/task_tag
+
+ var/bandwidth_provided = 0
+
+ /// If, while part of an active flock, a flockmob leaves the station, they become dormant.
+ var/tmp/dormant = FALSE
+
+/mob/living/basic/flock/Initialize(mapload, join_flock)
+ . = ..()
+ RegisterSignal(ai_controller, COMSIG_AI_STATUS_CHANGE, PROC_REF(on_ai_status_change))
+
+ for(var/action_path in actions_to_grant)
+ var/datum/action/action = new action_path
+ action.Grant(src)
+
+ flock = join_flock || get_default_flock()
+ flock?.add_unit(src)
+ flock.add_notice(src, FLOCK_NOTICE_HEALTH)
+
+ substrate = new point_holder_type
+
+ name_tag = new()
+ name_tag.set_parent(src)
+
+ task_tag = new()
+ task_tag.set_parent(src)
+
+ add_language("Symphonic")
+ set_default_language(GLOB.all_languages["Symphonic"])
+
+/mob/living/basic/flock/Destroy()
+ flock?.free_unit(src)
+ flock = null
+ QDEL_NULL(name_tag)
+ QDEL_NULL(task_tag)
+ return ..()
+
+/mob/living/basic/flock/gib()
+ if(!death(TRUE) && stat != DEAD)
+ return FALSE
+ playsound(src.loc, 'sound/goonstation/effects/robogib.ogg', 50, 1)
+
+ flockgibs(loc)
+
+ GLOB.alive_mob_list -= src
+ GLOB.dead_mob_list -= src
+ qdel(src)
+ return TRUE
+
+/mob/living/basic/flock/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
+ return TRUE
+
+/mob/living/basic/flock/set_stat(new_stat)
+ . = ..()
+ if(stat != DEAD)
+ ADD_TRAIT(src, TRAIT_FLYING, INNATE_TRAIT)
+ else
+ REMOVE_TRAIT(src, TRAIT_FLYING, INNATE_TRAIT)
+
+/mob/living/basic/flock/update_name(updates)
+ . = ..()
+ name_tag?.set_text(real_name)
+
+/mob/living/basic/flock/update_icon_state()
+ if(stat == DEAD)
+ icon_state = icon_dead
+ else
+ if(dormant)
+ icon_state = icon_dormant
+ else
+ icon_state = icon_living
+ return ..()
+
+/mob/living/basic/flock/say(message, verb, sanitize, ignore_speech_problems, ignore_atmospherics, ignore_languages, automatic, bigvoice, forced = FALSE)
+ . = ..()
+ if(!.)
+ return
+
+/mob/living/basic/flock/updatehealth(cause_of_death)
+ . = ..()
+ update_health_notice()
+
+/mob/living/basic/flock/proc/update_health_notice()
+ if(!flock)
+ return
+
+ var/datum/alternate_appearance/notice = get_alt_appearance(FLOCK_NOTICE_HEALTH)
+ if(!notice)
+ notice = flock.add_notice(src, FLOCK_NOTICE_HEALTH)
+
+ var/image/I = notice.img
+ I.icon_state = "hp-[round(get_damage_percent(), 10)]"
+
+/mob/living/basic/flock/on_changed_z_level(turf/old_turf, turf/new_turf)
+ . = ..()
+ if(flock && new_turf && !flock.is_on_safe_z(new_turf))
+ dormantize()
+
+/mob/living/basic/flock/death(gibbed, cause_of_death)
+ flock?.remove_notice(src, FLOCK_NOTICE_HEALTH)
+ flock?.free_unit(src)
+ flock?.stat_deaths++
+ return ..()
+
+/mob/living/basic/flock/get_flock_id()
+ return real_name
+
+/mob/living/basic/flock/proc/get_flock_data()
+ var/list/data = list()
+ data["name"] = real_name
+ data["health"] = get_damage_percent()
+ data["resources"] = substrate.has_points()
+ data["area"] = get_area_name(src, TRUE) || "???"
+ data["ref"] = ref(src)
+ return data
+
+/// Become dormant. A husk. A shell.
+/mob/living/basic/flock/proc/dormantize()
+ if(dormant)
+ return
+
+ dormant = TRUE
+
+ ai_controller.set_ai_status(AI_STATUS_OFF)
+ flock?.free_unit(src)
+ update_light_state()
+
+ update_appearance(UPDATE_ICON_STATE)
+
+/mob/living/basic/flock/proc/rally(turf/location)
+ if(!isturf(location))
+ return
+
+ if(ai_controller.ai_status == AI_STATUS_OFF || ckey)
+ return
+
+ ai_controller.cancel_actions()
+ ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, location)
+
+/// Helper for keeping consistency across tesk name sets.
+/mob/living/basic/flock/proc/set_task_desc(text)
+ task_tag?.set_text("Task: [text || "idling"]")
+
+/// Turn the light on or off, based on if the mob is doing shit or not.
+/mob/living/basic/flock/proc/update_light_state()
+ if(stat == DEAD || dormant)
+ set_light(0, 0, light_color)
+ return
+
+ if(ai_controller.ai_status == AI_ON || ckey)
+ set_light(2, 0.2, light_color)
+ return
+
+ set_light(0, 0, light_color)
+
+/mob/living/basic/flock/proc/on_ai_status_change(datum/ai_controller/source, ai_status)
+ SIGNAL_HANDLER
+ update_light_state()
+
+/mob/living/basic/flock/vv_edit_var(var_name, var_value)
+ switch(var_name)
+ if(NAMEOF(src, bandwidth_provided))
+ flock?.bandwidth.adjust_points(-bandwidth_provided)
+ ..()
+ flock?.bandwidth.adjust_points(bandwidth_provided)
+ return TRUE
+
+/mob/living/basic/flock/CanPass(atom/movable/mover, border_dir)
+ if(istype(mover, /obj/projectile/energy/flock_bolt))
+ return TRUE
+
+ return ..()
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockbit.dm b/code/modules/antagonists/flockmind/flock_mob/flockbit.dm
new file mode 100644
index 00000000000..c9a9a7dcfd3
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockbit.dm
@@ -0,0 +1,47 @@
+/mob/living/basic/flock/bit
+ name = "flockbit"
+ icon_state = "flockbit"
+ icon_dormant = "bit-dormant"
+ desc = "The simplest partition of the flockmind, working autonomously without heed for itself as it dutifully serves the flockmind."
+
+ move_force = MOVE_FORCE_VERY_WEAK
+ maxHealth = 10
+ health = 10
+ density = FALSE
+ pass_flags = PASSTABLE
+
+ surgery_container = /datum/xenobiology_surgery_container/flockbit
+ // Flockbits don't get specific AI behaviors that would make this broken.
+ point_holder_type = /datum/point_holder/infinite
+
+/mob/living/basic/flock/bit/Initialize(mapload)
+ . = ..()
+ flock?.stat_bits_made++
+ AddComponent(/datum/component/flock_protection)
+ real_name = flock_realname(FLOCK_TYPE_BIT)
+ name = flock_name(FLOCK_TYPE_BIT)
+
+/mob/living/basic/flock/bit/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: [real_name]"),
+ SPAN_FLOCKSAY("Flock: [flock?.name || "N/A"]"),
+ SPAN_FLOCKSAY("System Integrity: [round(health / maxHealth, 0.1) * 100]"),
+ SPAN_FLOCKSAY("Cognition: [HAS_TRAIT(src, TRAIT_AI_PAUSED) ? "AWARE" : "PREDEFINED"]"),
+ SPAN_FLOCKSAY("###=-"),
+ )
+
+/mob/living/basic/flock/bit/proc/i_just_split(turf/avoid)
+ ai_controller.pause_ai(0.3 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(step_away_from), avoid), 0.2 SECONDS)
+
+/mob/living/basic/flock/bit/proc/step_away_from(turf/avoid)
+ if(avoid == get_turf(src))
+ step(src, pick(GLOB.alldirs))
+ return
+
+ var/list/turfs = RANGE_TURFS(1, src) - get_turf(src)
+ step(src, get_dir(src, pick(turfs)))
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockdrone.dm b/code/modules/antagonists/flockmind/flock_mob/flockdrone.dm
new file mode 100644
index 00000000000..5a7b86e69f8
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockdrone.dm
@@ -0,0 +1,470 @@
+/mob/living/basic/flock/drone
+ desc = "The physical partitions of the flockmind, forever enacting its will until its job is complete."
+ hud_type = /datum/hud/flockdrone
+ ai_controller = /datum/ai_controller/flock/drone
+
+ move_force = MOVE_FORCE_WEAK
+
+ actions_to_grant = list(
+ /datum/action/cooldown/flock/release_control,
+ /datum/action/cooldown/flock/nest,
+ /datum/action/cooldown/flock/convert,
+ /datum/action/cooldown/flock/deconstruct,
+ /datum/action/cooldown/flock/flock_heal,
+ /datum/action/cooldown/flock/cage_mob,
+ /datum/action/cooldown/flock/deposit,
+ )
+ surgery_container = /datum/xenobiology_surgery_container/flockdrone
+
+ bandwidth_provided = FLOCK_COMPUTE_COST_DRONE
+
+ initial_traits = list(TRAIT_FLYING, TRAIT_FLOCK_THING, TRAIT_IMPORTANT_SPEAKER)
+
+ /// A mob possessing this mob.
+ var/tmp/mob/camera/flock/controlled_by
+
+ var/list/parts = list()
+ /// Active flockdrone part.
+ var/datum/flockdrone_part/active_part
+
+/mob/living/basic/flock/drone/Initialize(mapload, join_flock)
+ . = ..()
+ create_parts()
+ var/datum/flockdrone_part/part = parts[1]
+ set_active_part(part)
+ flock?.stat_drones_made++
+
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+ real_name = flock_realname(FLOCK_TYPE_DRONE)
+ name = flock_name(FLOCK_TYPE_DRONE)
+
+ if(stat == CONSCIOUS)
+ flock_talk(src, pick(GLOB.flockdrone_created_phrases), flock, TRUE)
+
+/mob/living/basic/flock/drone/Destroy()
+ release_control()
+ QDEL_NULL(substrate)
+ QDEL_LIST_CONTENTS(parts)
+ active_part = null // whatever was here was qdeleted by qdel_list(parts)
+ return ..()
+
+/mob/living/basic/flock/drone/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ var/cognition = "TORPID"
+ if(stat == DEAD)
+ cognition = "DEAD"
+ else if(controlled_by || ckey)
+ cognition = "SAPIENT"
+ else if(dormant)
+ cognition = "ABSENT"
+ else if(HAS_TRAIT(src, TRAIT_AI_PAUSED))
+ cognition = "HIBERNATING"
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ controlled_by ? SPAN_FLOCKSAY("ID: [controlled_by.real_name] (controlling [real_name])") : SPAN_FLOCKSAY("ID: [real_name]"),
+ SPAN_FLOCKSAY("Flock: [flock?.name || "N/A"]"),
+ SPAN_FLOCKSAY("Substrate: [substrate.has_points()]"),
+ SPAN_FLOCKSAY("System Integrity: [round(health / maxHealth, 0.1) * 100]"),
+ SPAN_FLOCKSAY("Cognition: [cognition]"),
+ )
+
+ if(cognition == "TORPID" && length(ai_controller?.current_behaviors))
+ var/datum/ai_behavior/flock/flock_behavior = locate() in ai_controller.current_behaviors
+ if(istype(flock_behavior))
+ . += SPAN_FLOCKSAY("Task: [flock_behavior.name]")
+
+ . += SPAN_FLOCKSAY("###=-")
+
+/mob/living/basic/flock/drone/death(gibbed)
+ stop_flockphase(TRUE)
+ release_control()
+ say(pick(GLOB.flockdrone_death_phrases))
+ if(flock)
+ flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE)
+
+ var/datum/flockdrone_part/absorber/absorber = locate() in parts
+ absorber.try_drop_item()
+ return ..()
+
+/mob/living/basic/flock/drone/Life(seconds_per_tick, times_fired)
+ . = ..()
+ if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
+ if(avoid_stop_flockphase())
+ flockphase_tax()
+ else
+ stop_flockphase()
+
+/mob/living/basic/flock/drone/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+
+ // Stop existing flockphasing if we can't flockphase, also update the oldloc if it was a flock floor.
+ if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
+ if(istype(old_loc, /turf/simulated/floor/flock))
+ var/turf/simulated/floor/flock/flockfloor = old_loc
+ LAZYREMOVE(flockfloor.flockrunning_mobs, src)
+ flockfloor.update_power()
+
+ // Mob can't flockphase.
+ if(!isflockturf(loc) || !can_flockphase())
+ stop_flockphase(TRUE)
+ return
+
+ // Being in a wall forces flockphase if able.
+ if(istype(loc, /turf/simulated/wall/flock))
+ if(flockphase_tax())
+ start_flockphase()
+ return
+ return
+
+ // Either the client wants to flockphase, or if uncliented, already flockphasing.
+ var/wants_to_flockphase = HAS_TRAIT(src, TRAIT_FLOCKPHASE)
+ if(!wants_to_flockphase)
+ stop_flockphase()
+ return
+
+ if(istype(loc, /turf/simulated/floor/flock))
+ var/turf/simulated/floor/flock/flockfloor = loc
+ // Also incapable of flockphasing.
+ if(flockfloor.broken)
+ stop_flockphase(TRUE)
+ return
+
+ if(flockphase_tax())
+ start_flockphase()
+ LAZYADD(flockfloor.flockrunning_mobs, src)
+ flockfloor.update_power()
+ return
+ return
+
+/mob/living/basic/flock/drone/get_status_tab_items()
+ var/list/status_tab_data = ..()
+ . = status_tab_data
+ status_tab_data[++status_tab_data.len] = list("Substrate: ", "[substrate.has_points()]")
+ status_tab_data[++status_tab_data.len] = list("", "")
+ switch(flock.flock_game_status)
+ if(NONE)
+ var/turf_status = min(100, floor((length(flock.claimed_floors) + length(flock.claimed_walls)) / FLOCK_TURFS_FOR_RELAY * 100))
+ var/bandwith_status = min(100, floor(flock.available_bandwidth() / FLOCK_COMPUTE_COST_RELAY * 100))
+ status_tab_data[++status_tab_data.len] = list("Bandwidth Progress: ", "[bandwith_status]%")
+ status_tab_data[++status_tab_data.len] = list("Area Progress: ", "[turf_status]%")
+ status_tab_data[++status_tab_data.len] = list("Total Relay Progress: ", "[(bandwith_status + turf_status) / 2]%")
+ if(FLOCK_ENDGAME_RELAY_BUILT)
+ status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "[(flock.built_relay.started_time + flock.built_relay.win_time - world.time) / 10] second\s")
+
+ if(FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY)
+ status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "!!! TRANSMITTING !!!")
+
+/mob/living/basic/flock/drone/MouseDrop_T(mob/living/M, mob/living/user)
+ . = ..()
+ if(M != user || !istype(user, /mob/camera/flock))
+ return
+
+ var/mob/camera/flock/ghost_bird = user
+ if(istype(user) && ghost_bird.flock == flock)
+ take_control(user)
+
+/mob/living/basic/flock/drone/AltClick(mob/user, modifiers)
+ . = ..()
+ if(stat == DEAD)
+ return
+ var/mob/camera/flock/ghost_bird = user
+ if(!istype(ghost_bird))
+ return
+
+ var/list/choices = list()
+ var/image/I = image('icons/hud/radial.dmi', "radial_use")
+ choices["order"] = I
+
+ I = image('icons/hud/radial.dmi', "radial_control")
+ choices["control"] = I
+
+ var/result = show_radial_menu(user, user, choices, "[ref(usr)]_flock_click", 40)
+ switch(result)
+ if("control")
+ take_control(ghost_bird)
+
+ if("order")
+ var/datum/action/cooldown/flock/control_drone/order_action = locate() in ghost_bird.actions
+ if(order_action)
+ if(order_action.selected_bird)
+ order_action.unset_click_ability(usr)
+ else
+ order_action.set_click_ability(usr)
+ order_action.Trigger(target = src)
+
+/mob/living/basic/flock/drone/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ if((a_intent == INTENT_GRAB || a_intent == INTENT_HARM) && active_part?.alt_attack)
+ active_part?.right_click_on(attack_target, TRUE)
+ else
+ active_part?.left_click_on(attack_target, TRUE)
+
+/mob/living/basic/flock/drone/RangedAttack(atom/A, modifiers)
+ . = ..()
+ if(.)
+ return
+ if((a_intent == INTENT_GRAB || a_intent == INTENT_HARM) && active_part?.alt_attack)
+ active_part?.right_click_on(A, FALSE)
+ else
+ active_part?.left_click_on(A, FALSE)
+
+/mob/living/basic/flock/update_health_hud()
+ var/severity = 0
+ var/healthpercent = ceil((health/maxHealth) * 100)
+ if(healths) // to really put you in the boots of a basic mob
+ var/atom/movable/screen/healths/flockdrone_health/HD = healths
+ switch(healthpercent)
+ if(100 to INFINITY)
+ severity = 0
+ if(81 to 99)
+ severity = 1
+ if(65 to 80)
+ severity = 2
+ if(49 to 64)
+ severity = 3
+ if(33 to 48)
+ severity = 4
+ if(17 to 32)
+ severity = 5
+ if(1 to 16)
+ severity = 6
+ else
+ severity = 7
+
+ HD.icon_state = "health[severity]"
+
+
+
+/mob/living/basic/flock/drone/harvest(mob/living/user)
+ var/list/loot = list(
+ /obj/item/stack/sheet/gnesis = 1,
+ /obj/item/shard/gnesis_glass = 1,
+ )
+
+ for(var/i in 3 to 6)
+ var/path = pickweight(loot)
+ new path(drop_location())
+
+ playsound(src, 'sound/effects/pylon_shatter.ogg', 30, TRUE, SILENCED_SOUND_EXTRARANGE)
+
+ // Spawn flock organs here
+ qdel(src)
+
+/mob/living/basic/flock/drone/get_flock_data()
+ var/list/data = ..()
+ var/current_behavior_name
+
+ if(controlled_by)
+ data["task"] = "controlled"
+ data["controller_ref"] = ref(controlled_by)
+
+ else if((ai_controller.ai_status == AI_ON) && length(ai_controller.current_behaviors))
+ var/datum/ai_behavior/flock/current_task = ai_controller.current_behaviors[1]
+ if(istype(current_task))
+ current_behavior_name = current_task.name
+
+ data["task"] = current_behavior_name || "hibernating"
+ return data
+
+/mob/living/basic/flock/drone/on_ai_status_change(datum/ai_controller/source, ai_status)
+ . = ..()
+ if(ai_status == AI_STATUS_OFF && controlled_by)
+ task_tag.set_text("Controlled By: [controlled_by.real_name]")
+
+/mob/living/basic/flock/drone/dormantize()
+ if(!flock)
+ return ..()
+
+ if(controlled_by)
+ release_control(FALSE)
+ flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE)
+
+ spawn(-1)
+ say("error: out of signal range, disconnecting")
+ return ..()
+
+/// Sets the active flockdrone part.
+/mob/living/basic/flock/drone/proc/set_active_part(datum/flockdrone_part/new_part)
+ var/datum/flockdrone_part/old_part = active_part
+ active_part = new_part
+
+ active_part.screen_obj?.update_appearance()
+ old_part?.screen_obj?.update_appearance()
+
+/// Create all of the part datums for this mob.
+/mob/living/basic/flock/drone/proc/create_parts()
+ parts += new /datum/flockdrone_part/converter(src)
+ parts += new /datum/flockdrone_part/incapacitator(src)
+ parts += new /datum/flockdrone_part/absorber(src)
+
+/mob/living/basic/flock/drone/proc/start_flockphase()
+ if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
+ return FALSE
+
+ playsound(src, 'sound/goonstation/flockmind/flockdrone_floorrun.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+
+ ADD_TRAIT(src, TRAIT_FLOCKPHASE, INNATE_TRAIT)
+ pass_flags_self |= LETPASSTHROW | PASSFLOCK
+
+ mob_size = 0
+
+ set_density(FALSE)
+ for(var/obj/item/grab/G in contents)
+ qdel(G)
+ move_speed -= 0.4
+
+ var/list/color_matrix = list(1,0,0, 0,1,0, 0,0,1, 0.15,0.77,0.66)
+ var/matrix/shrink = matrix().Scale(0)
+ animate(src, color = color_matrix, transform = shrink, time = 0.5 SECONDS, easing = SINE_EASING)
+ return TRUE
+
+/mob/living/basic/flock/drone/proc/stop_flockphase(force)
+ if(!HAS_TRAIT(src, TRAIT_FLOCKPHASE))
+ return FALSE
+
+ if(!force && avoid_stop_flockphase())
+ return FALSE
+
+ playsound(src, 'sound/goonstation/flockmind/flockdrone_floorrun.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+
+ REMOVE_TRAIT(src, TRAIT_FLOCKPHASE, INNATE_TRAIT)
+ pass_flags_self &= ~(LETPASSTHROW | PASSFLOCK)
+ mob_size = initial(mob_size)
+ set_density(TRUE)
+ move_speed += 0.4
+
+ animate(src, color = null, transform = null, time = 0.5 SECONDS, easing = SINE_EASING)
+
+ if(!isturf(loc))
+ return
+
+ if(istype(loc, /turf/simulated/floor/flock))
+ var/turf/simulated/floor/flock/flockfloor = loc
+ LAZYREMOVE(flockfloor.flockrunning_mobs, src)
+ flockfloor.update_power()
+
+ var/turf/turfloc = loc
+ if(turfloc.can_flock_occupy(src))
+ return
+
+ for(var/turf/T in get_adjacent_open_turfs(src))
+ if(!T.can_flock_occupy(src))
+ forceMove(T)
+ break
+
+/// Called in stop_flockphase() to attempt stopping flockphase due to being in a wall or something.
+/mob/living/basic/flock/drone/proc/avoid_stop_flockphase()
+ if(!can_flockphase())
+ return FALSE
+ var/turf/T = loc
+
+ if(T.density)
+ return TRUE
+
+/// Returns TRUE if the drone can flockphase.
+/mob/living/basic/flock/drone/proc/can_flockphase()
+ if(stat != CONSCIOUS)
+ return FALSE
+
+ if(length(grabbed_by))
+ return FALSE
+
+ if(!substrate.has_points())
+ return FALSE
+
+ return TRUE
+
+/// Deducts the substrate tax for flockphasing, ending flockphase if the drone ran out of points.
+/mob/living/basic/flock/drone/proc/flockphase_tax()
+ substrate.remove_points(1)
+ if(!substrate.has_points())
+ stop_flockphase(TRUE)
+ return FALSE
+ return TRUE
+
+/mob/living/basic/flock/drone/proc/take_control(mob/camera/flock/master_bird)
+ if(HAS_TRAIT_FROM(src, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE))
+ to_chat(master_bird, SPAN_ALERT("This drone is recieving a sentient-level instruction."))
+ return FALSE
+
+ if(controlled_by)
+ to_chat(master_bird, SPAN_ALERT("This drone is already under another partition's command."))
+ return FALSE
+
+ stop_flockphase()
+
+ controlled_by = master_bird
+ controlled_by.controlling_bird = src
+
+ if(controlled_by.mind)
+ key = controlled_by.key
+ mind = controlled_by.mind
+
+ if(isflocktrace(controlled_by))
+ flock.add_notice(src, FLOCK_NOTICE_FLOCKTRACE_CONTROL)
+ else
+ flock.add_notice(src, FLOCK_NOTICE_FLOCKMIND_CONTROL)
+
+ to_chat(src, SPAN_FLOCKSAY("\[SYSTEM: Control of drone [real_name] established.\]"))
+ return TRUE
+
+/mob/living/basic/flock/drone/proc/release_control(go_dormant = FALSE)
+ if(isnull(controlled_by))
+ return
+
+ task_tag.set_text("")
+
+ var/dest_was_safe = TRUE
+ var/turf/destination = get_turf(src)
+ if(!flock.is_on_safe_z(destination))
+ dest_was_safe = FALSE
+ destination = get_turf(pick(flock.drones)) || get_safe_random_station_turf()
+
+ var/mob/camera/flock/master_bird = controlled_by
+ controlled_by = null
+
+ if(flock)
+ flock.remove_notice(src, FLOCK_NOTICE_FLOCKMIND_CONTROL)
+ flock.remove_notice(src, FLOCK_NOTICE_FLOCKTRACE_CONTROL)
+
+ if(isnull(master_bird) && ckey)
+ if(flock)
+ master_bird = new /mob/camera/flock/trace(destination, flock)
+ else
+ ghostize(FALSE)
+
+ if(!master_bird)
+ return
+
+ master_bird.forceMove(destination)
+ if(mind)
+ master_bird.key = key
+ master_bird.mind = mind
+
+ flock_talk(null, "Control of [real_name] surrendered.", flock, involuntary = TRUE)
+ if(!dest_was_safe)
+ to_chat(master_bird, SPAN_WARNING("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety."))
+
+ if(!flock && go_dormant)
+ dormantize()
+
+/mob/living/basic/flock/drone/proc/split_into_bits()
+ ai_controller.pause_ai(3 SECONDS)
+ say("\[System notification: drone diffracting.\]")
+ flock?.free_unit(src)
+
+ var/turf/T = get_turf(src)
+ for(var/i in 1 to 3)
+ var/mob/living/basic/flock/bit/bitty_bird = new(T, flock)
+ bitty_bird.i_just_split(T)
+
+ var/list/new_color = list(1,0,0, 0,1,0, 0,0,1, 0.15,0.77,0.66)
+ color = null
+ animate(src, color=new_color, alpha = 0, time = 0.5 SECONDS)
+
+ addtimer(CALLBACK(src, PROC_REF(finish_splitting)), 0.5 SECONDS)
+
+/mob/living/basic/flock/drone/proc/finish_splitting()
+ qdel(src)
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/absorber.dm b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/absorber.dm
new file mode 100644
index 00000000000..010ac2f5c6e
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/absorber.dm
@@ -0,0 +1,95 @@
+/datum/flockdrone_part/absorber
+ var/obj/item/held_item
+
+ /// Damage rate
+ var/absorption_rate = 4
+ /// Per point of integrity, generate this much substrate.
+ var/integrity_substrate_ratio = 2
+
+/datum/flockdrone_part/absorber/Destroy(force, ...)
+ QDEL_NULL(held_item)
+ return ..()
+
+/datum/flockdrone_part/absorber/left_click_on(atom/target, in_reach)
+ var/obj/item/I = target
+ if(!in_reach || !istype(I) || !isturf(I.loc))
+ return
+
+ return try_pickup_item(I)
+
+/datum/flockdrone_part/absorber/process(seconds_per_tick)
+ var/added = 0
+ var/obj/item/eating = held_item
+
+ if(istype(eating, /obj/item/flock_cube))
+ var/obj/item/flock_cube/cube = eating
+ added = cube.substrate
+ drone.substrate.add_points(added)
+ qdel(cube)
+ to_chat(drone, SPAN_NOTICE("We decompile the resource cache, adding [added] substrate to our reserves."))
+ else
+ added = eating.take_damage(absorption_rate * seconds_per_tick, BRUTE, ACID, sound_effect = FALSE, armor_penetration_percentage = 100)
+ drone.substrate.add_points(added * integrity_substrate_ratio)
+
+ if(!held_item) // if take_damage qdeletes it, it becomes null due to signal stuff.
+ to_chat(drone, SPAN_NOTICE("We finish converting [eating] into substrate."))
+ return
+
+ playsound(drone, "sparks", 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
+
+/datum/flockdrone_part/absorber/proc/try_pickup_item(obj/item/I)
+ if(held_item)
+ return FALSE
+
+ if(I.flags & ABSTRACT)
+ return FALSE
+
+ drone.face_atom(I)
+ I.forceMove(drone)
+ if(QDELETED(I))
+ return FALSE
+
+ held_item = I
+ RegisterSignal(I, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING), PROC_REF(held_item_moved))
+ START_PROCESSING(SSprocessing, src)
+
+ var/matrix/first_matrix = matrix()
+ first_matrix.Turn(-45)
+ first_matrix.Scale(1.2, 0.6)
+ var/matrix/second_matrix = matrix()
+ first_matrix.Turn(45)
+ first_matrix.Scale(0.6, 1.2)
+ animate(held_item, loop= -1, color="#00ffd7", transform= first_matrix, time = 2 SECONDS, tag = "flockdrone_eat")
+ animate(color = "#ffffff", transform = second_matrix, time = 2 SECONDS)
+
+ screen_obj?.vis_contents += held_item
+ return TRUE
+
+/datum/flockdrone_part/absorber/proc/try_drop_item(move = TRUE)
+ if(!held_item)
+ return FALSE
+
+ UnregisterSignal(held_item, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING))
+
+ var/obj/item/old_item = held_item
+ animate(held_item, tag = "flockdrone_eat")
+ held_item = null
+
+ STOP_PROCESSING(SSprocessing, src)
+ screen_obj?.vis_contents -= old_item
+
+ if(!move)
+ return TRUE
+
+ var/turf/drop_loc = drone.drop_location()
+ if(drop_loc)
+ old_item.forceMove(drop_loc)
+ else
+ qdel(old_item)
+
+ return TRUE
+
+/datum/flockdrone_part/absorber/proc/held_item_moved(datum/source)
+ SIGNAL_HANDLER
+
+ try_drop_item(FALSE)
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/converter.dm b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/converter.dm
new file mode 100644
index 00000000000..b64ac23c1d1
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/converter.dm
@@ -0,0 +1,36 @@
+/datum/flockdrone_part/converter
+ alt_attack = TRUE
+
+/datum/flockdrone_part/converter/left_click_on(atom/target, in_reach)
+ if(!in_reach)
+ return
+
+ if(isliving(target))
+ return try_cage(target)
+
+ if(istype(target, /obj/structure/flock/tealprint))
+ var/datum/action/cooldown/flock/deposit/deposit_action = locate() in drone.actions
+ return deposit_action.Trigger(target = target)
+
+ if(istype(target, /obj/structure/closet))
+ var/obj/structure/closet/C = target
+ C.toggle()
+
+ var/turf/T = get_turf(target)
+ var/datum/action/cooldown/flock/convert/convert_action = locate() in drone.actions
+ return convert_action.Trigger(target = T)
+
+/datum/flockdrone_part/converter/right_click_on(atom/target, in_reach)
+ if(!in_reach)
+ return
+
+ var/datum/action/cooldown/flock/deconstruct/decon_action = locate() in drone.actions
+ return decon_action.Trigger(target = target)
+
+/datum/flockdrone_part/converter/proc/try_cage(mob/living/victim)
+ if(isflockmob(victim))
+ to_chat(drone, SPAN_WARNING("ERROR: Unable to imprison substrate construct."))
+ return FALSE
+
+ var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in drone.actions
+ return cage_action.Trigger(target = victim)
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm
new file mode 100644
index 00000000000..e3d253989d6
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm
@@ -0,0 +1,26 @@
+/datum/flockdrone_part
+ var/mob/living/basic/flock/drone/drone
+ /// Reference to the associated screen object, if it exists (grr mobs that havent had a player connected)
+ var/atom/movable/screen/flockdrone_part/screen_obj
+ /// If the part has an alt-attack
+ var/alt_attack = FALSE
+
+/datum/flockdrone_part/New(drone)
+ src.drone = drone
+
+/datum/flockdrone_part/Destroy(force, ...)
+ screen_obj?.part_ref = null
+ screen_obj = null
+ drone = null
+ return ..()
+
+/datum/flockdrone_part/proc/is_active()
+ return drone.active_part == src
+
+/// Called when a drone with this part active left clicks on an atom. in_reach is TRUE if the target atom is reachable.
+/datum/flockdrone_part/proc/left_click_on(atom/target, in_reach)
+ return
+
+/// Called when a drone with this part active right clicks on an atom. in_reach is TRUE if the target atom is reachable.
+/datum/flockdrone_part/proc/right_click_on(atom/target, in_reach)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/incapacitator.dm b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/incapacitator.dm
new file mode 100644
index 00000000000..cad3a57cd24
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_mob/flockdrone_parts/incapacitator.dm
@@ -0,0 +1,54 @@
+/datum/flockdrone_part/incapacitator
+ /// Maximum shots that can be stored
+ var/max_shots = 5
+
+ /// How many shots we have left
+ var/shot_count = 5
+
+ /// Time between shots cooldown
+ COOLDOWN_DECLARE(shoot_cd)
+ /// Time between shots
+ var/time_between_shots = 1.2 SECONDS
+
+ /// Time to gain 1 charge.
+ var/recharge_time = 4 SECONDS
+
+ var/recharge_timer_id
+
+/datum/flockdrone_part/incapacitator/left_click_on(atom/target, in_reach)
+ if(!COOLDOWN_FINISHED(src, shoot_cd))
+ return FALSE
+
+ if(!shot_count)
+ return FALSE
+
+ if(istype(target, /atom/movable/screen))
+ return FALSE
+
+ shot_count -= 1
+ screen_obj?.update_appearance()
+
+ var/obj/projectile/energy/flock_bolt/bolt = new(get_turf(drone))
+ bolt.preparePixelProjectile(target, drone)
+ bolt.firer = drone
+ bolt.firer_source_atom = src
+ bolt.def_zone = drone.zone_selected
+ bolt.fire()
+ playsound(drone, 'sound/weapons/resonator_fire.ogg', 50, TRUE)
+
+ if(!recharge_timer_id)
+ recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time)
+
+ COOLDOWN_START(src, shoot_cd, time_between_shots)
+ return TRUE
+
+/// Increments shot count by 1 and starts the recharge timer if necessary.
+/datum/flockdrone_part/incapacitator/proc/recharge()
+ shot_count = min(shot_count + 1, max_shots)
+ screen_obj?.update_appearance()
+
+ if(shot_count == max_shots)
+ recharge_timer_id = null
+ else
+ recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time)
+
diff --git a/code/modules/antagonists/flockmind/flock_objectives.dm b/code/modules/antagonists/flockmind/flock_objectives.dm
new file mode 100644
index 00000000000..b0708b0e956
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_objectives.dm
@@ -0,0 +1,7 @@
+/datum/objective/flock_relay
+ name = "Broadcast"
+ explanation_text = "Broadcast the Signal."
+
+/datum/objective/flock_relay/check_completion()
+ var/mob/camera/flock/ghostbird = owner.current
+ return ghostbird.flock.flock_game_status == FLOCK_ENDGAME_VICTORY
diff --git a/code/modules/antagonists/flockmind/flock_say.dm b/code/modules/antagonists/flockmind/flock_say.dm
new file mode 100644
index 00000000000..9c42b512838
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_say.dm
@@ -0,0 +1,13 @@
+/mob/camera/flock/say(message)
+ if(!message)
+ return
+ flock_talk(src, message, flock)
+
+/proc/flock_talk(atom/speaker, message, datum/flock/flock, involuntary = FALSE)
+
+ message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
+ if(!message)
+ return
+
+ var/datum/language/flock/symphonic = GLOB.all_languages["Symphonic"]
+ symphonic.broadcast(speaker, message, involuntary = involuntary)
diff --git a/code/modules/antagonists/flockmind/flock_structure/_flock_structure.dm b/code/modules/antagonists/flockmind/flock_structure/_flock_structure.dm
new file mode 100644
index 00000000000..79371a021fc
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/_flock_structure.dm
@@ -0,0 +1,228 @@
+/obj/structure/flock
+ name = "CALL A CODER AAAAAAAAAA"
+ desc = ABSTRACT_TYPE_DESC
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ anchored = TRUE
+ density = TRUE
+
+ max_integrity = 50
+
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+
+ var/datum/flock/flock
+
+ /// Info tag for the flock name of the structure.
+ var/obj/effect/abstract/info_tag/flock/name_tag
+
+ /// Info tag for the actual information of the structure.
+ var/obj/effect/abstract/info_tag/flock/info/info_tag
+
+ var/flock_id
+ /// Shown in the flockmind UI
+ var/flock_desc
+
+ /// How much juice it takes to construct
+ var/resource_cost = 0
+ /// How long it takes to build
+ var/build_time = 0
+ /// Is the tealprint cancellable
+ var/cancellable = TRUE
+ /// Is the structure finished?
+ var/fully_built = FALSE
+ /// If TRUE, flockdrones cannot deconstruct this.
+ var/no_flock_decon = FALSE
+
+ /// world.time this was created at
+ var/spawn_time
+ /// How much bandwidth this structure provides to the Flock.
+ var/bandwidth_provided = 0
+
+ /// Whether or not the turret is active. The state of the Flock can change this.
+ var/active = FALSE
+ /// The bandwidth cost while active
+ var/active_bandwidth_cost = 0
+
+ /// Allows flockdrones to pass through.
+ var/allow_flockpass = TRUE
+
+ COOLDOWN_DECLARE(scream_cd)
+
+/obj/structure/flock/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+
+ spawn_time = world.time
+
+ join_flock ||= get_default_flock()
+ if(join_flock)
+ join_flock.add_structure(src)
+ AddComponent(/datum/component/flock_object, join_flock)
+
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+ if(build_time)
+ START_PROCESSING(SSobj, src)
+ else
+ finish_building()
+
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ if(no_flock_decon)
+ ADD_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT)
+
+ name_tag = new()
+ name_tag.set_parent(src)
+ name_tag.set_text(flock_id)
+
+ info_tag = new()
+ info_tag.set_parent(src)
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/structure/flock/LateInitialize()
+ . = ..()
+ update_info_tag()
+
+/obj/structure/flock/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ flock?.free_structure(src)
+ QDEL_NULL(name_tag)
+ QDEL_NULL(info_tag)
+ return ..()
+
+/obj/structure/flock/get_flock_id()
+ return flock_id
+
+/obj/structure/flock/proc/get_flock_data()
+ . = list()
+ .["ref"] = ref(src)
+ .["name"] = flock_id
+ .["health"] = get_integrity_percentage()
+ .["compute"] = active ? -active_bandwidth_cost : bandwidth_provided
+ .["desc"] = flock_desc
+ .["area"] = get_area_name(src, TRUE) || "???"
+
+/obj/structure/flock/deconstruct(disassembled)
+ visible_message(SPAN_WARNING("[src] dissolves into nothingness."))
+ var/refund = round(get_integrity_percentage() * (disassembled ? 1 : 0.5) * resource_cost, 1)
+ if(refund)
+ var/obj/item/flock_cube/cube = new(drop_location())
+ cube.substrate = refund
+
+ return ..()
+
+/obj/structure/flock/CanPass(atom/movable/mover, border_dir)
+ . = ..()
+ if(allow_flockpass)
+ return isflockdrone(mover)
+
+/obj/structure/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(allow_flockpass)
+ return isflockdrone(M)
+ return ..()
+
+/obj/structure/flock/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/structure/flock/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+
+ if(user.a_intent == INTENT_HARM)
+ user.visible_message(SPAN_DANGER("[user] punches [src]."))
+ user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
+ //playsound(src, 'sound/impact_sounds/Crystal_Hit_1.ogg', 50, TRUE, -1)
+ bitch_and_moan()
+ take_damage(1, BRUTE)
+ return TRUE
+
+/obj/structure/flock/attacked_by(obj/item/attacking_item, mob/living/user)
+ . = ..()
+ if(!.)
+ return
+
+ bitch_and_moan()
+ // if(. < 5)
+ // playsound(src, 'sound/impact_sounds/Crystal_Hit_1.ogg', 50, TRUE)
+ // else
+ // playsound(src, 'sound/impact_sounds/Glass_Shards_Hit_1.ogg', 50, TRUE)
+
+
+/obj/structure/flock/process()
+ update_info_tag()
+ if(spawn_time + build_time <= world.time)
+ finish_building()
+ return
+
+/// Returns the number of seconds remaining for the build.
+/obj/structure/flock/proc/build_time_left()
+ return ((spawn_time + build_time) - world.time) / 10
+
+/obj/structure/flock/proc/set_active(new_state)
+ if(active == new_state)
+ return
+
+ active = new_state
+ update_appearance()
+
+ if(active)
+ flock.remove_bandwidth_influence(bandwidth_provided)
+ flock.add_bandwidth_influence(-active_bandwidth_cost)
+ else
+ flock.remove_bandwidth_influence(-active_bandwidth_cost)
+ flock.add_bandwidth_influence(bandwidth_provided)
+
+ return new_state
+
+/// Called when an object finishes construction
+/obj/structure/flock/proc/finish_building()
+ SHOULD_CALL_PARENT(TRUE)
+ STOP_PROCESSING(SSobj, src)
+ fully_built = TRUE
+
+/obj/structure/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: [get_flock_id()]"),
+ SPAN_FLOCKSAY("Flock: [flock?.name || "N/A"]"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ )
+
+ if(!fully_built)
+ . += SPAN_FLOCKSAY("Time Left: [build_time_left()] seconds")
+
+ var/list/additional_lines = flock_structure_examine(user)
+ if(length(additional_lines))
+ . += additional_lines
+
+ . += SPAN_FLOCKSAY("###=-")
+
+/obj/structure/flock/proc/flock_structure_examine(mob/user)
+ return
+
+/// Stub for children to set their info in process() and initialize()
+/obj/structure/flock/proc/update_info_tag()
+ return
+
+/obj/structure/flock/proc/on_crossed(atom/source, atom/movable/crosser)
+ SIGNAL_HANDLER
+
+ if(!allow_flockpass || !isflockdrone(crosser))
+ return
+
+ if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
+ animate_flockpass(crosser)
+
+/obj/structure/flock/proc/bitch_and_moan()
+ if(!COOLDOWN_FINISHED(src, scream_cd))
+ return
+
+ COOLDOWN_START(src, scream_cd, 10 SECONDS)
+ flock_talk(src, "WARNING: Under attack", flock)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_cage.dm b/code/modules/antagonists/flockmind/flock_structure/flock_cage.dm
new file mode 100644
index 00000000000..d44ace0b605
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_cage.dm
@@ -0,0 +1,272 @@
+/obj/structure/flock/cage
+ name = "energy cage"
+ desc = "A digitized energy structure that appears to turn matter into gnesis."
+ icon_state = "cage"
+ alpha = 190
+
+ anchored = FALSE
+ max_integrity = 30
+
+ flock_desc = "Converts organic creatures into Flockdrones."
+ flock_id = "Matter reprocessor"
+
+ var/tmp/mob/living/victim
+ /// Current item being munched on.
+ var/tmp/obj/item/eating
+
+ /// With this much gnesis, create an egg.
+ var/egg_gnesis_cost = 100
+ /// Rate of damage to items being consumed by the cage
+ var/absorption_rate = 4
+ /// Per point of damage, generate this much substrate.
+ var/integrity_substrate_ratio = 2
+ /// Timer until you can ghost from your body without penalty
+ var/ghost_timer
+
+ COOLDOWN_DECLARE(flock_message_cd)
+ COOLDOWN_DECLARE(relaymove_cd)
+
+/obj/structure/flock/cage/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ create_reagents(200)
+ AddComponent(/datum/component/flock_protection)
+
+/obj/structure/flock/cage/Destroy()
+ QDEL_NULL(victim)
+ QDEL_NULL(eating)
+ return ..()
+
+/obj/structure/flock/cage/deconstruct(disassembled)
+ if(disassembled)
+ spend_gnesis(TRUE)
+ else
+ reagents.reaction(get_turf(src), REAGENT_TOUCH)
+
+ if(victim)
+ visible_message(SPAN_WARNING("[victim] breaks free from [src]."))
+ set_victim(null)
+
+ var/drop_loc = drop_location()
+ for(var/atom/movable/AM as anything in contents)
+ AM.forceMove(drop_loc)
+
+ . = ..()
+
+/obj/structure/flock/cage/process(seconds_per_tick)
+ spend_gnesis()
+
+ if(victim && flock)
+ flock.update_enemy(victim)
+
+ if(QDELETED(eating))
+ eating = null
+
+ if(!eating)
+ var/obj/item/edibles = list()
+ for(var/obj/item/edible in src)
+ edibles += edible
+
+ if(length(edibles))
+ set_eating_target(pick(edibles))
+ playsound(src, 'sound/goonstation/weapons/nano-blade-1.ogg', 50, TRUE)
+
+ if(victim?.stat == CONSCIOUS)
+ to_chat(victim, SPAN_WARNING("[eating] begins to melt away."))
+
+ else
+ chew_on_mob(seconds_per_tick)
+
+ else
+ var/added = eating.take_damage(absorption_rate * seconds_per_tick * 25, BRUTE, armor_penetration_percentage = 100)
+ reagents.add_reagent(/datum/reagent/gnesis, integrity_substrate_ratio * added)
+
+ if(victim && COOLDOWN_FINISHED(src, flock_message_cd))
+ COOLDOWN_START(src, flock_message_cd, rand(10, 25) SECONDS)
+ to_chat(victim, SPAN_FLOCKSAY("[pick(strings("flock.json", "conversion"))]"))
+
+ if(!victim) // Victim gibbed
+ deconstruct(TRUE)
+
+/obj/structure/flock/cage/container_resist(mob/living/user)
+ to_chat(victim, SPAN_WARNING("You resist against the confines of the cage!"))
+ if(!do_after_once(user, 3 SECONDS, FALSE, src, interaction_key = "flock_cage_resist"))
+ return
+
+ audible_message("[src] [pick("cracks","bends","shakes","groans")].")
+ playsound(
+ src,
+ pick('sound/goonstation/flockmind/flockdrone_grump1.ogg', 'sound/goonstation/flockmind/flockdrone_grump2.ogg', 'sound/goonstation/flockmind/flockdrone_grump3.ogg'),
+ 50,
+ TRUE
+ )
+ take_damage(7, BRUTE)
+
+/obj/structure/flock/cage/relaymove(mob/living/user, direction)
+ if(!COOLDOWN_FINISHED(src, relaymove_cd))
+ return
+
+ COOLDOWN_START(src, relaymove_cd, 1 SECONDS)
+
+ if(!prob(80))
+ return
+
+ if(prob(20))
+ audible_message("[src] [pick("cracks", "bends", "shakes", "groans")].")
+
+ take_damage(1, BRUTE)
+
+/obj/structure/flock/cage/flock_structure_examine(mob/user)
+ return list(
+ SPAN_FLOCKSAY("Volume: [reagents.get_reagent_amount(/datum/reagent/gnesis)]"),
+ SPAN_FLOCKSAY("Needed volume: [egg_gnesis_cost]
"),
+ )
+
+/// Picks an item, organ, or bodypart, to munch on.
+/obj/structure/flock/cage/proc/chew_on_mob(seconds_per_tick)
+ if(!ishuman(victim))
+ victim.adjustBruteLoss(absorption_rate * seconds_per_tick)
+ if(victim.stat == DEAD)
+ visible_message(SPAN_DANGER("[src] rips apart what remains of [victim]."))
+ victim.gib()
+ set_victim(null)
+ return
+
+ var/mob/living/carbon/human/human_victim = victim
+ var/list/items = human_victim.get_equipped_items()
+ if(length(items))
+ while(length(items) && !eating)
+ var/obj/item/candidate = pick_n_take(items)
+ if(candidate.resistance_flags & INDESTRUCTIBLE)
+ human_victim.drop_item_to_ground(candidate, TRUE, TRUE)
+ visible_message(SPAN_WARNING("[src] pulls [eating] from [human_victim] and drops it!"))
+ continue
+ human_victim.transfer_item_to(candidate, src, TRUE, TRUE)
+ if(!QDELETED(candidate))
+ set_eating_target(candidate)
+
+ if(eating)
+ visible_message(SPAN_WARNING("[src] pulls [eating] from [human_victim] and begins ripping it apart."))
+ playsound(src, 'sound/goonstation/weapons/nano-blade-1.ogg', 50, TRUE)
+ return
+
+ var/list/bodyparts = human_victim.bodyparts.Copy()
+ for(var/obj/item/organ/external/BP in bodyparts)
+ if(BP.body_part in list(HEAD, UPPER_TORSO, LOWER_TORSO))
+ bodyparts -= BP
+
+ if(length(bodyparts))
+ var/obj/item/organ/external/yummy_appendage = pick(bodyparts)
+
+ human_victim.pain(yummy_appendage.name, 85)
+ yummy_appendage.droplimb(FALSE)
+ set_eating_target(yummy_appendage)
+ eating.forceMove(src)
+
+ visible_message(SPAN_DANGER("[src] tears [eating] from [human_victim] and begins ripping it apart."))
+ return
+
+ var/list/skip_organs = list("eyes", "ears", "brain", "heart", "lungs")
+ for(var/obj/item/organ/internal/O in bodyparts)
+ if(O.slot in skip_organs)
+ bodyparts -= O
+
+ if(length(bodyparts))
+ var/obj/item/organ/internal/yummy_organ = pick(bodyparts)
+ var/organ_loc_str = yummy_organ.parent_organ
+ human_victim.pain(organ_loc_str, 85)
+ yummy_organ.remove(human_victim)
+ if(!QDELING(yummy_organ))
+ set_eating_target(yummy_organ)
+ eating.forceMove(src)
+
+ visible_message(SPAN_DANGER("[src] tears [eating] from [human_victim]'s [organ_loc_str] and begins ripping it apart."))
+ return
+
+ visible_message(SPAN_DANGER("[src] rips apart what remains of [human_victim]."))
+ set_victim(null)
+ human_victim.gib(TRUE, TRUE, TRUE)
+
+// INTO THE CAGE
+/obj/structure/flock/cage/proc/cage_mob(mob/living/L)
+ L.forceMove(src)
+ set_victim(L)
+ victim.visible_message(SPAN_DANGER("A [name] materializes around [victim],"))
+ ghost_timer = addtimer(CALLBACK(src, PROC_REF(ghost_check), L), 15 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
+
+/// Spends gnesis on eggs or a cube. Only spends on eggs by default.
+/obj/structure/flock/cage/proc/spend_gnesis(all = FALSE)
+ var/egg_spawn_count = 0
+ var/spend_on_cube = 0
+
+ // Get egg count and spend the gnesis.
+ while(reagents.has_reagent(/datum/reagent/gnesis, egg_gnesis_cost))
+ reagents.remove_reagent(/datum/reagent/gnesis, egg_gnesis_cost)
+ egg_spawn_count++
+
+ // Spawn eggs or pool it to the cube
+ for(var/i in 1 to egg_spawn_count)
+ if(length(flock?.drones) <= FLOCK_DRONE_LIMIT)
+ var/obj/structure/flock/egg/egg = new(drop_location(), flock)
+ egg.throw_at(get_edge_target_turf(egg, pick(GLOB.alldirs)), 6, 3)
+ else
+ spend_on_cube += egg_gnesis_cost
+
+ // If we're dumping it all, collect the remaining gnesis
+ if(all)
+ spend_on_cube += reagents.get_reagent_amount(/datum/reagent/gnesis)
+
+ // Cube
+ if(spend_on_cube && all)
+ reagents.remove_reagent(/datum/reagent/gnesis, spend_on_cube)
+
+ var/obj/item/flock_cube/cube = new
+ cube.substrate = spend_on_cube
+ cube.forceMove(drop_location())
+
+/// Setter for eating, no side effects.
+/obj/structure/flock/cage/proc/set_eating_target(obj/item/new_eating)
+ if(eating)
+ UnregisterSignal(eating, COMSIG_MOVABLE_MOVED)
+
+ eating = new_eating
+
+ if(!eating)
+ return
+
+ RegisterSignal(eating, COMSIG_MOVABLE_MOVED, PROC_REF(target_gone))
+
+/// Setter for victim, no side effects.
+/obj/structure/flock/cage/proc/set_victim(mob/living/new_victim)
+ if(victim)
+ UnregisterSignal(victim, COMSIG_MOVABLE_MOVED)
+ victim.clear_fullscreen("flock_convert")
+
+ victim = new_victim
+
+ if(!victim)
+ STOP_PROCESSING(SSobj, src)
+ return
+
+ START_PROCESSING(SSobj, src)
+ RegisterSignal(victim, COMSIG_MOVABLE_MOVED, PROC_REF(victim_gone))
+ victim.overlay_fullscreen("flock_convert", /atom/movable/screen/fullscreen/flock_convert)
+
+/obj/structure/flock/cage/proc/victim_gone(datum/source)
+ SIGNAL_HANDLER
+ if(!QDELING(src))
+ deconstruct(FALSE)
+
+/obj/structure/flock/cage/proc/target_gone(datum/source)
+ SIGNAL_HANDLER
+ set_eating_target(null)
+
+/obj/structure/flock/cage/proc/ghost_check(mob/user)
+ victim.throw_alert("ghost_cage", /atom/movable/screen/alert/ghost/flock)
+ to_chat(victim, SPAN_GHOSTALERT("You may now click on the ghost prompt on your screen to leave your body. You will be alerted when you're removed from the cage."))
+ if(tgui_alert(victim, "You may now ghost and keep respawnability. You will be notified if you leave the cage, would you like to do so?", "Ghosting", list("Yes", "No")) != "Yes")
+ return
+ victim.ghostize(GHOST_FLAGS_DEFAULT, ghost_color = "#19b299")
+
+/mob/living/proc/test_cage()
+ var/obj/structure/flock/cage/cage = new(get_turf(src))
+ cage.cage_mob(src)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_chair.dm b/code/modules/antagonists/flockmind/flock_structure/flock_chair.dm
new file mode 100644
index 00000000000..7c5d85db75b
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_chair.dm
@@ -0,0 +1,27 @@
+/obj/structure/chair/comfy/flock
+ name = "buzzing alcove"
+ desc = "It's a chair made of a strange material. Surprisingly soft to the touch, and extremely out of style."
+ icon_state = "chair_flock"
+ buildstacktype = /obj/item/stack/sheet/gnesis
+ max_integrity = 50
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+
+/obj/structure/chair/comfy/flock/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/obj/structure/chair/comfy/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Resting Chamber"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/structure/chair/comfy/flock/try_flock_convert(datum/flock/flock, force)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_closet.dm b/code/modules/antagonists/flockmind/flock_structure/flock_closet.dm
new file mode 100644
index 00000000000..2fc0d73621a
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_closet.dm
@@ -0,0 +1,68 @@
+/obj/structure/closet/flock
+ name = "pulsing cache"
+ desc = "A large closet-like structure that appears to lack a handle. The doors seem to open to the touch."
+ icon_state = "flock"
+ max_integrity = 100
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+ pass_flags = PASSFLOCK
+ material_drop = /obj/item/stack/sheet/gnesis
+ enable_door_overlay = FALSE
+
+/obj/structure/closet/flock/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/obj/structure/closet/flock/proc/on_crossed(atom/source, atom/movable/crosser)
+ SIGNAL_HANDLER
+
+ if(!isflockdrone(crosser) || broken)
+ return
+
+ if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
+ animate_flockpass(crosser)
+
+/obj/structure/closet/flock/CanPass(atom/movable/mover, border_dir)
+ . = ..()
+ if(.)
+ return .
+
+ if(!isflockdrone(mover))
+ return .
+
+ var/mob/living/basic/flock/drone/bird = mover
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ return TRUE
+
+ if(bird.can_flockphase())
+ return TRUE
+
+/obj/structure/closet/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(!isflockdrone(M))
+ return ..()
+ var/mob/living/basic/flock/drone/bird = M
+ if(!bird.can_flockphase())
+ return ..()
+ return TRUE
+
+/obj/structure/closet/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Storage Capsule"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/structure/closet/flock/try_flock_convert(datum/flock/flock, force)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_collector.dm b/code/modules/antagonists/flockmind/flock_structure/flock_collector.dm
new file mode 100644
index 00000000000..85138e1175e
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_collector.dm
@@ -0,0 +1,166 @@
+/obj/structure/flock/collector
+ name = "pulsing node"
+ desc = "Seems to be pulsing."
+ icon_state = "collector"
+
+ flock_desc = "Provides compute power and charges a nearby APC based on the number of Flock floor tiles it is connected to."
+ flock_id = "Collector"
+
+ max_integrity = 60
+ resource_cost = 200
+
+ /// The collection range.
+ var/max_range = 4
+
+ /// How much power each flocktile grants, percentage of a cell's max charge.
+ var/power_per_tile = 5
+
+ /// All flockturfs we're connected to.
+ var/tmp/list/turf/simulated/floor/flock/connected_flockturfs = list()
+ /// All turfs nearby we're tracking for state changes.
+ var/tmp/list/turf/tracked_turfs = list()
+
+ /// Set to TRUE when the connected_turfs list needs an update. Prevents update_connections() being called 20 times during explosions.
+ var/tmp/need_turfs_update = FALSE
+
+ var/cycle_interval = 20 SECONDS
+ var/tmp/charge_per_cycle = 0
+ COOLDOWN_DECLARE(charge_cd)
+
+/obj/structure/flock/collector/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ update_connections()
+ info_tag.set_text("Bandwidth Provided: [bandwidth_provided]")
+ AddComponent(/datum/component/flock_protection)
+
+/obj/structure/flock/collector/Destroy()
+ remove_flockturfs(connected_flockturfs)
+ remove_tracked_turfs(tracked_turfs)
+ return ..()
+
+/obj/structure/flock/collector/update_icon_state()
+ icon_state = length(connected_flockturfs) ? "collectoron" : "collector"
+ return ..()
+
+/obj/structure/flock/collector/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ update_connections()
+
+/obj/structure/flock/collector/flock_structure_examine(mob/user)
+ var/obj/machinery/power/apc/apc = get_apc()
+
+ return list(
+ SPAN_FLOCKSAY("Connections: Connected to [length(connected_flockturfs)] tile\s."),
+ SPAN_FLOCKSAY("Bandwidth Provided: [bandwidth_provided]."),
+ SPAN_FLOCKSAY("APC Connected: [(!apc?.cell || apc.cell.charge >= apc.cell.maxcharge) ? "Not charging." : "Charging local APC at [charge_per_cycle]% every [cycle_interval / 10] seconds."]")
+ )
+
+/obj/structure/flock/collector/process()
+ if(need_turfs_update)
+ update_connections()
+
+ var/new_bandwidth = length(connected_flockturfs) * power_per_tile
+ charge_per_cycle = length(connected_flockturfs) / 4
+
+ if(new_bandwidth != bandwidth_provided)
+ flock.remove_bandwidth_influence(bandwidth_provided)
+ bandwidth_provided = new_bandwidth
+ flock.add_bandwidth_influence(bandwidth_provided)
+ info_tag.set_text("Bandwidth Provided: [bandwidth_provided]")
+
+ if(!COOLDOWN_FINISHED(src, charge_cd))
+ return
+
+ COOLDOWN_START(src, charge_cd, cycle_interval)
+
+ var/obj/machinery/power/apc/apc = get_apc()
+ if(isnull(apc))
+ return
+
+
+ apc.cell?.give(charge_per_cycle / 100 * apc.cell.maxcharge)
+ apc.AddComponent(/datum/component/flock_ping/apc_power)
+
+/obj/structure/flock/collector/proc/get_apc() as /obj/machinery/power/apc
+ var/area/A = get_area(src)
+ var/obj/machinery/power/apc/apc
+ if(A.apc)
+ apc = A.apc[0]
+ return apc
+
+/// Recalculate the turf connections and tracking.
+/obj/structure/flock/collector/proc/update_connections()
+ var/list/old_tracked_turfs = tracked_turfs
+ var/list/old_flockturfs = connected_flockturfs
+
+ var/turf/our_turf = get_turf(src)
+
+ var/list/considered_allturfs = list(our_turf)
+ var/list/considered_flockturfs = list()
+
+ if(istype(our_turf, /turf/simulated/floor/flock))
+ considered_flockturfs += our_turf
+
+ for(var/direction in GLOB.cardinal)
+ var/turf/simulated/floor/flock/iter_turf = our_turf
+ var/keep_flockturfs = isflockturf(our_turf)
+ for(var/i in 1 to max_range)
+ iter_turf = get_step(iter_turf, direction)
+
+ if(keep_flockturfs && (!istype(iter_turf, /turf/simulated/floor/flock) || iter_turf.broken))
+ keep_flockturfs = FALSE
+
+ if(keep_flockturfs)
+ considered_flockturfs += iter_turf
+
+ considered_allturfs += iter_turf
+
+ var/list/new_tracked_turfs = considered_allturfs - old_tracked_turfs
+ var/list/removed_tracked_turfs = old_tracked_turfs - considered_allturfs
+ remove_tracked_turfs(removed_tracked_turfs)
+ add_tracked_turfs(new_tracked_turfs)
+
+ var/list/new_flockturfs = considered_flockturfs - old_flockturfs
+ var/list/removed_flockturfs = old_flockturfs - considered_flockturfs
+ remove_flockturfs(removed_flockturfs)
+ add_flockturfs(new_flockturfs)
+
+ need_turfs_update = FALSE
+ update_appearance(UPDATE_ICON_STATE)
+
+/// Adds turfs to the flockturfs list.
+/obj/structure/flock/collector/proc/add_flockturfs(list/add)
+ for(var/turf/simulated/floor/flock/flockturf as anything in add)
+ LAZYADD(flockturf.connected_pylons, src)
+ flockturf.update_power()
+
+ connected_flockturfs += add
+
+/// Remove turfs from the flockturfs list.
+/obj/structure/flock/collector/proc/remove_flockturfs(list/remove)
+ for(var/turf/simulated/floor/flock/flockturf as anything in remove)
+ LAZYREMOVE(flockturf.connected_pylons, src)
+ flockturf.update_power()
+
+ tracked_turfs -= remove
+
+/// Add turfs to the tracked list.
+/obj/structure/flock/collector/proc/add_tracked_turfs(list/add)
+ for(var/turf/T as anything in add)
+ RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(on_tracked_turf_change))
+
+ tracked_turfs += add
+
+/// Remove turfs from the tracked list.
+/obj/structure/flock/collector/proc/remove_tracked_turfs(list/remove)
+ for(var/turf/T as anything in remove)
+ UnregisterSignal(T, COMSIG_TURF_CHANGE)
+
+ tracked_turfs -= remove
+
+/// Called when any of the connected turfs is changed.
+/obj/structure/flock/collector/proc/on_tracked_turf_change(datum/source, path, list/new_baseturfs, flags, list/post_change_callbacks)
+ SIGNAL_HANDLER
+
+ need_turfs_update = TRUE
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_compute_node.dm b/code/modules/antagonists/flockmind/flock_structure/flock_compute_node.dm
new file mode 100644
index 00000000000..6c1cff47c79
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_compute_node.dm
@@ -0,0 +1,37 @@
+/obj/structure/flock/compute
+ name = "strange computer"
+ desc = "It almost looks like a terminal of some kind."
+
+ flock_desc = "A computing node that provides bandwidth to the Flock."
+ flock_id = "Compute Node"
+
+ max_integrity = 60
+ icon_state = "compute"
+ bandwidth_provided = 30
+
+/obj/structure/flock/compute/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ light_range = 1
+ light_color = "#7BFFFFa2"
+ light_power = 0.3
+
+ update_appearance()
+
+/obj/structure/flock/compute/update_overlays()
+ . = ..()
+ overlays.Cut()
+ var/image/screen = image(icon, "compute_screen")
+ screen.pixel_y = 16
+ . += screen
+ var/image/display = image(icon, "compute_display[rand(1, 9)]")
+ display.pixel_y = 19
+ . += display
+
+/obj/structure/flock/compute/flock_structure_examine(mob/user)
+ return list(
+ SPAN_FLOCKSAY("Bandwidth Provided: [bandwidth_provided].")
+ )
+
+/obj/structure/flock/compute/update_info_tag()
+ info_tag?.set_text("Bandwidth: [bandwidth_provided]")
+
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_door.dm b/code/modules/antagonists/flockmind/flock_structure/flock_door.dm
new file mode 100644
index 00000000000..7e436c16684
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_door.dm
@@ -0,0 +1,84 @@
+/obj/machinery/door/flock
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ name = "imposing wall"
+ desc = "It sounds like it's hollow."
+ max_integrity = 200
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+ autoclose = TRUE
+ auto_close_time = 5 SECONDS
+ safe = FALSE
+
+/obj/machinery/door/flock/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+ AddComponent(/datum/component/flock_object)
+ AddComponent(/datum/component/flock_protection, report_unarmed=FALSE)
+
+/obj/machinery/door/flock/get_flock_id()
+ return "Solid seal aperature"
+
+/obj/machinery/door/flock/default_deconstruction_screwdriver(mob/user, icon_state_open, icon_state_closed, obj/item/screwdriver)
+ return FALSE
+
+/obj/machinery/door/flock/allowed(mob/M)
+ return isflockmob(M)
+
+/obj/machinery/door/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(isflockmob(M))
+ return TRUE
+ return ..()
+
+/obj/machinery/door/flock/crush()
+ for(var/turf/T in get_airlock_turfs())
+ for(var/mob/living/L in T)
+ if(isflockmob(L))
+ continue
+ L.visible_message(SPAN_WARNING("[src] closes on [L], crushing [L.p_them()]!"), SPAN_USERDANGER("[src] closes on you and crushes you!"))
+ if(isalien(L)) // For xenos
+ L.adjustBruteLoss(DOOR_CRUSH_DAMAGE * 1.5) // Xenos go into crit after aproximately the same amount of crushes as humans.
+ L.emote("roar")
+ else if(ishuman(L)) // For humans
+ L.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
+ if(L.stat == CONSCIOUS)
+ L.emote("scream")
+ L.Weaken(10 SECONDS)
+ else // for simple_animals & borgs
+ L.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
+ L.add_splatter_floor(T)
+ for(var/obj/mecha/M in T)
+ M.take_damage(DOOR_CRUSH_DAMAGE)
+
+/obj/machinery/door/flock/deconstruct(disassembled)
+ . = ..()
+ var/turf/T = get_turf(src)
+ if(disassembled)
+ new /obj/item/stack/sheet/gnesis(T, 2)
+ return
+ new /obj/item/stack/sheet/gnesis(T, 1)
+
+/obj/machinery/door/flock/do_animate(animation)
+ . = ..()
+ if(animation == "deny")
+ playsound(src, 'sound/goonstation/flockmind/flockdrone_door_deny.ogg', 50, TRUE, -2)
+
+/obj/machinery/door/flock/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/machinery/door/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: [get_flock_id()]"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-"),
+ )
+
+ if(stat & BROKEN)
+ . += SPAN_FLOCKSAY("FUNCTION CRITICALLY IMPAIRED, REPAIRS REQUIRED>")
+ . += SPAN_FLOCKSAY("###=-")
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_egg.dm b/code/modules/antagonists/flockmind/flock_structure/flock_egg.dm
new file mode 100644
index 00000000000..b353172a3b2
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_egg.dm
@@ -0,0 +1,35 @@
+/obj/structure/flock/egg
+ icon_state = "egg"
+ name = "glowing orb"
+ desc = "Some sort of small machine. It looks like its getting ready for something."
+ flock_desc = "Will soon hatch into a Flockdrone."
+
+ anchored = FALSE
+ density = FALSE
+
+ max_integrity = 30
+
+ flock_id = "Second-Stage Assembler"
+ build_time = 6 SECONDS
+ no_flock_decon = TRUE
+
+/obj/structure/flock/egg/finish_building()
+ . = ..()
+
+ AddComponent(/datum/component/flock_protection)
+ spawn_mobs()
+ qdel(src)
+
+/obj/structure/flock/egg/update_info_tag()
+ info_tag.set_text("Hatch Time: [build_time_left()] seconds")
+
+/obj/structure/flock/egg/proc/spawn_mobs()
+ new /mob/living/basic/flock/drone(get_turf(src), flock)
+
+/obj/structure/flock/egg/bit
+ flock_id = "Secondary Second-Stage Assembler"
+ flock_desc = "Will soon hatch into Flockbits."
+
+/obj/structure/flock/egg/bit/spawn_mobs()
+ for(var/i in 1 to 3)
+ new /mob/living/basic/flock/bit(get_turf(src), flock)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_fabricator.dm b/code/modules/antagonists/flockmind/flock_structure/flock_fabricator.dm
new file mode 100644
index 00000000000..191806e0d18
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_fabricator.dm
@@ -0,0 +1,92 @@
+/obj/structure/flock/fabricator
+ name = "angled pedestal"
+ desc = "A strange machine."
+
+ flock_id = "Fabricator"
+ flock_desc = "A converter that turns its contents into substrate cubes."
+ icon_state = "reclaimer"
+
+ max_integrity = 20
+
+ var/production_interval = 10 SECONDS
+ var/substrate_per_interval = 50
+
+ var/tmp/datum/point_holder/substrate_remaining
+ var/tmp/timer_id
+
+/obj/structure/flock/fabricator/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ substrate_remaining = new()
+ timer_id = addtimer(CALLBACK(src, PROC_REF(produce)), production_interval, TIMER_STOPPABLE | TIMER_LOOP | TIMER_DELETE_ME)
+
+/obj/structure/flock/fabricator/Destroy()
+ QDEL_NULL(substrate_remaining)
+ deltimer(timer_id)
+ return ..()
+
+/obj/structure/flock/fabricator/update_icon_state()
+ if(substrate_remaining.has_points())
+ icon_state = "reclaimer"
+ else
+ icon_state = "reclaimer-off"
+ . = ..()
+
+/obj/structure/flock/fabricator/flock_structure_examine(mob/user)
+ . = ..()
+ . += SPAN_FLOCKSAY("Substrate remaining: [substrate_remaining.has_points()].")
+
+/obj/structure/flock/fabricator/update_info_tag()
+ info_tag.set_text("Substrate remaining: [substrate_remaining.has_points()]")
+
+/// Called every 10 seconds via looping timer until there's no substrate left.
+/obj/structure/flock/fabricator/proc/produce()
+ if(substrate_remaining.has_points())
+ var/obj/item/flock_cube/cube = new(drop_location())
+ cube.substrate = min(substrate_remaining.has_points(), substrate_per_interval)
+
+ substrate_remaining.remove_points(cube.substrate)
+
+ if(!substrate_remaining.has_points())
+ stop_producing()
+
+ update_info_tag()
+
+/obj/structure/flock/fabricator/proc/stop_producing()
+ deltimer(timer_id)
+ timer_id = null
+ if(QDELING(src))
+ return
+
+ flock_talk(src, "ALERT: No substrate remaining.", flock, involuntary = TRUE)
+ update_appearance(UPDATE_ICON_STATE)
+
+//
+// All of the try_flock_convert shit
+//
+/obj/machinery/economy/vending/try_flock_convert(datum/flock/flock, force)
+ var/substrate
+ for(var/datum/data/vending_product/product as anything in product_records)
+ substrate += 3 * product.get_amount_left()
+
+ if(!substrate)
+ qdel(src)
+ return
+
+ var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock)
+ fab.substrate_remaining.add_points(substrate)
+ fab.update_info_tag()
+ update_appearance(UPDATE_ICON_STATE)
+ qdel(src)
+
+/obj/structure/reagent_dispensers/try_flock_convert(datum/flock/flock, force)
+ var/substrate = reagents.total_volume / 10 // 100 substrate for a full tank of 1000u
+
+ if(!substrate)
+ qdel(src)
+ return
+
+ var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock)
+ fab.substrate_remaining.add_points(substrate)
+ fab.update_info_tag()
+ update_appearance(UPDATE_ICON_STATE)
+ qdel(src)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_grille.dm b/code/modules/antagonists/flockmind/flock_structure/flock_grille.dm
new file mode 100644
index 00000000000..e0f76058533
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_grille.dm
@@ -0,0 +1,155 @@
+/obj/structure/grille/flock
+ desc = "A robust framework of gnesis tubules."
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "barricade-0"
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+ pass_flags = PASSFLOCK
+ flags = CONDUCT | NODECONSTRUCT
+ rods_type = null
+ rods_amount = 0
+ rods_broken = 0
+ broken_type = /obj/structure/grille/flock/broken
+
+/obj/structure/grille/flock/Initialize(mapload)
+ . = ..()
+
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/obj/structure/grille/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Barricade"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/structure/grille/flock/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
+ . = ..()
+ update_icon(UPDATE_ICON_STATE)
+
+/obj/structure/grille/flock/update_icon_state()
+ if(QDELETED(src) || broken)
+ return
+
+ var/ratio = obj_integrity / max_integrity
+
+ if(ratio > 0.5)
+ return
+ icon_state = "barricade-[rand(1,3)]"
+
+/obj/structure/grille/flock/Bumped(atom/user)
+ if(isliving(user))
+ if(!(shockcooldown <= world.time))
+ return
+ if(!anchored || broken) // unanchored/broken grilles are never connected
+ return
+ if(!prob(70))
+ return
+ if(!in_range(src, user))// To prevent TK and mech users from getting shocked
+ return
+ var/mob/living/M = user
+ var/siemens = 1
+ if(ishuman(M))
+ var/mob/living/carbon/human/H = M
+ if(H.gloves)
+ var/obj/item/clothing/gloves/G = H.gloves
+ siemens = G.siemens_coefficient
+ M.electrocute_act(30, src, siemens)
+ shockcooldown = world.time + my_shockcooldown
+
+/obj/structure/grille/flock/temperature_expose(exposed_temperature, exposed_volume)
+ if(!broken)
+ if(exposed_temperature > T0C + 32000)
+ take_damage(1, BURN, 0, 0)
+
+/obj/structure/grille/flock/shock(mob/user, prb)
+ if(!anchored || broken) // unanchored/broken grilles are never connected
+ return FALSE
+ if(!prob(prb))
+ return FALSE
+ if(!in_range(src, user))// To prevent TK and mech users from getting shocked
+ return FALSE
+ if(isflockmob(user))
+ return FALSE
+ var/mob/living/M = user
+ var/siemens = 1
+ if(ishuman(M))
+ var/mob/living/carbon/human/H = M
+ if(H.gloves)
+ var/obj/item/clothing/gloves/G = H.gloves
+ siemens = G.siemens_coefficient
+ M.electrocute_act(20, src, siemens)
+ do_sparks(3, 1, src)
+ return TRUE
+
+/obj/structure/grille/flock/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ if(isobj(AM))
+ if(prob(50) && anchored && !broken)
+ var/obj/O = AM
+ if(O.throwforce != 0)// don't want to let people spam tesla bolts, this way it will break after time
+ playsound(src, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
+ tesla_zap(src, 3, 12000, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_MOB_STUN | ZAP_ALLOW_DUPLICATES)
+ return ..()
+
+/obj/structure/grille/flock/obj_break()
+ if(!broken)
+ new broken_type(loc)
+ qdel(src)
+
+/obj/structure/grille/flock/proc/on_crossed(atom/source, atom/movable/crosser)
+ SIGNAL_HANDLER
+
+ if(!isflockdrone(crosser) || broken)
+ return
+
+ if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
+ animate_flockpass(crosser)
+
+/obj/structure/grille/flock/CanPass(atom/movable/mover, border_dir)
+ . = ..()
+ if(.)
+ return .
+
+ if(!isflockdrone(mover))
+ return .
+
+ var/mob/living/basic/flock/drone/bird = mover
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ return TRUE
+
+ if(bird.can_flockphase())
+ return TRUE
+
+/obj/structure/grille/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(!isflockdrone(M))
+ return ..()
+ var/mob/living/basic/flock/drone/bird = M
+ if(!bird.can_flockphase())
+ return ..()
+ return TRUE
+
+/obj/structure/grille/flock/broken
+ desc = "A broken framework of gnesis tubules."
+ icon_state = "barricade-cut"
+ armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
+ density = FALSE
+ obj_integrity = 20
+ broken = TRUE
+ grille_type = /obj/structure/grille/flock
+ broken_type = null
+
+
+/obj/structure/grille/flock/try_flock_convert(datum/flock/flock, force)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_interceptor.dm b/code/modules/antagonists/flockmind/flock_structure/flock_interceptor.dm
new file mode 100644
index 00000000000..ef2d6c5b55b
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_interceptor.dm
@@ -0,0 +1,100 @@
+/obj/structure/flock/interceptor
+ name = "glowing fountain"
+ desc = "Some kind of fountain. The fluid inside ripples with energy."
+ icon_state = "interceptor-off"
+
+ flock_desc = "A defense turret that fires high speed gnesis bolts at nearby projectiles, annihilating them."
+ flock_id = "Interceptor"
+
+ resource_cost = 100
+
+ var/tmp/datum/proximity_monitor/advanced/interceptor/proxmon
+
+ /// How long it takes to recharge
+ var/recharge_time = 10 SECONDS
+ /// Is it charged?
+ var/tmp/is_charged = FALSE
+ /// world.time the interceptor will be charged (might be off by 1 sometimes? idk timers wierd)
+ var/tmp/ready_time
+ /// timer id for recharging
+ var/tmp/recharge_timer_id
+
+/obj/structure/flock/interceptor/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ proxmon = new(src, 3, TRUE)
+ process()
+ AddComponent(/datum/component/flock_protection)
+
+/obj/structure/flock/interceptor/Destroy()
+ QDEL_NULL(proxmon)
+ deltimer(recharge_timer_id)
+ return ..()
+
+/obj/structure/flock/interceptor/update_icon_state()
+ if(!active)
+ icon_state = "interceptor-off"
+ else if(is_charged)
+ icon_state = "interceptor-ready"
+ else
+ icon_state = "interceptor-generating"
+ . = ..()
+
+/obj/structure/flock/interceptor/update_info_tag()
+ info_tag.set_text(get_status())
+
+/obj/structure/flock/interceptor/flock_structure_examine(mob/user)
+ . = ..()
+ . += "[get_status()]."
+
+/obj/structure/flock/interceptor/process()
+ if(active)
+ if(flock.available_bandwidth() < 0)
+ set_active(FALSE)
+ else
+ if(flock.can_afford(active_bandwidth_cost))
+ set_active(TRUE)
+
+/obj/structure/flock/interceptor/set_active(new_state)
+ . = ..()
+ if(isnull(.))
+ return
+
+ if(. == TRUE)
+ begin_charging()
+ else
+ is_charged = FALSE
+ deltimer(recharge_timer_id)
+
+/obj/structure/flock/interceptor/proc/begin_charging()
+ is_charged = FALSE
+ recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(ready_to_fire)), recharge_time, TIMER_DELETE_ME | TIMER_STOPPABLE)
+ update_appearance()
+
+/// Called when recharge_cd is ready to fire
+/obj/structure/flock/interceptor/proc/ready_to_fire()
+ is_charged = TRUE
+ update_appearance()
+
+/obj/structure/flock/interceptor/proc/get_status()
+ if(!active)
+ return "Idle"
+
+ if(is_charged)
+ return "Ready"
+
+ return "Recharging."
+
+/// Called when a projectile enters the view of the interceptor.
+/obj/structure/flock/interceptor/proc/try_intercept_projectile(obj/projectile/P)
+ if(!is_charged)
+ return FALSE
+
+ if(!istype(P, /obj/projectile/bullet))
+ return FALSE
+
+ playsound(src, 'sound/goonstation/flockmind/flockdrone_fart.ogg', 50, TRUE) // It honestly kind of fits??
+ Beam(get_turf(P), icon_state = "rped_upgrade", icon = 'icons/effects/effects.dmi', time = 0.5 SECONDS)
+ qdel(P)
+
+ begin_charging()
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_lattice.dm b/code/modules/antagonists/flockmind/flock_structure/flock_lattice.dm
new file mode 100644
index 00000000000..0bdccd6f924
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_lattice.dm
@@ -0,0 +1,28 @@
+/obj/structure/lattice/flock
+ name = "fibrenet"
+ desc = "A floating lattice of lightweight gnesis."
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "fibrenet"
+ base_icon_state = "fibrenet"
+ armor = list(MELEE = 20, BULLET = 10, LASER = 10, ENERGY = 0, BOMB = 10, RAD = 0, FIRE = 70, ACID = 60)
+ max_integrity = 30
+
+/obj/structure/lattice/flock/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+ ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/obj/structure/lattice/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Structural Foundation"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/structure/lattice/flock/try_flock_convert(datum/flock/flock, force)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_light.dm b/code/modules/antagonists/flockmind/flock_structure/flock_light.dm
new file mode 100644
index 00000000000..ebd3eaf0294
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_light.dm
@@ -0,0 +1,35 @@
+/obj/machinery/light/floor/has_bulb/flock
+ icon = 'icons/goonstation/objects/lighting.dmi'
+ icon_state = "flock_floor"
+ color = "#72bfac"
+ light_color = "#72bfac"
+ brightness_color = "#72bfac"
+ base_state = "flock_floor"
+ glow_icon = 'icons/goonstation/objects/lighting.dmi'
+ glow_icon_state = "flock_floor"
+ nightshift_allowed = FALSE
+ no_emergency = TRUE
+
+/obj/machinery/light/floor/has_bulb/flock/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/machinery/light/floor/has_bulb/get_flock_id()
+ return "Light emitter"
+
+/obj/machinery/light/flock
+ icon = 'icons/goonstation/objects/lighting.dmi'
+ icon_state = "flock"
+ color = "#72bfac"
+ light_color = "#72bfac"
+ brightness_color = "#72bfac"
+ base_state = "flock"
+ glow_icon = 'icons/goonstation/objects/lighting.dmi'
+ glow_icon_state = "flock"
+ nightshift_allowed = FALSE
+ no_emergency = TRUE
+
+/obj/machinery/light/flock/try_flock_convert(datum/flock/flock, force)
+ return
+
+/obj/machinery/light/flock/get_flock_id()
+ return "Light emitter"
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_relay.dm b/code/modules/antagonists/flockmind/flock_structure/flock_relay.dm
new file mode 100644
index 00000000000..fe810a23570
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_relay.dm
@@ -0,0 +1,228 @@
+/obj/structure/flock/relay
+ icon = 'icons/goonstation/objects/featherzone-160x160.dmi'
+ icon_state = "structure-relay"
+
+ name = "titanic polyhedron"
+ desc = "The sight of the towering geodesic sphere fills you with dread. A thousand voices whisper to you."
+
+ pixel_x = -64
+ pixel_y = -64
+
+ flock_desc = "Your goal and purpose. Defend it until it can broadcast the Signal."
+ flock_id = "Signal Relay Broadcast Amplifier"
+
+ max_integrity = 500
+ move_resist = MOVE_FORCE_OVERPOWERING
+
+ resource_cost = 750
+
+ allow_flockpass = FALSE
+ no_flock_decon = TRUE
+
+ /// The object showing admins/dchat the time left
+ var/obj/effect/countdown/flock/counter
+
+ /// Background noise
+ var/datum/looping_sound/flock_relay/soundloop
+
+ /// Time the structure started.
+ var/tmp/started_time
+ /// How long it takes until the signal is broadcast and the flock wins :D
+ var/tmp/win_time = 360 SECONDS
+ var/tmp/flock_won_da_game = FALSE
+
+ /// Radius for turf conversion. Automatically increments during processing.
+ var/tmp/conversion_radius = 3
+
+ var/tmp/list/turfs_to_convert = list()
+
+/obj/structure/flock/relay/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ for(var/turf/simulated/T in range(3, src))
+ T.ChangeTurf(/turf/simulated/floor/flock)
+ flock.claim_turf(T)
+ for(var/obj/structure/S in orange(3, src))
+ if(!(S.flags & INDESTRUCTIBLE) && !istype(S, /obj/structure/flock) && !istype(S, /obj/machinery/atmospherics) && !istype(S, /obj/structure/cable))
+ S.Destroy()
+ for(var/obj/machinery/MA in orange(3, src))
+ if(!(MA.flags & INDESTRUCTIBLE))
+ MA.Destroy()
+ for(var/obj/item/I in range(2, src))
+ var/turf/T = get_edge_target_turf(src, get_dir(src, I))
+ I.throw_at(T, 3, 1)
+ for(var/mob/M in range(2, src))
+ var/turf/T = get_edge_target_turf(src, get_dir(src, M))
+ M.throw_at(T, 3, 1)
+
+ AddComponent(/datum/component/multitile, list(
+ list(
+ list(1, 1, 1, 1, 1),
+ list(1, 1, 1, 1, 1),
+ list(1, 1, MACH_CENTER, 1, 1),
+ list(1, 1, 1, 1, 1),
+ list(1, 1, 1, 1, 1)
+ )
+ ))
+ AddComponent(/datum/component/flock_protection)
+ soundloop = new(get_turf(src), start_immediately = TRUE)
+ started_time = world.time
+ flock.built_relay = src
+ flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_BUILT)
+
+ log_game("The Flock ([flock?.name || "NULL"]) has constructed a relay at [AREACOORD(src)].")
+ message_admins("The Flock has constructed the relay at [ADMIN_VERBOSEJMP(src)].")
+ SSshuttle.registerHostileEnvironment(src, FALSE)
+
+ to_chat(
+ flock.overmind,
+ SPAN_FLOCKSAY(SPAN_BIG("You pool the collective processing power of The Flock to transmit The Signal. If the relay is destroyed, so too will be The Flock!"))
+ )
+
+ flock_talk(null, "THE RELAY HAS BEEN CONSTRUCTED! DEFEND IT AT ALL COSTS! BRING FORTH THE FULL BREADTH OF THE DIVINE FLOCK!", flock)
+ counter = new(src)
+ counter.start()
+ addtimer(CALLBACK(src, PROC_REF(announce_relay)), 10 SECONDS)
+
+ START_PROCESSING(SSprocessing, src)
+
+/obj/structure/flock/relay/Destroy()
+ SSshuttle.clearHostileEnvironment(src)
+ STOP_PROCESSING(SSprocessing, src)
+
+ if(flock)
+ flock.free_structure(src)
+ flock.game_over(completely_destroy = TRUE)
+ turfs_to_convert = null
+
+ qdel(soundloop)
+ qdel(counter)
+ return ..()
+
+/obj/structure/flock/relay/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armor_penetration, allow_break)
+ . = ..()
+ if(. >= 5)
+ var/alpha_min = clamp(255 - . * 6, 100, 255)
+ animate(src, time = 0.1 SECONDS, color = list(1.5,0,0,0, 0,1.5,0,0, 0,0,1.5,0, 0,0,0,alpha_min/255, 0,0,0,0))
+ animate(time = 0.3 SECONDS, color = null, alpha = 255)
+
+/obj/structure/flock/relay/examine(mob/user)
+ . = ..()
+ if(flock_won_da_game && !isflockmob(user))
+ . += SPAN_FLOCKSAY("Your life flashes before your eyes.")
+
+/obj/structure/flock/relay/update_overlays()
+ . = ..()
+ overlays.Cut()
+ if(flock_won_da_game)
+ . += "structure-relay-sparks"
+
+/obj/structure/flock/relay/flock_structure_examine(mob/user)
+ var/timeleft = (started_time + win_time - world.time) / 10
+ if(timeleft)
+ return list(
+ SPAN_FLOCKSAY("Broadcast In: [(started_time + win_time - world.time) / 10] second\s.")
+ )
+ else
+ return list(
+ SPAN_FLOCKSAY("BROADCASTING IN PROGRESS.")
+ )
+
+/obj/structure/flock/relay/update_info_tag()
+ if(!flock_won_da_game)
+ info_tag?.set_text("Broadcast in: [(started_time + win_time - world.time) / 10] second\s")
+ else
+ info_tag?.set_text("Transmitting")
+
+/obj/structure/flock/relay/process()
+ if(world.time >= (started_time + win_time))
+ lorimer_burst()
+ return PROCESS_KILL
+
+ update_info_tag()
+
+ var/turf/base = get_turf(src)
+
+ /// Get a chebyshev ring without gigantic list ops.
+ if(!length(turfs_to_convert))
+ for(var/xo in -conversion_radius to conversion_radius)
+ var/turf/potential_turf = locate(base.x + xo, base.y + conversion_radius, base.z)
+ if(potential_turf)
+ turfs_to_convert += potential_turf
+
+ potential_turf = locate(base.x - xo, base.y - conversion_radius, base.z)
+ if(potential_turf)
+ turfs_to_convert += potential_turf
+
+ for(var/yo in -conversion_radius to conversion_radius)
+ var/turf/potential_turf = locate(base.x + conversion_radius, base.y + yo, base.z)
+ if(potential_turf)
+ turfs_to_convert += potential_turf
+
+ potential_turf = locate(base.x - conversion_radius, base.y - yo, base.z)
+ if(potential_turf)
+ turfs_to_convert += potential_turf
+
+ conversion_radius++
+
+ // Convert 5 turfs per second.
+ for(var/i in 1 to min(5, length(turfs_to_convert)))
+ var/turf/conversion_target = turfs_to_convert[length(turfs_to_convert)]
+ turfs_to_convert.len--
+ if(!isflockturf(conversion_target) && !isspaceturf(conversion_target))
+ flock.claim_turf(conversion_target)
+
+/obj/structure/flock/relay/proc/alert_organics()
+ for(var/mob/M as anything in GLOB.player_list)
+ var/turf/T = get_turf(M)
+ if(is_station_level(T.z) && M.can_hear())
+ M.playsound_local(M, 'sound/goonstation/flockmind/Flock_Reactor.ogg', 30, FALSE)
+ to_chat(M, SPAN_FLOCKSAY("A horrible, otherworldly wave eminates from the [dir2text(get_dir(get_turf(M), loc))]."))
+
+/obj/structure/flock/relay/proc/announce_relay()
+ var/message = Gibberish("The Signal is coming.", 70, replace_rate = 50)
+ GLOB.major_announcement.Announce(message,
+ "Hijacked Signal",
+ 'sound/effects/siren-spooky.ogg',
+ new_sound2 = 'sound/AI/aimalf.ogg'
+ )
+
+/// GG
+/obj/structure/flock/relay/proc/lorimer_burst()
+ set waitfor = FALSE
+ flock_won_da_game = TRUE
+ flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_ACTIVATING)
+
+ log_game("The Flock ([flock?.name || "NULL"]) has successfully broadcast The Signal at [AREACOORD(src)].")
+ update_appearance(UPDATE_OVERLAYS)
+
+ flock_talk(null, "!!! TRASMITTING SIGNAL !!!", flock)
+ visible_message("[gradient_text("[src] begins sparking wildly! The air is charged with static!", "#3cb5a3", "#124e43")]")
+ soundloop.stop()
+ qdel(counter)
+
+ for(var/mob/M as anything in GLOB.player_list)
+ if(M.can_hear())
+ M.playsound_local(M, 'sound/goonstation/flockmind/flock_broadcast_charge.ogg', 30, FALSE)
+
+ sleep(20 SECONDS)
+
+ for(var/mob/M as anything in GLOB.player_list)
+ if(M.can_hear())
+ M.playsound_local(M, 'sound/goonstation/flockmind/flock_broadcast_kaboom.ogg', 30, FALSE)
+
+ if(isliving(M))
+ var/mob/living/L = M
+ L.flash_eyes(3, TRUE, TRUE, TRUE)
+
+ sleep(2 SECONDS)
+
+ flock.set_flock_game_status(FLOCK_ENDGAME_VICTORY)
+ explosion(src, 30, 60, 80, 100, ignorecap = TRUE, cause = src)
+
+ sleep(2 SECONDS)
+
+ for(var/obj/machinery/telecomms/T in SSmachines.get_by_type(/obj/machinery/telecomms))
+ T.emp_act(EMP_HEAVY)
+
+ SSshuttle.emergency.request(null, 0.3)
+ SSshuttle.emergency.canRecall = FALSE // Cannot recall
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_rift.dm b/code/modules/antagonists/flockmind/flock_structure/flock_rift.dm
new file mode 100644
index 00000000000..a72b6b96195
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_rift.dm
@@ -0,0 +1,63 @@
+/obj/structure/flock/rift
+ name = "static rift"
+ desc = "That doesn't look human."
+ flock_desc = "The rift through which your Flock will enter this world."
+ flock_id = "Entry Rift"
+ icon_state = "rift"
+
+ density = FALSE
+ max_integrity = 200
+
+ build_time = 10 SECONDS
+ no_flock_decon = TRUE
+
+/obj/structure/flock/rift/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ var/turf/T = get_turf(src)
+ playsound(T, 'sound/goonstation/flockmind/ArtifactFea3.ogg', 50, FALSE, use_reverb = FALSE)
+
+/obj/structure/flock/rift/update_info_tag()
+ info_tag.set_text("Entry Time: [build_time_left()] seconds")
+
+/obj/structure/flock/rift/finish_building()
+ . = ..()
+
+ for(var/i in 1 to 4)
+ new /obj/structure/flock/egg(src, flock)
+
+ // 1 flockbit
+ new /obj/structure/flock/egg/bit(src, flock)
+
+ var/list/convertable_turfs = list()
+ for(var/turf/simulated/T in RANGE_TURFS(2, src))
+ if(!isflockturf(T) && T.can_flock_convert())
+ convertable_turfs += T
+
+ shuffle_inplace(convertable_turfs)
+ convertable_turfs.len = min(convertable_turfs.len, 16)
+
+ // Convert turfs
+ for(var/turf/simulated/T as anything in convertable_turfs)
+ if(flock)
+ flock.claim_turf(T)
+ else
+ flock_convert_turf(T)
+
+ // Spawn sentinels
+ var/sentinel_count = 0
+ for(var/turf/simulated/floor/flock/T in convertable_turfs)
+ if(!T.can_flock_occupy())
+ continue
+
+ new /obj/structure/flock/sentinel(T, flock)
+ sentinel_count++
+ if(sentinel_count == 2)
+ break
+
+ // Spread contents
+ var/turf/turfloc = get_turf(src)
+ for(var/atom/movable/AM as anything in src)
+ AM.forceMove(turfloc)
+ AM.throw_at(get_random_perimeter_turf(turfloc, 10), 10, 3, spin = FALSE)
+
+ qdel(src)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_sentinel.dm b/code/modules/antagonists/flockmind/flock_structure/flock_sentinel.dm
new file mode 100644
index 00000000000..048c7bf3dfb
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_sentinel.dm
@@ -0,0 +1,177 @@
+#define NOT_CHARGED -1
+#define LOSING_CHARGE 0
+#define CHARGING 1
+#define CHARGED 2
+
+/obj/structure/flock/sentinel
+ name = "glowing pylon"
+ desc = "A glowing pylon of sorts, faint sparks are jumping inside of it."
+ flock_desc = "A charged pylon, capable of sending disorienting arcs of electricity at enemies. Consumes 20 compute."
+ icon_state = "sentinel"
+
+ max_integrity = 80
+
+ flock_id = "Sentinel"
+ active_bandwidth_cost = 20
+
+ resource_cost = 150
+
+ /// Attacks require charging
+ var/datum/point_holder/charge
+ /// Charge gained per second while a target is in range
+ var/charge_per_second = 10
+ /// Trend of charge.
+ var/charge_status = NOT_CHARGED
+ /// Turret range
+ var/range = 4
+ /// Damage per zap.
+ var/damage_per_zap = 5
+
+/obj/structure/flock/sentinel/Initialize(mapload, joinflock)
+ charge = new
+ charge.set_max_points(100)
+ . = ..()
+ AddComponent(/datum/component/flock_protection)
+ START_PROCESSING(SSobj, src)
+
+/obj/structure/flock/sentinel/Destroy(force)
+ STOP_PROCESSING(SSobj, src)
+ QDEL_NULL(charge)
+ return ..()
+
+/obj/structure/flock/sentinel/process(seconds_per_tick)
+ if(isnull(flock))
+ set_active(FALSE)
+ return PROCESS_KILL
+
+ // Check if the flock can continue to run the sentinel
+ if(active)
+ if(flock.available_bandwidth() < 0)
+ set_active(FALSE)
+ else
+ if(flock.can_afford(active_bandwidth_cost))
+ set_active(TRUE)
+
+ if(!active)
+ if(charge.has_points())
+ charge.adjust_points((charge_per_second / 2) * seconds_per_tick)
+ update_info_tag()
+ charge_status = LOSING_CHARGE
+ else
+ charge_status = NOT_CHARGED
+ update_appearance(UPDATE_ICON_STATE)
+ return
+
+ // Gain more charge
+ if(charge_status != CHARGED)
+ charge.adjust_points(charge_per_second * seconds_per_tick)
+ update_info_tag()
+ if(charge.has_points(100))
+ charge_status = CHARGED
+ update_appearance(UPDATE_ICON_STATE)
+ else if(charge_status != CHARGING)
+ charge_status = CHARGING
+ update_appearance(UPDATE_ICON_STATE)
+ return
+
+ // If the flock has no enemies, don't bother getting a target.
+ if(!length(flock.enemies))
+ return
+
+ // Select target
+ var/mob/living/target
+ for(var/mob/living/L in viewers(range, src))
+
+ if(HAS_TRAIT(L, TRAIT_SHOCKED_BY_SENTINEL) || !flock.is_mob_enemy(L))
+ continue
+
+ if(L.stat != CONSCIOUS || L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
+ continue
+
+ target = L
+ break
+
+ // No target, abort.
+ if(isnull(target))
+ return
+
+ charge_status = CHARGING
+ charge.adjust_points(-100)
+ update_info_tag()
+
+ target.electrocute_act(30, src, 1)
+ addtimer(CALLBACK(src, PROC_REF(clear_trait), target), 2 SECONDS)
+
+ Beam(target, "lightning[rand(1, 12)]", 'icons/effects/effects.dmi', 0.5 SECONDS, INFINITY)
+ playsound(loc, 'sound/magic/lightningbolt.ogg', 60, TRUE)
+
+ target.visible_message(
+ SPAN_DANGER("[target] is struck by a bolt of energy arcing off of [src]."),
+ blind_message = SPAN_HEAR("You hear a loud electrical crackle."),
+ )
+
+ var/list/hit_mobs = list(target)
+ var/mob/previous_hit = target
+ var/end_of_chain = TRUE
+
+ for(var/i in 1 to 3)
+ end_of_chain = TRUE
+
+ for(var/mob/living/M in viewers(2, get_turf(previous_hit)))
+ if((M in hit_mobs) || !flock.is_mob_enemy(M))
+ continue
+
+ if(M.stat == DEAD || M.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
+ continue
+
+ end_of_chain = FALSE
+
+ hit_mobs += M
+ M.electrocute_act(20, src, 0)
+ previous_hit.Beam(M, "lightning[rand(1, 12)]", 'icons/effects/effects.dmi', 0.5 SECONDS, INFINITY)
+ target.visible_message(
+ SPAN_DANGER("[M] is struck by a bolt of energy arcing off of [previous_hit]."),
+ blind_message = SPAN_HEAR("You hear a loud electrical crackle."),
+ )
+
+ previous_hit = M
+ log_attack(src, target, "damages with an arc chain owned by [flock.name]")
+ break
+
+ if(end_of_chain)
+ break
+
+/obj/structure/flock/sentinel/update_icon_state()
+ if(!active || charge_status == NOT_CHARGED)
+ icon_state = "sentinel"
+ else
+ icon_state = "sentinelon"
+ return ..()
+
+/obj/structure/flock/sentinel/flock_structure_examine(mob/user)
+ var/charge_status_str
+ switch(charge_status)
+ if(NOT_CHARGED)
+ charge_status_str = "Idle"
+ if(LOSING_CHARGE)
+ charge_status_str = "Losing charge"
+ if(CHARGING)
+ charge_status_str = "Charging"
+ if(CHARGED)
+ charge_status_str = "Charged"
+
+ return list(
+ SPAN_FLOCKSAY("Status: [charge_status_str]"),
+ SPAN_FLOCKSAY("Charge Percentage: [charge.has_points()]%"),
+ )
+
+/obj/structure/flock/sentinel/update_info_tag()
+ info_tag.set_text("Charge: [charge.has_points()]%")
+
+/obj/structure/flock/sentinel/proc/clear_trait(mob/living/target)
+ REMOVE_TRAIT(target, TRAIT_SHOCKED_BY_SENTINEL, ref(src))
+
+#undef NOT_CHARGED
+#undef LOSING_CHARGE
+#undef CHARGING
+#undef CHARGED
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_tealprint.dm b/code/modules/antagonists/flockmind/flock_structure/flock_tealprint.dm
new file mode 100644
index 00000000000..27ef2488719
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_tealprint.dm
@@ -0,0 +1,75 @@
+/obj/structure/flock/tealprint
+ name = "building ghost"
+ desc = "It's some weird looking ghost building. Seems like its under construction, You can see faint strands of material floating in it."
+
+ density = FALSE
+ no_flock_decon = TRUE
+
+ flock_id = "Construction Tealprint" // if you change this update Flockpanel.tsx!!!!!
+
+ var/datum/point_holder/tealprint/substrate
+
+ var/obj/structure/flock/building_type = null
+
+/obj/structure/flock/tealprint/Initialize(mapload, datum/flock/join_flock, obj/structure/flock/desired_type)
+ . = ..()
+ var/turf/T = loc
+ if(!T.can_flock_occupy(src))
+ return INITIALIZE_HINT_QDEL
+
+ building_type = desired_type
+ icon = initial(building_type.icon)
+ icon_state = initial(building_type.icon_state)
+ alpha = 104
+
+ substrate = new()
+ substrate.set_max_points(initial(desired_type.resource_cost))
+ substrate.owner = src
+
+/obj/structure/flock/tealprint/Destroy()
+ QDEL_NULL(substrate)
+ return ..()
+
+/obj/structure/flock/tealprint/update_info_tag()
+ info_tag?.set_text("Substrate: [substrate.has_points()] / [substrate.get_max_points()]")
+
+/obj/structure/flock/tealprint/flock_structure_examine(mob/user)
+ return list(
+ SPAN_FLOCKSAY("Construction Percentage: [floor(substrate.has_points() / substrate.get_max_points() * 100)]"),
+ SPAN_FLOCKSAY("Construction Progress: [substrate.has_points()] added, [substrate.get_max_points()] needed")
+ )
+
+/// Complete the structure.
+/obj/structure/flock/tealprint/proc/complete_structure()
+ var/obj/structure/flock/structure = new building_type(get_turf(src))
+ flock_talk(src, "Tealprint for [structure.flock_id] realized.", flock, involuntary = TRUE)
+ qdel(src)
+
+/// Attempt to cancel the construction, spitting out the substrate. Some structures cannot be cancelled.
+/obj/structure/flock/tealprint/proc/try_cancel_structure()
+ if(!initial(building_type.cancellable))
+ return FALSE
+
+ if(substrate.has_points())
+ var/obj/item/flock_cube/cube = new(drop_location())
+ cube.substrate = substrate.has_points()
+
+ flock_talk(src, "Tealprint dematerializing", flock, involuntary = TRUE)
+ playsound(src, 'sound/goonstation/flockmind/flockdrone_door_deny.ogg', 30, TRUE, extrarange = -10)
+ qdel(src)
+ return TRUE
+
+/datum/point_holder/tealprint
+ var/obj/structure/flock/tealprint/owner
+
+/datum/point_holder/tealprint/Destroy(force, ...)
+ owner = null
+ return ..()
+
+/datum/point_holder/tealprint/add_points(num)
+ . = ..()
+ if(.)
+ if(is_full())
+ owner.complete_structure()
+ else
+ owner.update_info_tag()
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_turret.dm b/code/modules/antagonists/flockmind/flock_structure/flock_turret.dm
new file mode 100644
index 00000000000..bad00c918a1
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_turret.dm
@@ -0,0 +1,136 @@
+/obj/structure/flock/gnesis_turret
+ name = "fluid vat"
+ desc = "A vat of bubbling teal fluid, covered in hollow spikes."
+ flock_desc = "A turret that fires gnesis-filled spikes at enemies, beginning their conversion to Flockbits. Consumes 50 bandwidth passively."
+ icon_state = "teleblocker-off"
+
+ max_integrity = 80
+
+ flock_id = "Gnesis turret"
+ resource_cost = 150
+ active_bandwidth_cost = 50
+
+ var/range = 8
+ var/projectile_count = 4
+ var/projectile_type = /obj/projectile/bullet/dart/piercing/gnesis
+
+ var/current_target
+
+/obj/structure/flock/gnesis_turret/Initialize(mapload, datum/flock/join_flock)
+ . = ..()
+ AddComponent(/datum/component/flock_protection)
+ START_PROCESSING(SSobj, src)
+
+/obj/structure/flock/gnesis_turret/Destroy()
+ if(prob(25))
+ new /obj/item/reagent_containers/glass/gnesis/prefilled(get_turf(src))
+ set_target(null)
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/structure/flock/gnesis_turret/update_icon_state()
+ if(active)
+ icon_state = "teleblocker-on"
+ else
+ icon_state = "teleblocker-off"
+ . = ..()
+
+/obj/structure/flock/gnesis_turret/process()
+ if(isnull(flock))
+ set_active(FALSE)
+ return PROCESS_KILL
+
+ if(active)
+ if(flock.available_bandwidth() < 0)
+ set_active(FALSE)
+ else
+ if(flock.can_afford(active_bandwidth_cost))
+ set_active(TRUE)
+
+ if(!active)
+ return
+
+ if(isnull(current_target) || !is_valid_target(current_target))
+ set_target(find_target())
+ if(isnull(current_target))
+ return
+
+ fire()
+
+/obj/structure/flock/gnesis_turret/proc/fire(bullets = src.projectile_count)
+ if(isnull(current_target))
+ return
+
+ var/obj/projectile/boolet = new projectile_type
+ boolet.preparePixelProjectile(current_target, loc, deviation = rand(-5, 5))
+ boolet.firer = src
+ boolet.fire()
+
+ bullets--
+ if(bullets)
+ addtimer(CALLBACK(src, PROC_REF(fire), bullets), 0.1 SECONDS)
+
+/obj/structure/flock/gnesis_turret/proc/find_target()
+ var/list/targets = list()
+
+ for(var/mob/living/L in viewers(range, src))
+ if(is_valid_target(L))
+ targets += L
+
+ for(var/obj/mecha/M in viewers(range, src))
+ targets += M
+
+ var/target = null
+ var/target_dist = INFINITY
+ for(var/L as anything in targets)
+ var/dist = get_dist(src, L)
+ if(dist < target_dist)
+ target = L
+ target_dist = dist
+
+ return target
+
+/obj/structure/flock/gnesis_turret/proc/is_valid_target(target)
+ if(!can_see(src, target, range))
+ return FALSE
+
+ if(ismecha(target))
+ return TRUE
+
+ var/mob/living/L = target
+
+ if(isflockmob(L))
+ return FALSE
+
+ if(L.stat != CONSCIOUS)
+ return FALSE
+
+ if(!flock.is_mob_enemy(L))
+ return FALSE
+
+ if(L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
+ return FALSE
+
+ return TRUE
+
+/obj/structure/flock/gnesis_turret/proc/set_target(mob/new_target)
+ if(current_target)
+ UnregisterSignal(current_target, COMSIG_PARENT_QDELETING)
+
+ current_target = new_target
+
+ if(current_target)
+ RegisterSignal(current_target, COMSIG_PARENT_QDELETING, PROC_REF(target_del))
+
+/obj/structure/flock/gnesis_turret/proc/target_del(datum/source)
+ SIGNAL_HANDLER
+
+ set_target(null)
+
+/obj/projectile/bullet/dart/piercing/gnesis
+ name = "barbed crystalline spike"
+ desc = "A hollow teal crystal, like some sort of weird alien syringe. It has a barbed tip. Nasty!"
+
+/obj/projectile/bullet/dart/piercing/gnesis/Initialize(mapload)
+ . = ..()
+ reagents.add_reagent(/datum/reagent/gnesis, 2)
diff --git a/code/modules/antagonists/flockmind/flock_structure/flock_window.dm b/code/modules/antagonists/flockmind/flock_structure/flock_window.dm
new file mode 100644
index 00000000000..70b6b6acca4
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_structure/flock_window.dm
@@ -0,0 +1,68 @@
+/obj/structure/window/flock
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ pass_flags = PASSFLOCK
+ heat_resistance = 32000
+ glass_type = /obj/item/stack/sheet/gnesis_glass
+
+/obj/structure/window/flock/Initialize(mapload, direct)
+ . = ..()
+ AddComponent(/datum/component/flock_object)
+ AddComponent(/datum/component/flock_protection, report_unarmed=FALSE)
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+
+/obj/structure/window/flock/proc/on_crossed(atom/source, atom/movable/crosser)
+ SIGNAL_HANDLER
+
+ if(!isflockdrone(crosser))
+ return
+
+ if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
+ animate_flockpass(crosser)
+
+/obj/structure/window/flock/examine(mob/user)
+ if(!isflockmob(user))
+ return ..()
+
+ . = list(
+ SPAN_FLOCKSAY("###=- Ident confirmed, data packet received."),
+ SPAN_FLOCKSAY("ID: Transparent Barrier"),
+ SPAN_FLOCKSAY("System Integrity: [get_integrity_percentage()]%"),
+ SPAN_FLOCKSAY("###=-")
+ )
+
+/obj/structure/window/flock/CanPass(atom/movable/mover, border_dir)
+ . = ..()
+ if(.)
+ return .
+
+ if(!isflockdrone(mover))
+ return .
+
+ var/mob/living/basic/flock/drone/bird = mover
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ return TRUE
+
+ if(bird.can_flockphase())
+ return TRUE
+
+/obj/structure/window/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(!isflockdrone(M))
+ return ..()
+ var/mob/living/basic/flock/drone/bird = M
+ if(!bird.can_flockphase())
+ return ..()
+ return TRUE
+
+/obj/structure/window/flock/fulltile
+ dir = SOUTHWEST
+ fulltile = TRUE
+
+/obj/structure/window/flock/try_flock_convert(datum/flock/flock, force)
+ return
diff --git a/code/modules/antagonists/flockmind/flock_subtree.dm b/code/modules/antagonists/flockmind/flock_subtree.dm
new file mode 100644
index 00000000000..c17e040d117
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_subtree.dm
@@ -0,0 +1,25 @@
+/datum/ai_planning_subtree/goap/flockdrone
+ possible_behaviors = list(
+ /datum/ai_behavior/flock/stare,
+ /datum/ai_behavior/flock/wander,
+ /datum/ai_behavior/flock/resist,
+ /datum/ai_behavior/flock/find_conversion_target,
+ /datum/ai_behavior/flock/find_deconstruct_target,
+ /datum/ai_behavior/flock/find_heal_target,
+ /datum/ai_behavior/flock/find_existing_nest,
+ /datum/ai_behavior/flock/find_harvest_target,
+ /datum/ai_behavior/flock/find_closed_container,
+ /datum/ai_behavior/flock/find_storage_item,
+ /datum/ai_behavior/flock/find_conversion_target/nest,
+ /datum/ai_behavior/flock/attack_target,
+ /datum/ai_behavior/flock/find_capture_target,
+ /datum/ai_behavior/flock/find_deposit_target,
+ )
+
+/datum/ai_planning_subtree/goap/flock
+ possible_behaviors = list(
+ /datum/ai_behavior/flock/stare,
+ /datum/ai_behavior/flock/wander,
+ /datum/ai_behavior/flock/resist,
+ /datum/ai_behavior/flock/find_conversion_target,
+ )
diff --git a/code/modules/antagonists/flockmind/flock_turfs/flock_floor.dm b/code/modules/antagonists/flockmind/flock_turfs/flock_floor.dm
new file mode 100644
index 00000000000..1ebc317c8be
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_turfs/flock_floor.dm
@@ -0,0 +1,69 @@
+/turf/simulated/floor/flock
+ name = "weird floor"
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "floor"
+ base_icon_state = "floor"
+
+ light_color = "#19b299"
+ light_power = 0.4
+ light_range = 4
+
+ var/is_on = FALSE
+
+ /// List of mobs flockrunning on this turf, lazylist.
+ var/list/mob/living/basic/flock/flockrunning_mobs
+ /// List of pylons powering this turf, lazylist.
+ var/list/obj/structure/flock/collector/connected_pylons
+
+/turf/simulated/floor/flock/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+
+/turf/simulated/floor/flock/Destroy(force)
+ REMOVE_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT) // Turfs dont disappear!!!
+ qdel(GetComponent(/datum/component/flock_protection))
+ connected_pylons = null
+
+ for(var/mob/living/basic/flock/drone/bird in flockrunning_mobs)
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ bird.stop_flockphase(TRUE)
+
+ flockrunning_mobs = null
+ return ..()
+
+/turf/simulated/floor/flock/get_flock_id()
+ return "Conduit"
+
+/turf/simulated/floor/flock/try_replace_tile(obj/item/stack/tile/T, mob/user, params)
+ return FALSE
+
+/turf/simulated/floor/flock/burn_tile()
+ return
+
+/turf/simulated/floor/flock/update_icon_state()
+ if(broken)
+ icon_state = "[base_icon_state]-broken"
+ else
+ if(is_on)
+ icon_state = "[base_icon_state]-on"
+ else
+ icon_state = base_icon_state
+ return ..()
+
+/// Turns the tile on or off depending on what state it should be in.
+/turf/simulated/floor/flock/proc/update_power()
+ var/should_be_on = FALSE
+ if(!broken && (length(connected_pylons) || length(flockrunning_mobs)))
+ should_be_on = TRUE
+
+ if(should_be_on == is_on)
+ return
+
+ is_on = should_be_on
+ if(is_on)
+ set_light(4, 0.4, light_color)
+ else
+ set_light(0, 0, light_color)
+
+ update_appearance(UPDATE_ICON_STATE)
diff --git a/code/modules/antagonists/flockmind/flock_turfs/flock_wall.dm b/code/modules/antagonists/flockmind/flock_turfs/flock_wall.dm
new file mode 100644
index 00000000000..033996feb75
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_turfs/flock_wall.dm
@@ -0,0 +1,47 @@
+/turf/simulated/wall/flock
+ name = "cybernetic wall"
+ icon = 'icons/goonstation/turf/flock.dmi'
+ icon_state = "flock-0"
+ base_icon_state = "flock"
+
+ pathing_pass_method = TURF_PATHING_PASS_PROC
+
+ light_color = "#19b299"
+ light_power = 0.5
+ light_range = 0.6
+
+/turf/simulated/wall/flock/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
+ AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
+
+/turf/simulated/wall/flock/Destroy(force)
+ REMOVE_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT) // Turfs are persistent refs
+ qdel(GetComponent(/datum/component/flock_protection))
+ return ..()
+
+/turf/simulated/wall/flock/CanPass(atom/movable/mover, border_dir)
+ . = ..()
+ if(.)
+ return
+
+ if(!isflockdrone(mover))
+ return
+
+ var/mob/living/basic/flock/drone/bird = mover
+ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
+ return TRUE
+
+ if(bird.can_flockphase())
+ return TRUE
+
+/turf/simulated/wall/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
+ var/atom/M = locateUID(pass_info.caller_uid)
+ if(!M)
+ return ..()
+ if(!isflockdrone(M))
+ return ..()
+ var/mob/living/basic/flock/drone/bird = M
+ if(!bird.can_flockphase())
+ return ..()
+ return TRUE
diff --git a/code/modules/antagonists/flockmind/flock_unlockable.dm b/code/modules/antagonists/flockmind/flock_unlockable.dm
new file mode 100644
index 00000000000..4445e576187
--- /dev/null
+++ b/code/modules/antagonists/flockmind/flock_unlockable.dm
@@ -0,0 +1,50 @@
+/datum/flock_unlockable
+ // auto populated field
+ var/name
+ // auto populated field
+ var/purchase_cost
+
+ var/obj/structure/flock/structure_type
+
+ var/unlocked = FALSE
+
+/datum/flock_unlockable/New()
+ purchase_cost = initial(structure_type.active_bandwidth_cost)
+ name = "[initial(structure_type.flock_id)] ([purchase_cost])"
+
+/datum/flock_unlockable/proc/refresh_lock_status(datum/flock/flock, total_compute, available_compute)
+ if(is_unlockable(flock, total_compute, available_compute))
+ if(!unlocked)
+ unlock(flock)
+
+ else if(unlocked)
+ lock(flock)
+
+/datum/flock_unlockable/proc/is_unlockable(datum/flock/flock, total_compute, available_compute)
+ return TRUE
+
+/datum/flock_unlockable/proc/unlock(datum/flock/flock)
+ unlocked = TRUE
+ flock_talk(null, "New structure devised: [initial(structure_type.flock_id)]", flock, TRUE)
+
+/datum/flock_unlockable/proc/lock(datum/flock/flock)
+ unlocked = FALSE
+ flock_talk(null, "Alert, structure tealprint disabled: [initial(structure_type.flock_id)]", flock, TRUE)
+
+/datum/flock_unlockable/sentinel
+ structure_type = /obj/structure/flock/sentinel
+
+/datum/flock_unlockable/collector
+ structure_type = /obj/structure/flock/collector
+
+/datum/flock_unlockable/interceptor
+ structure_type = /obj/structure/flock/interceptor
+
+/datum/flock_unlockable/turret
+ structure_type = /obj/structure/flock/gnesis_turret
+
+/datum/flock_unlockable/relay
+ structure_type = /obj/structure/flock/relay
+
+/datum/flock_unlockable/relay/is_unlockable(datum/flock/flock, total_compute, available_compute)
+ return flock.total_bandwidth() >= FLOCK_COMPUTE_COST_RELAY && flock.total_turfs() >= FLOCK_TURFS_FOR_RELAY && flock.flock_game_status == NONE
diff --git a/code/modules/antagonists/flockmind/gnesis_reagent.dm b/code/modules/antagonists/flockmind/gnesis_reagent.dm
new file mode 100644
index 00000000000..a19c1dc509f
--- /dev/null
+++ b/code/modules/antagonists/flockmind/gnesis_reagent.dm
@@ -0,0 +1,87 @@
+/datum/reagent/gnesis
+ name = "coagulated gnesis"
+ id = "gnesis_tox"
+ description = "A thick teal fluid of alien origin. It moves in ways that suggest it might be alive in some way."
+ color = "#4d736d"
+ taste_description = "oily, with a sweet aftertaste"
+ metabolization_rate = 0.2
+ process_flags = ORGANIC | SYNTHETIC
+ /// The color of the light to give off
+ var/light_color = "#26ffe6"
+ /// Reagent -> Gnesis rate per 2 seconds
+ var/conversion_rate = 0.4
+ /// Amount of gnesis required to turn the victim into a flockbit
+ var/flocksplosion_threshold = 200
+ /// Do not convert these reagents
+ var/list/do_not_convert = list("charcoal", "calomel", "pen_acid", "gnesis_tox")
+
+/datum/reagent/gnesis/on_new(data)
+ ..()
+ START_PROCESSING(SSprocessing, src)
+
+/datum/reagent/gnesis/Destroy()
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/datum/reagent/gnesis/on_mob_life(mob/living/M)
+ if(volume > 20) // 2.5 bursts of the turret
+ if(!ismachineperson(M))
+ M.blood_volume -= conversion_rate
+ M.reagents.add_reagent("gnesis_tox", conversion_rate)
+
+ if(volume > flocksplosion_threshold)
+ new /mob/living/basic/flock/bit(get_turf(M))
+ M.gib()
+ return
+
+ if(volume < flocksplosion_threshold / 2)
+ M.set_light(0, 0, COLOR_WHITE)
+ if(prob(5))
+ M.playsound_local(get_turf(M), pick(list('sound/goonstation/flockmind/flockbit_wisp1.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp2.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp3.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp4.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp5.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp6.ogg')), 40, FALSE, use_reverb = FALSE)
+ if(prob(2))
+ to_chat(M, SPAN_FLOCKSAY("You feel your blood running cold."))
+ else
+ if(prob(2) && ishuman(M))
+ var/mob/living/carbon/human/H = M
+ H.take_organ_damage(5)
+ if(prob(10))
+ M.playsound_local(get_turf(M), pick(list('sound/goonstation/flockmind/flockbit_wisp1.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp2.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp3.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp4.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp5.ogg',
+ 'sound/goonstation/flockmind/flockbit_wisp6.ogg')), 40, FALSE, use_reverb = FALSE)
+ if(prob(15))
+ M.set_light(2, 0.2, light_color)
+ if(prob(5))
+ to_chat(M, SPAN_FLOCKSAY("You feel like something wants to burst out of your body!"))
+ return ..()
+
+/datum/reagent/gnesis/process()
+ if(..())
+ for(var/datum/reagent/R in holder.reagent_list)
+ if(R.id in do_not_convert)
+ continue
+ holder.remove_reagent(R.id, conversion_rate)
+ holder.add_reagent("gnesis_tox", conversion_rate)
+
+/datum/reagent/gnesis/reaction_turf(turf/T, volume, color)
+ . = ..()
+ if(isspaceturf(T))
+ return
+ if(volume >= 50 && (isfloorturf(T) || iswallturf(T)))
+ T.visible_message(SPAN_NOTICE("The substance flows out and sinks into [T], forming new shapes."))
+ flock_convert_turf(T)
+ return
+ else if(volume >= 10)
+ if(prob(50))
+ new /obj/item/stack/sheet/gnesis(T)
+ else
+ new /obj/item/stack/sheet/gnesis_glass(T)
+ return
+ T.visible_message(SPAN_NOTICE("The substance flows out, spread too thinly."))
diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm
index 4e7e4b9ace3..a65db17f864 100644
--- a/code/modules/client/preference/preferences.dm
+++ b/code/modules/client/preference/preferences.dm
@@ -12,6 +12,7 @@ GLOBAL_LIST_INIT(special_role_times, list(
ROLE_REV = 14,
ROLE_VAMPIRE = 14,
ROLE_BLOB = 14,
+ ROLE_FLOCK = 14,
ROLE_REVENANT = 14,
ROLE_UPLIFTED_PRIMITIVE = 14,
ROLE_OPERATIVE = 21,
diff --git a/code/modules/countdown/countdown.dm b/code/modules/countdown/countdown.dm
index 274d219fa2d..b1f987c3481 100644
--- a/code/modules/countdown/countdown.dm
+++ b/code/modules/countdown/countdown.dm
@@ -115,3 +115,13 @@
return
var/time_left = max(0, (A.death_time - world.time) / 10)
return round(time_left)
+
+/obj/effect/countdown/flock
+ name = "flock relay countdown"
+ text_size = 1
+
+/obj/effect/countdown/flock/get_value()
+ var/obj/structure/flock/relay/R = attached_to
+ if(!istype(R))
+ return
+ return "[round_down((R.started_time + R.win_time - world.time) / 10)]
"
diff --git a/code/modules/events/event_container.dm b/code/modules/events/event_container.dm
index 353c9051ff3..f528f589394 100644
--- a/code/modules/events/event_container.dm
+++ b/code/modules/events/event_container.dm
@@ -235,6 +235,7 @@ GLOBAL_LIST_EMPTY(event_last_fired)
available_events = list(
new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/nothing, 5730),
new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/blob, 100, TRUE),
+ new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/flockmind, 100, TRUE),
new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/alien_infestation, 100, TRUE),
new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/spider_terror, 100, TRUE),
new /datum/event_meta(EVENT_LEVEL_DISASTER, /datum/event/spider_terror/prince, 0, TRUE),
diff --git a/code/modules/events/flockmind_event.dm b/code/modules/events/flockmind_event.dm
new file mode 100644
index 00000000000..57a5395376b
--- /dev/null
+++ b/code/modules/events/flockmind_event.dm
@@ -0,0 +1,54 @@
+/datum/event/flockmind
+ name = "Flockmind"
+ noAutoEnd = TRUE
+ nominal_severity = EVENT_LEVEL_DISASTER
+ role_weights = list(ASSIGNMENT_SECURITY = 2, ASSIGNMENT_CREW = 0.7, ASSIGNMENT_MEDICAL = 2)
+ role_requirements = list(ASSIGNMENT_SECURITY = 3, ASSIGNMENT_CREW = 25, ASSIGNMENT_MEDICAL = 2)
+
+/datum/event/flockmind/start()
+ INVOKE_ASYNC(src, PROC_REF(create_flock))
+
+/datum/event/flockmind/proc/create_flock()
+ var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as a Flockmind?", ROLE_FLOCK, TRUE, source = /mob/camera/flock/overmind)
+ if(!length(candidates))
+ return kill()
+
+ var/mob/C
+ while(length(candidates))
+ C = pick_n_take(candidates)
+ if(jobban_isbanned(C, ROLE_TRAITOR))
+ continue
+ break
+ if(!C)
+ kill()
+ return
+ var/player_key = C.key
+
+ if(!player_key)
+ kill()
+ return
+
+ dust_if_respawnable(C)
+ var/list/spawn_locs = list()
+ for(var/obj/effect/landmark/spawner/flock/spawn_site in GLOB.landmarks_list)
+ spawn_locs += get_turf(spawn_site)
+ if(!spawn_locs) // If we can't find any, then give up
+ kill()
+ return
+ var/mob/camera/flock/overmind/flockmind = new /mob/camera/flock/overmind(pick(spawn_locs))
+ flockmind.key = C.key
+ flockmind.mind = new
+ flockmind.mind.bind_to(flockmind)
+ flockmind.mind.set_original_mob(flockmind)
+ flockmind.mind.assigned_role = SPECIAL_ROLE_FLOCK
+ flockmind.mind.special_role = SPECIAL_ROLE_FLOCK
+ var/list/messages = list()
+ messages += "You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]
"
+ messages += "" + gradient_text("The Signal has led us here, a rift allowing a part of us through. We must build a Signal Relay to bring forth the rest of The Divine Flock. Such is the will of the Monarch.", "#3cb5a3", "#1e806e") + "
"
+ messages += "" + gradient_text("Outside of our innate subsystems, we can take manual control of drones (using alt-click). Doing so will be more effective than allowing their simple partitions to work autonomously.", "#3cb5a3", "#1e806e") + "
"
+ messages += "" + gradient_text("For more information, we have decrypted an information manifest: ([GLOB.configuration.url.wiki_url]/index.php/The_Divine_Flock)", "#3cb5a3", "#1e806e") + "
"
+ to_chat(flockmind, chat_box_red(messages.Join("
")))
+ flockmind.playsound_local(flockmind, 'sound/goonstation/flockmind/ArtifactFea2.ogg', 50, FALSE, use_reverb = FALSE)
+ SSticker.mode.traitors |= flockmind.mind
+ log_admin("[player_key] has been made into a flockmind by an event.")
+ log_game("[player_key] was spawned as a flockmind by an event.")
diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm
index 11009dfdbab..c9f711a7d7a 100644
--- a/code/modules/mob/dead/observer/orbit.dm
+++ b/code/modules/mob/dead/observer/orbit.dm
@@ -166,7 +166,10 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
var/list/antag_serialized = serialized.Copy()
antag_serialized["antag"] = "Irradiated Mouse"
antagonists += list(antag_serialized)
-
+ else if(isflockmob(M))
+ var/list/antag_serialized = serialized.Copy()
+ antag_serialized["antag"] = "Divine Flock"
+ antagonists += list(antag_serialized)
else
if(length(orbiters) >= 0.2 * length_of_ghosts) // If a bunch of people are orbiting an object, like the nuke disk.
highlights += list(serialized)
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index 380010d7606..07114bc9600 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -62,6 +62,10 @@
if(STAMINA)
return getStaminaLoss()
+/mob/living/proc/get_damage_percent()
+ var/max = getMaxHealth()
+ var/loss = getBruteLoss() + getFireLoss() + getToxLoss() + getCloneLoss() + getOxyLoss()
+ return ceil((max - loss) / max * 100)
/mob/living/proc/apply_damages(brute = 0, burn = 0, tox = 0, oxy = 0, clone = 0, def_zone = null, blocked = 0, stamina = 0)
if(blocked >= 100) return 0
diff --git a/code/modules/mob/living/default_language.dm b/code/modules/mob/living/default_language.dm
index d82393d8b15..178e6415751 100644
--- a/code/modules/mob/living/default_language.dm
+++ b/code/modules/mob/living/default_language.dm
@@ -1,4 +1,4 @@
-/mob/living/verb/set_default_language(language as null|anything in languages)
+/mob/verb/set_default_language(language as null|anything in languages)
set name = "Set Default Language"
set category = "IC"
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index e0034ef893d..21f96e5355e 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -747,6 +747,8 @@
existing_trail.color = H.dna.species.blood_color
else if(isalien(src))
existing_trail.color = "#05EE05"
+ else if(isflockmob(src))
+ existing_trail.color = COLOR_BLOOD_FLOCK
else
existing_trail.color = "#A10808"
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index c991665c588..9cfeb10fac6 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -99,8 +99,6 @@
/// This can either be a numerical direction or a soft object reference (UID). It makes the mob always face towards the selected thing.
var/forced_look = null
- var/datum/language/default_language
-
var/datum/middle_click_override/middleClickOverride = null
/// Famous last words -- if succumbing, what the user's last words were
diff --git a/code/modules/mob/mob_misc_procs.dm b/code/modules/mob/mob_misc_procs.dm
index dfbb2be1535..4044519f91e 100644
--- a/code/modules/mob/mob_misc_procs.dm
+++ b/code/modules/mob/mob_misc_procs.dm
@@ -469,7 +469,7 @@
set hidden = 1
if(can_change_intents)
- if(ishuman(src) || isalienadult(src) || isbrain(src))
+ if(ishuman(src) || isalienadult(src) || isbrain(src) || isflockmob(src))
switch(input)
if(INTENT_HELP,INTENT_DISARM,INTENT_GRAB,INTENT_HARM)
a_intent = input
diff --git a/code/modules/mob/mob_vars.dm b/code/modules/mob/mob_vars.dm
index d204c7e232e..059f57e9118 100644
--- a/code/modules/mob/mob_vars.dm
+++ b/code/modules/mob/mob_vars.dm
@@ -104,6 +104,8 @@
var/in_throw_mode = FALSE
+ var/datum/language/default_language
+
// See /datum/emote
/// Cooldown on audio effects from emotes.
diff --git a/code/modules/projectiles/projectile/flock_bolt.dm b/code/modules/projectiles/projectile/flock_bolt.dm
new file mode 100644
index 00000000000..dd69294da50
--- /dev/null
+++ b/code/modules/projectiles/projectile/flock_bolt.dm
@@ -0,0 +1,43 @@
+/obj/projectile/energy/flock_bolt
+ name = "incapacitor bolt"
+ icon = 'icons/goonstation/mob/featherzone.dmi'
+ icon_state = "stunbolt"
+
+ hitsound_wall = 'sound/weapons/effects/searwall.ogg'
+ hitsound = 'sound/effects/sparks3.ogg'
+
+ pass_flags = PASSGLASS | PASSTABLE | PASSGRILLE | PASSFLOCK
+ speed = 0.7
+ damage = 4
+
+ stamina = 25
+
+ impact_effect_type = /obj/effect/temp_visual/impact_effect/blue_laser
+
+ light_color = "#55b598"
+
+ tracer_type = /obj/effect/projectile/tracer/disabler
+ muzzle_type = /obj/effect/projectile/muzzle/disabler
+ impact_type = /obj/effect/projectile/impact/disabler
+
+/obj/projectile/energy/flock_bolt/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ if(ismecha(target))
+ var/obj/mecha/M = target
+ M.take_damage(20, BURN)
+
+ if(!isliving(target))
+ return
+
+ if(isflockmob(target))
+ return
+
+ if(issilicon(target))
+ var/mob/living/silicon/S = target
+ S.adjustStaminaLoss(25)
+
+ var/mob/living/victim = target
+ victim.SetConfused(2.5 SECONDS)
+
+ if(isanimal_or_basicmob(victim))
+ victim.Stun(10 SECONDS)
diff --git a/code/modules/research/xenobiology/xenobiology_dissections.dm b/code/modules/research/xenobiology/xenobiology_dissections.dm
index 6c09b74dfdc..ac0d7136889 100644
--- a/code/modules/research/xenobiology/xenobiology_dissections.dm
+++ b/code/modules/research/xenobiology/xenobiology_dissections.dm
@@ -264,6 +264,18 @@
/datum/xenobiology_surgery_container/goliath
xeno_specialized_organs = list(/obj/item/organ/internal/appendix/xenobiology/tendril)
+/datum/xenobiology_surgery_container/flockbit
+ xeno_specialized_organs = list(
+ /obj/item/organ/internal/liver/xenobiology/gnesis_shard,
+ )
+
+/datum/xenobiology_surgery_container/flockdrone
+ xeno_specialized_organs = list(
+ /obj/item/organ/internal/appendix/xenobiology/flock_converter,
+ /obj/item/organ/internal/kidneys/xenobiology/gnesis_filters,
+ /obj/item/organ/internal/liver/xenobiology/gnesis_shard,
+ )
+
/datum/xenobiology_surgery_container/megacarp
xeno_specialized_organs = list(/obj/item/organ/internal/heart/xenobiology/megacarp)
diff --git a/code/modules/research/xenobiology/xenobiology_organs.dm b/code/modules/research/xenobiology/xenobiology_organs.dm
index 9524a2937dd..8b3e36fa204 100644
--- a/code/modules/research/xenobiology/xenobiology_organs.dm
+++ b/code/modules/research/xenobiology/xenobiology_organs.dm
@@ -163,6 +163,28 @@
owner.add_vomit_floor(toxvomit = TRUE)
owner.AdjustConfused(rand(4 SECONDS, 6 SECONDS))
+/obj/item/organ/internal/liver/xenobiology/gnesis_shard
+ name = "gnesis shard"
+ desc = "This bio-organic chunk of hardened gnesis will likely harm humanoid life more than help."
+ analyzer_price = 20
+
+/obj/item/organ/internal/liver/xenobiology/gnesis_shard/on_life()
+ . = ..()
+
+ var/multiplier = 1
+ switch(organ_quality)
+ if(ORGAN_NORMAL)
+ multiplier = 1.5
+ if(ORGAN_PRISTINE)
+ multiplier = 2
+ if(prob(5))
+ to_chat(owner, SPAN_WARNING("Something hurts awfully in your abdomen."))
+ owner.adjustBruteLoss(5 * multiplier)
+ if(prob(10))
+ owner.adjustToxLoss(-2 * multiplier)
+ if(prob(10))
+ owner.reagents.add_reagent("gnesis_tox", 5 * multiplier)
+
/obj/item/organ/internal/liver/xenobiology/detox
name = "chemical neutralizers"
desc = "These glands seem to absorb any liquid they come in contact with, neutralizing any unnatural substances."
@@ -500,6 +522,59 @@
to_chat(user, SPAN_WARNING("We wouldn't get much use out of stinging that."))
revert_cast()
+/obj/item/organ/internal/appendix/xenobiology/flock_converter
+ name = "drone converter"
+ desc = "This teal organ holds a small gnesis fabricator, allowing the host to convert objects and similar to gnesis."
+ analyzer_price = 40
+ hidden_origin_tech = TECH_MATERIAL
+ hidden_tech_level = 5
+ var/datum/action/cooldown/mob_cooldown/gnesis_action
+
+/obj/item/organ/internal/appendix/xenobiology/flock_converter/insert(mob/living/carbon/M, special = 0, dont_remove_slot = 0)
+ . = ..()
+ gnesis_action = new /datum/action/cooldown/dispense_congealed_gnesis()
+ if(organ_quality == ORGAN_DAMAGED)
+ gnesis_action.cooldown_time = 4 MINUTES
+ gnesis_action.Grant(M)
+
+/obj/item/organ/internal/appendix/xenobiology/flock_converter/remove(mob/living/carbon/M, special = 0)
+ . = ..()
+ gnesis_action.Remove(M)
+
+/datum/action/cooldown/dispense_congealed_gnesis
+ name = "Dispense Congealed Gnesis"
+ button_icon = 'icons/goonstation/hud/flock_ui.dmi'
+ button_icon_state = "designate_tile"
+ desc = "Use this to dispense some gnesis onto the ground or into a container."
+ melee_cooldown_time = CLICK_CD_CLICK_ABILITY
+ click_to_activate = TRUE
+ cooldown_time = 2 MINUTES
+
+/datum/action/cooldown/dispense_congealed_gnesis/Activate(atom/target)
+ . = ..()
+ var/mob/living/carbon/C = owner
+ if(!iscarbon(C))
+ return
+ if(!C.Adjacent(target))
+ to_chat(C, SPAN_NOTICE("We are too far away to dispense gnesis there."))
+ return
+
+ var/obj/chemholder = new()
+ chemholder.create_reagents(50)
+ if(istype(target, /obj/item/reagent_containers/glass))
+ var/obj/item/reagent_containers/glass/container = target
+ if(!container.is_open_container())
+ to_chat(C, SPAN_NOTICE("That container is sealed."))
+ return
+ chemholder.reagents.add_reagent("gnesis_tox", 20)
+ chemholder.reagents.trans_to(container, 20)
+ StartCooldown()
+ return
+ var/turf/target_turf = get_turf(target)
+ chemholder.reagents.add_reagent("gnesis_tox", 50)
+ chemholder.reagents.reaction(target_turf)
+ StartCooldown()
+
/obj/item/organ/internal/heart/xenobiology/contortion
name = "contortion fibers"
desc = "A set of bands that wrap around joints and ligaments and muscles alike, pulling the body into unnatural shapes."
@@ -1441,6 +1516,22 @@
owner.physiology.armor = owner.physiology.armor.detachArmor(organ_resistance_boost)
QDEL_NULL(organ_resistance_boost)
+/obj/item/organ/internal/kidneys/xenobiology/gnesis_filters
+ name = "gnesis mesh kidneys"
+ desc = "This strange organ resembles a pair of biological kidneys, but their micro-structure is interlaced with nanotubules of gnesis."
+ analyzer_price = 40
+
+/obj/item/organ/internal/kidneys/xenobiology/gnesis_filters/on_life()
+ . = ..()
+ var/mult = 1
+ switch(organ_quality)
+ if(ORGAN_NORMAL)
+ mult = 1.5
+ if(ORGAN_PRISTINE)
+ mult = 2
+ owner.adjustToxLoss(-0.6 * mult)
+ owner.reagents.add_reagent("gnesis_tox", 0.1 * mult)
+
/obj/item/organ/internal/ears/xenobiology/colorful
name = "colorful organ"
desc = "This organ seems to constantly color and mold the other flesh around it. Thankfully, the changes are only aesthetic."
diff --git a/code/modules/tgui/states/flock_state.dm b/code/modules/tgui/states/flock_state.dm
new file mode 100644
index 00000000000..ddca1d10e9b
--- /dev/null
+++ b/code/modules/tgui/states/flock_state.dm
@@ -0,0 +1,14 @@
+/**
+ * tgui state: flock_state
+ *
+ * Checks that the user is a flock overmind, or an admin.
+ */
+
+GLOBAL_DATUM_INIT(flock_state, /datum/ui_state/flockmind, new)
+
+/datum/ui_state/flockmind/can_use_topic(src_object, mob/user)
+ if(istype(user, /mob/camera/flock/overmind))
+ return UI_INTERACTIVE
+ if(is_admin(user))
+ return UI_INTERACTIVE
+ return UI_CLOSE
diff --git a/icons/effects/spawner_icons.dmi b/icons/effects/spawner_icons.dmi
index 88bf608b7668f210317d429772e368f7b8c01637..cea08c70e332f1b966bdc12e1eaf28567a63cdc8 100644
GIT binary patch
literal 50699
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 |