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>
This commit is contained in:
PollardTheDragon
2026-07-08 17:31:28 -04:00
committed by GitHub
parent ab05a4ee3a
commit 24845b238a
228 changed files with 9724 additions and 49 deletions
+13
View File
@@ -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
+135
View File
@@ -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
+8 -2
View File
@@ -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])
+6
View File
@@ -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)
@@ -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)
+47
View File
@@ -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 ..()
@@ -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()
@@ -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
+64
View File
@@ -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()
@@ -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
+72
View File
@@ -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
@@ -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)
@@ -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)