Converts giant spiders to basic mobs (#29796)

* Giant spiders initial

* Ling spiders, araneous, conversion to basic

* Removed some extra

* Fixes cling spiders

* Linters

* Do_afters

* Nurse AI works now

* Cling spider AI

* Forgot an element

* Updatepaths

* New Linters

* AI New linters

* Fixed action buttons

* No longer wraps spiderlings, adds a movement delay

* Fixes the sarge hatching from eggs, increases action cooldown on eggs

* Improved cling spider AI, improved insect random speech

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2025-07-31 02:08:18 -04:00
committed by GitHub
parent 435637e257
commit 8976c80adc
30 changed files with 732 additions and 438 deletions
@@ -0,0 +1,221 @@
// basic spider mob, these generally guard nests
/mob/living/basic/giant_spider
name = "giant spider"
desc = "Furry and black, it makes you shudder to look at it. This one has deep red eyes."
icon_state = "guard"
icon_living = "guard"
icon_dead = "guard_dead"
mob_biotypes = MOB_ORGANIC | MOB_BUG
speak_emote = list("chitters")
see_in_dark = 8
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE
step_type = FOOTSTEP_MOB_CLAW
a_intent = INTENT_HARM
butcher_results = list(/obj/item/food/monstermeat/spidermeat = 2, /obj/item/food/monstermeat/spiderleg = 8)
response_help_continuous = "pets"
response_help_simple = "pets"
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently pushes aside"
maxHealth = 200
health = 200
obj_damage = 60
melee_damage_lower = 15
melee_damage_upper = 20
melee_attack_cooldown_min = 1.5 SECONDS
melee_attack_cooldown_max = 2.5 SECONDS
unsuitable_heat_damage = 20
unsuitable_cold_damage = 20
faction = list("spiders")
pass_flags = PASSTABLE
attack_verb_simple = "bite"
attack_verb_continuous = "bites"
attack_sound = 'sound/weapons/bite.ogg'
gold_core_spawnable = HOSTILE_SPAWN
contains_xeno_organ = TRUE
surgery_container = /datum/xenobiology_surgery_container/spider
ai_controller = /datum/ai_controller/basic_controller/giant_spider
/// How much venom is injected per bite into the poor victim
var/venom_per_bite = 0
/// Actions to grant on Initialize
var/list/innate_actions = list(/datum/action/innate/web_giant_spider = BB_SPIDER_WEB_ACTION)
/mob/living/basic/giant_spider/Initialize(mapload)
. = ..()
grant_actions_by_list(innate_actions)
/mob/living/basic/giant_spider/Destroy()
for(var/datum/action/innate/web_giant_spider/web_action in actions)
web_action.Remove(src)
return ..()
/mob/living/basic/giant_spider/melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(. && venom_per_bite > 0 && iscarbon(target) && (!client || a_intent == INTENT_HARM))
var/mob/living/carbon/C = target
var/inject_target = pick("chest", "head")
if(C.can_inject(null, FALSE, inject_target, FALSE))
C.reagents.add_reagent("spidertoxin", venom_per_bite)
/mob/living/basic/giant_spider/get_spacemove_backup(movement_dir)
. = ..()
// If we don't find any normal thing to use, attempt to use any nearby spider structure instead.
if(!.)
for(var/obj/structure/spider/S in range(1, get_turf(src)))
return S
/mob/living/basic/giant_spider/proc/create_web()
var/T = loc
visible_message("<span class='notice'>[src] begins to secrete a sticky substance.</span>")
if(!do_after_once(src, 4 SECONDS, target = loc, attempt_cancel_message = "You stop spinning a web.", interaction_key = "spider_web_create"))
return
new /obj/structure/spider/stickyweb(T)
// Nursemaids - these create webs and eggs
/mob/living/basic/giant_spider/nurse
desc = "Furry and black, it makes you shudder to look at it. This one has brilliant green eyes."
icon_state = "nurse"
icon_living = "nurse"
icon_dead = "nurse_dead"
butcher_results = list(/obj/item/food/monstermeat/spidermeat = 2, /obj/item/food/monstermeat/spiderleg = 8, /obj/item/food/monstermeat/spidereggs = 4)
ai_controller = /datum/ai_controller/basic_controller/giant_spider/nurse
maxHealth = 40
health = 40
melee_damage_lower = 5
melee_damage_upper = 10
gold_core_spawnable = NO_SPAWN
venom_per_bite = 30
innate_actions = list(
/datum/action/innate/web_giant_spider = BB_SPIDER_WEB_ACTION,
/datum/action/innate/wrap_giant_spider = BB_SPIDER_WRAP_ACTION,
/datum/action/innate/lay_eggs_giant_spider = BB_SPIDER_EGG_LAYING_ACTION
)
/// How much have we eaten
var/fed = 0
/mob/living/basic/giant_spider/nurse/proc/wrap_target()
var/atom/cocoon_target
if(!client)
cocoon_target = ai_controller.blackboard[BB_SPIDER_WRAP_TARGET]
if(!cocoon_target && client)
var/list/choices = list()
for(var/mob/living/L in view(1, src))
if(L == src)
continue
if(L.stat != DEAD)
continue
if(istype(L, /mob/living/basic/giant_spider))
continue
if(Adjacent(L))
choices += L
for(var/obj/O in get_turf(src))
if(O.anchored)
continue
if(!(isitem(O) || isstructure(O) || ismachinery(O)))
continue
if(Adjacent(O))
choices += O
if(length(choices))
cocoon_target = tgui_input_list(src, "What do you wish to cocoon?", "Cocoon Wrapping", choices)
else
to_chat(src, "<span class='warning'>No suitable dead prey or wrappable objects found nearby.")
return
if(cocoon_target)
visible_message("<span class='notice'>[src] begins to secrete a sticky substance around [cocoon_target].</span>")
if(!do_after_once(src, 5 SECONDS, target = cocoon_target, attempt_cancel_message = "You stop wrapping [cocoon_target].", interaction_key = "spider_web_wrap"))
return
if(cocoon_target && isturf(cocoon_target.loc) && get_dist(src, cocoon_target) <= 1)
var/obj/structure/spider/cocoon/C = new(cocoon_target.loc)
var/large_cocoon = FALSE
C.pixel_x = cocoon_target.pixel_x
C.pixel_y = cocoon_target.pixel_y
for(var/obj/item/I in C.loc)
I.loc = C
for(var/obj/structure/S in C.loc)
if(!S.anchored)
S.loc = C
large_cocoon = TRUE
for(var/obj/machinery/M in C.loc)
if(!M.anchored)
M.loc = C
large_cocoon = TRUE
for(var/mob/living/L in C.loc)
if(istype(L, /mob/living/basic/giant_spider))
continue
if(L.stat != DEAD)
continue
large_cocoon = TRUE
L.loc = C
C.pixel_x = L.pixel_x
C.pixel_y = L.pixel_y
fed++
visible_message("<span class='danger'>[src] sticks a proboscis into [L] and sucks a viscous substance out.</span>")
break
if(large_cocoon)
C.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3")
/mob/living/basic/giant_spider/nurse/proc/lay_spider_eggs()
var/obj/structure/spider/eggcluster/E = locate() in get_turf(src)
if(E)
to_chat(src, "<span class='notice'>There is already a cluster of eggs here!</span>")
return
if(!fed)
to_chat(src, "<span class='warning'>You are too hungry to do this!</span>")
return
visible_message("<span class='notice'>[src] begins to lay a cluster of eggs.</span>")
if(!do_after_once(src, 4 SECONDS, target = loc, attempt_cancel_message = "You stop laying eggs.", interaction_key = "spider_egg_lay"))
return
var/obj/structure/spider/eggcluster/C = new /obj/structure/spider/eggcluster(loc)
C.faction = faction.Copy()
C.master_commander = master_commander
if(ckey)
C.player_spiders = TRUE
fed--
/mob/living/basic/giant_spider/nurse/proc/find_cocoon_target()
// Prioritize food
var/list/food = list()
var/list/can_see = view(src, 10)
for(var/mob/living/C in can_see)
if(C.stat && !istype(C, /mob/living/basic/giant_spider) && !C.anchored)
food += C
if(length(food))
return pick(food)
var/list/objects = list()
for(var/obj/O in can_see)
if(O.anchored)
continue
if(isitem(O) || isstructure(O) || ismachinery(O))
objects += O
if(length(objects))
return pick(objects)
return
// Hunters have the most poison and move the fastest, so they can find prey
/mob/living/basic/giant_spider/hunter
desc = "Furry and black, it makes you shudder to look at it. This one has sparkling purple eyes."
icon_state = "hunter"
icon_living = "hunter"
icon_dead = "hunter_dead"
maxHealth = 120
health = 120
melee_damage_lower = 10
venom_per_bite = 10
/mob/living/basic/giant_spider/araneus
name = "Sergeant Araneus"
real_name = "Sergeant Araneus"
desc = "A fierce companion for any person of power, this spider has been carefully trained by Nanotrasen specialists. Its beady, staring eyes send shivers down your spine."
faction = list("spiders")
maxHealth = 250
health = 250
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 2, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
gender = FEMALE
ai_controller = /datum/ai_controller/basic_controller/giant_spider/retaliate
/mob/living/basic/giant_spider/araneus/Initialize(mapload)
. = ..()
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/pet_bonus, "chitters!")
@@ -0,0 +1,26 @@
/datum/action/innate/web_giant_spider
name = "Lay Web"
button_icon = 'icons/effects/effects.dmi'
button_icon_state = "stickyweb1"
/datum/action/innate/web_giant_spider/Activate()
var/mob/living/basic/giant_spider/user = owner
user.create_web()
/datum/action/innate/wrap_giant_spider
name = "Wrap"
button_icon = 'icons/effects/effects.dmi'
button_icon_state = "cocoon_large1"
/datum/action/innate/wrap_giant_spider/Activate()
var/mob/living/basic/giant_spider/nurse/user = owner
user.wrap_target()
/datum/action/innate/lay_eggs_giant_spider
name = "Lay Eggs"
button_icon = 'icons/effects/effects.dmi'
button_icon_state = "eggs"
/datum/action/innate/lay_eggs_giant_spider/Activate()
var/mob/living/basic/giant_spider/nurse/user = owner
user.lay_spider_eggs()
@@ -0,0 +1,292 @@
/// Attacks people it can see, spins webs if it can't see anything to attack.
/datum/ai_controller/basic_controller/giant_spider
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
movement_delay = 1 SECONDS
idle_behavior = /datum/idle_behavior/idle_random_walk/less_walking
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED | AI_FLAG_PAUSE_DURING_DO_AFTER
planning_subtrees = list(
/datum/ai_planning_subtree/random_speech/insect,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_unwebbed_turf,
/datum/ai_planning_subtree/spin_web,
)
/datum/ai_controller/basic_controller/giant_spider/nurse
planning_subtrees = list(
/datum/ai_planning_subtree/random_speech/insect,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/lay_eggs,
/datum/ai_planning_subtree/find_unwrapped_target,
/datum/ai_planning_subtree/find_unwebbed_turf,
/datum/ai_planning_subtree/wrap_target,
/datum/ai_planning_subtree/spin_web,
)
/datum/ai_controller/basic_controller/giant_spider/changeling
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
)
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target/cling_spider,
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/cling_spider_follow,
)
/// Used by Araneus, who only attacks those who attack first. He is house-trained and will not web up the HoS office.
/datum/ai_controller/basic_controller/giant_spider/retaliate
planning_subtrees = list(
/datum/ai_planning_subtree/random_speech/insect,
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/datum/ai_planning_subtree/random_speech/insect
speech_chance = 2
sound = list('sound/creatures/chitter.ogg')
emote_hear = list("chitters.")
/// Search for a nearby location to put webs on
/datum/ai_planning_subtree/find_unwebbed_turf
/datum/ai_planning_subtree/find_unwebbed_turf/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
controller.queue_behavior(/datum/ai_behavior/find_unwebbed_turf)
/// Find an unwebbed nearby turf and store it
/datum/ai_behavior/find_unwebbed_turf
action_cooldown = 5 SECONDS
/// Where do we store the target data
var/target_key = BB_SPIDER_WEB_TARGET
/// How far do we look for unwebbed turfs?
var/scan_range = 3
/datum/ai_behavior/find_unwebbed_turf/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/spider = controller.pawn
var/atom/current_target = controller.blackboard[target_key]
if(current_target && !(locate(/obj/structure/spider/stickyweb) in current_target))
// Already got a target
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.clear_blackboard_key(target_key)
var/turf/our_turf = get_turf(spider)
if(is_valid_web_turf(our_turf, spider))
controller.set_blackboard_key(target_key, our_turf)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
var/list/turfs_by_range = list()
for(var/i in 1 to scan_range)
turfs_by_range["[i]"] = list()
for(var/turf/turf_in_view in oview(scan_range, our_turf))
if(!is_valid_web_turf(turf_in_view, spider))
continue
turfs_by_range["[get_dist(our_turf, turf_in_view)]"] += turf_in_view
var/list/final_turfs
for(var/list/turf_list as anything in turfs_by_range)
if(length(turfs_by_range[turf_list]))
final_turfs = turfs_by_range[turf_list]
break
if(!length(final_turfs))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(target_key, pick(final_turfs))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_unwebbed_turf/proc/is_valid_web_turf(turf/target_turf, mob/living/spider)
if(locate(/obj/structure/spider/stickyweb) in target_turf)
return FALSE
if(HAS_TRAIT(target_turf, TRAIT_SPINNING_WEB_TURF))
return FALSE
return !target_turf.is_blocked_turf(source_atom = spider)
/// Run the spin web behaviour if we have an ability to use for it
/datum/ai_planning_subtree/spin_web
/// Key where the web spinning action is stored
var/action_key = BB_SPIDER_WEB_ACTION
/// Key where the target turf is stored
var/target_key = BB_SPIDER_WEB_TARGET
/datum/ai_planning_subtree/spin_web/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(action_key) && controller.blackboard_key_exists(target_key))
controller.queue_behavior(/datum/ai_behavior/spin_web, action_key, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
/// Move to an unwebbed nearby turf and web it up
/datum/ai_behavior/spin_web
action_cooldown = 10 SECONDS // We don't want them doing this too quickly
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/spin_web/setup(datum/ai_controller/controller, action_key, target_key)
var/datum/action/innate/web_giant_spider/web_action = controller.blackboard[action_key]
var/turf/target_turf = controller.blackboard[target_key]
if(!web_action || !target_turf)
return FALSE
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/spin_web/perform(seconds_per_tick, datum/ai_controller/controller, action_key, target_key)
var/datum/action/innate/web_giant_spider/web_action = controller.blackboard[action_key]
if(web_action?.Trigger())
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/spin_web/finish_action(datum/ai_controller/controller, succeeded, action_key, target_key)
controller.clear_blackboard_key(target_key)
return ..()
/// Search for a nearby location to put webs on
/datum/ai_planning_subtree/find_unwrapped_target
/datum/ai_planning_subtree/find_unwrapped_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
controller.queue_behavior(/datum/ai_behavior/find_unwrapped_target)
/// Find an unwrapped target and store it
/datum/ai_behavior/find_unwrapped_target
action_cooldown = 5 SECONDS
/// Where do we store the target data
var/target_key = BB_SPIDER_WRAP_TARGET
/// How far do we look for unwebbed items?
var/scan_range = 3
/datum/ai_behavior/find_unwrapped_target/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/spider = controller.pawn
var/atom/current_target = controller.blackboard[target_key]
if(current_target)
// Already got a target
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
// Prioritize food
var/list/food = list()
var/list/can_see = view(scan_range, spider)
for(var/mob/living/C in can_see)
if(C.stat && !istype(C, /mob/living/basic/giant_spider) && !C.anchored)
food += C
if(length(food))
controller.set_blackboard_key(target_key, pick(food))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
var/list/objects = list()
for(var/obj/O in can_see)
if(O.anchored)
continue
if(istype(O, /obj/structure/spider))
continue // Don't web up spider structures or spiderlings
if(isitem(O) || isstructure(O) || ismachinery(O))
objects += O
if(length(objects))
controller.set_blackboard_key(target_key, pick(objects))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/// Run the wrap behaviour if we have an ability to use for it
/datum/ai_planning_subtree/wrap_target
/// Key where the web spinning action is stored
var/action_key = BB_SPIDER_WRAP_ACTION
/// Key where the target is stored
var/target_key = BB_SPIDER_WRAP_TARGET
/datum/ai_planning_subtree/wrap_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(action_key) && controller.blackboard_key_exists(target_key))
controller.queue_behavior(/datum/ai_behavior/wrap_target, action_key, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
/// Move to an unwrapped item and wrap it
/datum/ai_behavior/wrap_target
action_cooldown = 15 SECONDS // We don't want them doing this too quickly
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/wrap_target/setup(datum/ai_controller/controller, action_key, target_key)
var/datum/action/innate/wrap_giant_spider/wrap_action = controller.blackboard[action_key]
var/atom/target = controller.blackboard[target_key]
var/turf/target_turf = get_turf(target)
if(!wrap_action || !target_turf)
return FALSE
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/wrap_target/perform(seconds_per_tick, datum/ai_controller/controller, action_key, target_key)
var/datum/action/innate/wrap_giant_spider/wrap_action = controller.blackboard[action_key]
if(wrap_action?.Trigger())
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/wrap_target/finish_action(datum/ai_controller/controller, succeeded, action_key, target_key)
controller.clear_blackboard_key(target_key)
return ..()
/// Run the egg laying behavior
/datum/ai_planning_subtree/lay_eggs
/// Key where the egg laying action is stored
var/action_key = BB_SPIDER_EGG_LAYING_ACTION
/datum/ai_planning_subtree/lay_eggs/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/giant_spider/nurse/spider = controller.pawn
if(controller.blackboard_key_exists(action_key) && spider.fed > 0)
controller.queue_behavior(/datum/ai_behavior/lay_eggs, action_key)
return SUBTREE_RETURN_FINISH_PLANNING
/// Attempt to lay eggs if we're fed
/datum/ai_behavior/lay_eggs
action_cooldown = 2 MINUTES
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/lay_eggs/setup(datum/ai_controller/controller, action_key)
var/datum/action/innate/lay_eggs_giant_spider/egg_action = controller.blackboard[action_key]
if(!egg_action)
return FALSE
return ..()
/datum/ai_behavior/lay_eggs/perform(seconds_per_tick, datum/ai_controller/controller, action_key)
var/datum/action/innate/lay_eggs_giant_spider/egg_action = controller.blackboard[action_key]
if(egg_action?.Trigger())
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/// Spider only attacks when it has the valid order.
/datum/ai_planning_subtree/simple_find_target/cling_spider
/datum/ai_planning_subtree/simple_find_target/cling_spider/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard[BB_CHANGELING_SPIDER_ORDER] == 1 || controller.blackboard[BB_CHANGELING_SPIDER_ORDER] == 3)
return
return ..()
/// Spider follows who created it
/datum/ai_planning_subtree/cling_spider_follow
/// Key where the owner is stored
var/target_key = BB_CURRENT_PET_TARGET
/datum/ai_planning_subtree/cling_spider_follow/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(target_key) && controller.blackboard[BB_CHANGELING_SPIDER_ORDER] < 2)
controller.queue_behavior(/datum/ai_behavior/cling_spider_follow, target_key)
return SUBTREE_RETURN_FINISH_PLANNING
/// Attempt to follow the owner
/datum/ai_behavior/cling_spider_follow
required_distance = 0
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
/datum/ai_behavior/cling_spider_follow/setup(datum/ai_controller/controller, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/cling_spider_follow/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_DELAY