diff --git a/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm b/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
index 2e99a4f45eb..db8daf0aed2 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/ussp.dmm
@@ -6061,7 +6061,7 @@
},
/area/ruin/space/derelict/arrival)
"qA" = (
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter,
+/mob/living/basic/giant_spider/hunter,
/turf/simulated/floor/plasteel,
/area/ruin/space/derelict/crew_quarters)
"qB" = (
@@ -6865,7 +6865,7 @@
/turf/simulated/wall/indestructible/titanium/soviet/nodiagonal,
/area/ruin/space/derelict/arrival)
"Ae" = (
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter,
+/mob/living/basic/giant_spider/hunter,
/turf/simulated/floor/plasteel/dark,
/area/ruin/space/derelict/crew_quarters)
"Bx" = (
diff --git a/_maps/map_files/stations/boxstation.dmm b/_maps/map_files/stations/boxstation.dmm
index 883e66783df..a6b8958bf14 100644
--- a/_maps/map_files/stations/boxstation.dmm
+++ b/_maps/map_files/stations/boxstation.dmm
@@ -2986,7 +2986,7 @@
/turf/simulated/floor/carpet,
/area/station/command/office/hos)
"amv" = (
-/mob/living/simple_animal/hostile/retaliate/araneus,
+/mob/living/basic/giant_spider/araneus,
/turf/simulated/floor/carpet,
/area/station/command/office/hos)
"amy" = (
diff --git a/_maps/map_files/stations/cerestation.dmm b/_maps/map_files/stations/cerestation.dmm
index 2950a21a77e..6e5e7932093 100644
--- a/_maps/map_files/stations/cerestation.dmm
+++ b/_maps/map_files/stations/cerestation.dmm
@@ -94143,7 +94143,7 @@
/obj/structure/bed/dogbed{
name = "Araneus' bed"
},
-/mob/living/simple_animal/hostile/retaliate/araneus,
+/mob/living/basic/giant_spider/araneus,
/turf/simulated/floor/wood,
/area/station/command/office/hos)
"sGP" = (
diff --git a/_maps/map_files/stations/deltastation.dmm b/_maps/map_files/stations/deltastation.dmm
index 7e05fd3948a..1c0267dc450 100644
--- a/_maps/map_files/stations/deltastation.dmm
+++ b/_maps/map_files/stations/deltastation.dmm
@@ -67005,7 +67005,7 @@
},
/area/station/hallway/primary/port/north)
"kTO" = (
-/mob/living/simple_animal/hostile/retaliate/araneus,
+/mob/living/basic/giant_spider/araneus,
/obj/structure/bed/dogbed{
name = "Araneus's lair"
},
diff --git a/_maps/map_files/stations/emeraldstation.dmm b/_maps/map_files/stations/emeraldstation.dmm
index 5518993d9ae..303dbeb9a4d 100644
--- a/_maps/map_files/stations/emeraldstation.dmm
+++ b/_maps/map_files/stations/emeraldstation.dmm
@@ -42324,7 +42324,7 @@
/obj/machinery/atmospherics/pipe/simple/hidden/supply{
dir = 10
},
-/mob/living/simple_animal/hostile/retaliate/araneus,
+/mob/living/basic/giant_spider/araneus,
/turf/simulated/floor/plasteel{
icon_state = "darkgrey"
},
diff --git a/_maps/map_files/stations/metastation.dmm b/_maps/map_files/stations/metastation.dmm
index ce09696b334..23f583b9a87 100644
--- a/_maps/map_files/stations/metastation.dmm
+++ b/_maps/map_files/stations/metastation.dmm
@@ -64275,7 +64275,7 @@
/area/station/science/research)
"oyY" = (
/obj/structure/bed/dogbed,
-/mob/living/simple_animal/hostile/retaliate/araneus,
+/mob/living/basic/giant_spider/araneus,
/obj/machinery/firealarm/directional/east,
/turf/simulated/floor/wood,
/area/station/command/office/hos)
diff --git a/code/__DEFINES/ai/ai_defines.dm b/code/__DEFINES/ai/ai_defines.dm
index 1cf3ea125a1..7923d351393 100644
--- a/code/__DEFINES/ai/ai_defines.dm
+++ b/code/__DEFINES/ai/ai_defines.dm
@@ -11,6 +11,12 @@
/// The AI is currently in idle mode.
#define AI_STATUS_IDLE "ai_idle"
+// Flags returned by get_able_to_run()
+/// pauses AI processing
+#define AI_UNABLE_TO_RUN (1<<1)
+/// bypass canceling our actions on set_ai_status()
+#define AI_PREVENT_CANCEL_ACTIONS (1<<2)
+
// How far should we, by default, be looking for interesting things to de-idle?
#define AI_DEFAULT_INTERESTING_DIST 10
#define AI_SIMPLE_INTERESTING_DIST 15
diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm
index 4291ac1cd55..adb7f72e57a 100644
--- a/code/__DEFINES/ai/blackboard_defines.dm
+++ b/code/__DEFINES/ai/blackboard_defines.dm
@@ -159,3 +159,17 @@
#define BB_INCURSION_HOME_PORTAL "BB_INCURSION_HOME_PORTAL"
#define BB_INCURSION_HOME_PORTAL_TARGET "BB_INCURSION_HOME_PORTAL_TARGET"
#define BB_PROWL_TARGET "BB_PROWL_TARGET"
+
+// Giant Spiders
+/// Our webbing target
+#define BB_SPIDER_WEB_TARGET "BB_spider_web_target"
+/// Key where we store the web-spinning ability
+#define BB_SPIDER_WEB_ACTION "BB_spider_web_action"
+/// Our wrapping target
+#define BB_SPIDER_WRAP_TARGET "BB_spider_wrap_target"
+/// Key where we define the wrapping ability
+#define BB_SPIDER_WRAP_ACTION "BB_spider_wrap_action"
+/// Key where we define the egg laying ability
+#define BB_SPIDER_EGG_LAYING_ACTION "BB_spider_egg_laying_action"
+/// Key used by changelings who control spiders
+#define BB_CHANGELING_SPIDER_ORDER "BB_changeling_spider_order"
diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm
index 9207e195dc2..4154e87b750 100644
--- a/code/__DEFINES/dcs/mob_signals.dm
+++ b/code/__DEFINES/dcs/mob_signals.dm
@@ -211,6 +211,11 @@
///cancel post eating
#define COMSIG_MOB_TERMINATE_EAT (1<<0)
+/// Sent from /proc/do_after if someone starts a do_after action bar.
+#define COMSIG_DO_AFTER_BEGAN "mob_do_after_began"
+/// Sent from /proc/do_after once a do_after action completes, whether via the bar filling or via interruption.
+#define COMSIG_DO_AFTER_ENDED "mob_do_after_ended"
+
// ghost signals
/// from observer_base/do_observe(): (mob/now_followed)
diff --git a/code/__HELPERS/mob_helpers.dm b/code/__HELPERS/mob_helpers.dm
index e38cdd818c3..9960df73b07 100644
--- a/code/__HELPERS/mob_helpers.dm
+++ b/code/__HELPERS/mob_helpers.dm
@@ -388,9 +388,13 @@
* param {boolean} hidden - By default, any action 1 second or longer shows a cog over the user while it is in progress. If hidden is set to TRUE, the cog will not be shown.
* If allow_sleeping_or_dead is true, dead and sleeping mobs will continue. Good if you want to show a progress bar to the user but it doesn't need them to do anything, like modsuits.
*/
-/proc/do_after(mob/user, delay, needhand = 1, atom/target = null, progress = 1, allow_moving = 0, must_be_held = 0, list/extra_checks = list(), use_default_checks = TRUE, allow_moving_target = FALSE, hidden = FALSE, allow_sleeping_or_dead = FALSE)
+/proc/do_after(mob/user, delay, needhand = 1, atom/target = null, progress = 1, allow_moving = 0, must_be_held = 0, list/extra_checks = list(), interaction_key = null, use_default_checks = TRUE, allow_moving_target = FALSE, hidden = FALSE, allow_sleeping_or_dead = FALSE)
if(!user)
return FALSE
+
+ if(interaction_key)
+ var/current_interaction_count = LAZYACCESS(user.do_afters, interaction_key) || 0
+ LAZYSET(user.do_afters, interaction_key, current_interaction_count + 1)
var/atom/Tloc = null
if(target)
Tloc = target.loc
@@ -415,6 +419,7 @@
if(!hidden && delay >= 1 SECONDS)
cog = new(user)
+ SEND_SIGNAL(user, COMSIG_DO_AFTER_BEGAN)
var/endtime = world.time + delay
var/starttime = world.time
. = TRUE
@@ -462,6 +467,14 @@
if(progress)
qdel(progbar)
cog?.remove()
+ if(interaction_key)
+ var/reduced_interaction_count = (LAZYACCESS(user.do_afters, interaction_key) || 0) - 1
+ if(reduced_interaction_count > 0) // Not done yet!
+ LAZYSET(user.do_afters, interaction_key, reduced_interaction_count)
+ return
+ // all out, let's clear er out fully
+ LAZYREMOVE(user.do_afters, interaction_key)
+ SEND_SIGNAL(user, COMSIG_DO_AFTER_ENDED)
// Upon any of the callbacks in the list returning TRUE, the proc will return TRUE.
/proc/check_for_true_callbacks(list/extra_checks)
@@ -472,7 +485,7 @@
#define DOAFTERONCE_MAGIC "Magic~~"
GLOBAL_LIST_EMPTY(do_after_once_tracker)
-/proc/do_after_once(mob/user, delay, needhand = 1, atom/target = null, progress = 1, allow_moving, must_be_held, attempt_cancel_message = "Attempt cancelled.", special_identifier, hidden = FALSE)
+/proc/do_after_once(mob/user, delay, needhand = 1, atom/target = null, progress = 1, allow_moving, must_be_held, attempt_cancel_message = "Attempt cancelled.", special_identifier, hidden = FALSE, interaction_key = null)
if(!user || !target)
return
@@ -482,7 +495,7 @@ GLOBAL_LIST_EMPTY(do_after_once_tracker)
to_chat(user, "[attempt_cancel_message]")
return FALSE
GLOB.do_after_once_tracker[cache_key] = TRUE
- . = do_after(user, delay, needhand, target, progress, allow_moving, must_be_held, extra_checks = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(do_after_once_checks), cache_key, hidden)))
+ . = do_after(user, delay, needhand, target, progress, allow_moving, must_be_held, extra_checks = list(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(do_after_once_checks), cache_key, hidden)), interaction_key = interaction_key)
GLOB.do_after_once_tracker[cache_key] = FALSE
/proc/do_after_once_checks(cache_key)
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index 11fd577aa94..cb317e90ab3 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -527,6 +527,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
// turf trait sources
#define FLOOR_EFFECT_TRAIT "floor_effect_trait"
+/// A web is being spun on this turf presently
+#define TRAIT_SPINNING_WEB_TURF "spinning_web_turf"
+
//***** EFFECT TRAITS *****//
// Causes the effect to go through a teleporter instead of being deleted by it.
#define TRAIT_EFFECT_CAN_TELEPORT "trait_effect_can_teleport"
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 18ac10d17cc..54f8727dc89 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -149,7 +149,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
/turf = list(
"bluespace_speed_trait" = TRAIT_BLUESPACE_SPEED,
- "TRAIT_RUSTY" = TRAIT_RUSTY
+ "TRAIT_RUSTY" = TRAIT_RUSTY,
+ /// A web is being spun on this turf presently
+ "TRAIT_SPINNING_WEB_TURF" = TRAIT_SPINNING_WEB_TURF
),
/obj/effect = list(
diff --git a/code/datums/ai/ai_controller.dm b/code/datums/ai/ai_controller.dm
index fc23d17a333..984b14731a1 100644
--- a/code/datums/ai/ai_controller.dm
+++ b/code/datums/ai/ai_controller.dm
@@ -70,6 +70,11 @@ RESTRICT_TYPE(/datum/ai_controller)
var/can_idle = TRUE
/// What distance should we be checking for interesting things when considering idling/deidling? Defaults to AI_DEFAULT_INTERESTING_DIST
var/interesting_dist = AI_DEFAULT_INTERESTING_DIST
+ /// TRUE if we're able to run, FALSE if we aren't
+ /// Should not be set manually, override get_able_to_run() instead
+ /// Make sure you hook update_able_to_run() in setup_able_to_run() to whatever parameters changing that you added
+ /// Otherwise we will not pay attention to them changing
+ var/able_to_run = FALSE
/// are we currently on failed planning timeout?
var/on_failed_planning_timeout = FALSE
@@ -166,6 +171,8 @@ RESTRICT_TYPE(/datum/ai_controller)
RegisterSignal(pawn, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
RegisterSignal(pawn, COMSIG_PARENT_QDELETING, PROC_REF(on_pawn_qdeleted))
+ update_able_to_run()
+ setup_able_to_run()
/datum/ai_controller/proc/on_movement_target_move(datum/source)
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
@@ -254,13 +261,30 @@ RESTRICT_TYPE(/datum/ai_controller)
if(destroy)
qdel(src)
-/// Returns TRUE if the ai controller can actually run at the moment.
-/datum/ai_controller/proc/able_to_run()
+/datum/ai_controller/proc/setup_able_to_run()
+ // paused_until is handled by PauseAi() 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()
+ UnregisterSignal(pawn, list(SIGNAL_ADDTRAIT(TRAIT_AI_PAUSED), SIGNAL_REMOVETRAIT(TRAIT_AI_PAUSED)))
+
+/datum/ai_controller/proc/update_able_to_run()
+ SIGNAL_HANDLER
+ var/run_flags = get_able_to_run()
+ if(run_flags & AI_UNABLE_TO_RUN)
+ able_to_run = FALSE
+ GLOB.move_manager.stop_looping(pawn) //stop moving
+ else
+ able_to_run = TRUE
+ set_ai_status(get_expected_ai_status(), run_flags)
+
+/// Returns TRUE if the ai controller can actually run at the moment, FALSE otherwise
+/datum/ai_controller/proc/get_able_to_run()
if(HAS_TRAIT(pawn, TRAIT_AI_PAUSED))
- return FALSE
+ return AI_UNABLE_TO_RUN
if(world.time < paused_until)
- return FALSE
- return TRUE
+ return AI_UNABLE_TO_RUN
+ return NONE
/datum/ai_controller/proc/ai_can_interact()
SHOULD_CALL_PARENT(TRUE)
@@ -295,7 +319,7 @@ RESTRICT_TYPE(/datum/ai_controller)
// in the AI controller implementation are actually the managing subsystem's `wait`.
seconds_per_tick /= (1 SECONDS)
- if(!able_to_run())
+ if(!able_to_run)
GLOB.move_manager.stop_looping(pawn) //stop moving
return //this should remove them from processing in the future through event-based stuff.
@@ -457,6 +481,7 @@ RESTRICT_TYPE(/datum/ai_controller)
/datum/ai_controller/proc/on_stat_changed(mob/living/source, new_stat)
SIGNAL_HANDLER // COMSIG_MOB_STATCHANGE
reset_ai_status()
+ update_able_to_run()
/datum/ai_controller/proc/on_sentience_gained()
SIGNAL_HANDLER // COMSIG_MOB_LOGIN
diff --git a/code/datums/ai/basic_mobs/basic_controller.dm b/code/datums/ai/basic_mobs/basic_controller.dm
index 0dd0ea31cd2..77e7fc668da 100644
--- a/code/datums/ai/basic_mobs/basic_controller.dm
+++ b/code/datums/ai/basic_mobs/basic_controller.dm
@@ -17,3 +17,24 @@
SIGNAL_HANDLER
var/food_cooldown = blackboard[BB_EAT_FOOD_COOLDOWN] || EAT_FOOD_COOLDOWN
set_blackboard_key(BB_NEXT_FOOD_EAT, world.time + food_cooldown)
+
+/datum/ai_controller/basic_controller/on_stat_changed(mob/living/source, new_stat)
+ . = ..()
+ update_able_to_run()
+
+/datum/ai_controller/basic_controller/setup_able_to_run()
+ . = ..()
+ if(ai_traits & AI_FLAG_PAUSE_DURING_DO_AFTER)
+ RegisterSignals(pawn, list(COMSIG_DO_AFTER_BEGAN, COMSIG_DO_AFTER_ENDED), PROC_REF(update_able_to_run))
+
+/datum/ai_controller/basic_controller/clear_able_to_run()
+ UnregisterSignal(pawn, list(COMSIG_DO_AFTER_BEGAN, COMSIG_DO_AFTER_ENDED))
+ return ..()
+
+/datum/ai_controller/basic_controller/get_able_to_run()
+ . = ..()
+ if(. & AI_UNABLE_TO_RUN)
+ return .
+ var/mob/living/living_pawn = pawn
+ if(ai_traits & AI_FLAG_PAUSE_DURING_DO_AFTER && LAZYLEN(living_pawn.do_afters))
+ return AI_UNABLE_TO_RUN | AI_PREVENT_CANCEL_ACTIONS // dont erase targets post a do_after
diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm
index a16ebbd3115..716e57e44f3 100644
--- a/code/datums/ai/idle_behaviors/idle_random_walk.dm
+++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm
@@ -14,3 +14,6 @@
return FALSE
living_pawn.Move(destination_turf, move_dir)
return TRUE
+
+/datum/idle_behavior/idle_random_walk/less_walking
+ walk_chance = 10
diff --git a/code/datums/ai/movement/ai_movement.dm b/code/datums/ai/movement/ai_movement.dm
index d9d5e1c5ff5..4cbcadab6c7 100644
--- a/code/datums/ai/movement/ai_movement.dm
+++ b/code/datums/ai/movement/ai_movement.dm
@@ -56,7 +56,7 @@
var/datum/ai_controller/controller = source.extra_info
// Check if this controller can actually run, so we don't chase people with corpses
- if(!controller.able_to_run())
+ if(!controller.able_to_run)
controller.cancel_actions()
qdel(source) // stop moving
return MOVELOOP_SKIP_STEP
diff --git a/code/datums/elements/pet_bonus.dm b/code/datums/elements/pet_bonus.dm
new file mode 100644
index 00000000000..767214548d9
--- /dev/null
+++ b/code/datums/elements/pet_bonus.dm
@@ -0,0 +1,33 @@
+/**
+ * # Pet bonus element!
+ *
+ * Bespoke element that plays a fun message, and sends a heart out when you pet this animal.
+ */
+/datum/element/pet_bonus
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+
+ ///string key of the emote to do when pet.
+ var/emote_name
+
+/datum/element/pet_bonus/Attach(datum/target, emote_name)
+ . = ..()
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.emote_name = emote_name
+ RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand))
+
+/datum/element/pet_bonus/Detach(datum/target)
+ . = ..()
+ UnregisterSignal(target, COMSIG_ATOM_ATTACK_HAND)
+
+/datum/element/pet_bonus/proc/on_attack_hand(mob/living/pet, mob/living/petter, list/modifiers)
+ SIGNAL_HANDLER
+
+ if(pet.stat != CONSCIOUS || petter.intent != INTENT_HELP || LAZYACCESS(modifiers, RIGHT_CLICK))
+ return
+
+ new /obj/effect/temp_visual/heart(get_turf(pet))
+ if(emote_name && prob(33))
+ pet.emote("me", EMOTE_VISIBLE, emote_name)
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index 91b301762c9..44afc8309d9 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -44,7 +44,7 @@
AddElement(/datum/element/connect_loc, loc_connections)
/obj/structure/spider/stickyweb/proc/on_atom_exit(datum/source, atom/exiter)
- if(istype(exiter, /mob/living/simple_animal/hostile/poison/giant_spider) || isterrorspider(exiter))
+ if(istype(exiter, /mob/living/basic/giant_spider) || isterrorspider(exiter))
return
if(isliving(exiter) && prob(50))
to_chat(exiter, "You get stuck in [src] for a moment.")
@@ -181,8 +181,8 @@
qdel(src)
return
if(!grow_as)
- grow_as = pick(typesof(/mob/living/simple_animal/hostile/poison/giant_spider) - /mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider)
- var/mob/living/simple_animal/hostile/poison/giant_spider/S = new grow_as(loc)
+ grow_as = pick(typesof(/mob/living/basic/giant_spider) - list(/mob/living/basic/giant_spider/hunter/infestation_spider, /mob/living/basic/giant_spider/araneus))
+ var/mob/living/basic/giant_spider/S = new grow_as(loc)
S.faction = faction.Copy()
S.master_commander = master_commander
if(HAS_TRAIT(src, TRAIT_XENOBIO_SPAWNED))
diff --git a/code/modules/antagonists/changeling/powers/summon_spiders.dm b/code/modules/antagonists/changeling/powers/summon_spiders.dm
index d36f1fe9b5b..c332a660742 100644
--- a/code/modules/antagonists/changeling/powers/summon_spiders.dm
+++ b/code/modules/antagonists/changeling/powers/summon_spiders.dm
@@ -1,6 +1,6 @@
-#define IDLE_AGGRESSIVE 0
-#define FOLLOW_AGGRESSIVE 1
-#define FOLLOW_RETALIATE 2
+#define FOLLOW_AGGRESSIVE 0
+#define FOLLOW_RETALIATE 1
+#define IDLE_AGGRESSIVE 2
#define IDLE_RETALIATE 3
/datum/action/changeling/spiders
@@ -30,9 +30,11 @@
if(do_after(user, 4 SECONDS, FALSE, target = user, hidden = TRUE)) // Takes 4 seconds to spawn a spider
spider_counter++
user.visible_message("[user] vomits up an arachnid!")
- var/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/S = new(user.loc)
+ var/mob/living/basic/giant_spider/hunter/infestation_spider/S = new(user.loc)
S.owner_UID = user.UID()
- S.faction |= list("spiders", "\ref[owner]") // Makes them friendly only to the owner & other spiders
+ S.ai_controller.set_blackboard_key(BB_CURRENT_PET_TARGET, user)
+ S.ai_controller.set_blackboard_key(BB_CHANGELING_SPIDER_ORDER, IDLE_AGGRESSIVE)
+ S.befriend(user)
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
is_operating = FALSE
return TRUE
@@ -40,26 +42,28 @@
return FALSE
/// Child of giant_spider because this should do everything the spider does and more
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider
+/mob/living/basic/giant_spider/hunter/infestation_spider
/// References to the owner changeling
var/mob/owner_UID
/// Handles the spider's behavior
var/current_order = IDLE_AGGRESSIVE
- var/list/enemies = list()
sentience_type = SENTIENCE_OTHER
venom_per_bite = 3
- speak_chance = 0
- wander = 0
gold_core_spawnable = NO_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/giant_spider/changeling
/// To check and gib the spider when dead, then remove only one of the counter for the changeling owner
var/gibbed = FALSE
+/mob/living/basic/giant_spider/hunter/infestation_spider/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/ai_retaliate)
+
//These two below are needed to both gib the spider always, and even if it was gibbed only remove 1 from the counter of spider_counter instead of death's gib calling death again and removing 2
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/gib()
+/mob/living/basic/giant_spider/hunter/infestation_spider/gib()
gibbed = TRUE
return ..()
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/death(gibbed)
+/mob/living/basic/giant_spider/hunter/infestation_spider/death(gibbed)
var/mob/owner_mob = locateUID(owner_UID)
if(!ismob(owner_mob))
return ..(TRUE)
@@ -71,7 +75,7 @@
gib()
return ..(TRUE)
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/examine(mob/user)
+/mob/living/basic/giant_spider/hunter/infestation_spider/examine(mob/user)
. = ..()
if(user.UID() != owner_UID)
return
@@ -85,113 +89,37 @@
if(IDLE_RETALIATE)
. += "The giant spider will remain idle and calm, only attacking if it is attacked."
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/AltShiftClick(mob/user)
+/mob/living/basic/giant_spider/hunter/infestation_spider/AltShiftClick(mob/user)
. = ..()
if(user.UID() != owner_UID)
return
+ var/list/possible_orders = list("Escort", "Escort Aggressively", "Idle", "Idle Aggressively")
+ var/new_order = tgui_input_list(user, "How do you want your spiders to behave?", "Spider Orders", possible_orders)
+ switch(new_order)
+ if("Escort")
+ current_order = FOLLOW_RETALIATE
+ if("Escort Aggressively")
+ current_order = FOLLOW_AGGRESSIVE
+ if("Idle")
+ current_order = IDLE_RETALIATE
+ if("Idle Aggressively")
+ current_order = IDLE_AGGRESSIVE
spider_order(user)
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/proc/spider_order(mob/user)
- enemies = list()
+/mob/living/basic/giant_spider/hunter/infestation_spider/proc/spider_order(mob/user)
switch(current_order)
- if(IDLE_AGGRESSIVE)
+ if(FOLLOW_AGGRESSIVE)
to_chat(user, "We order the giant spider to follow us but attack anyone on sight.")
- current_order = FOLLOW_AGGRESSIVE
- if(FOLLOW_AGGRESSIVE)
+ ai_controller.set_blackboard_key(BB_CHANGELING_SPIDER_ORDER, FOLLOW_AGGRESSIVE)
+ if(FOLLOW_RETALIATE)
to_chat(user, "We order the giant spider to follow us and to remain calm, only attacking if it is attacked.")
- current_order = FOLLOW_RETALIATE
- if(FOLLOW_RETALIATE)
+ ai_controller.set_blackboard_key(BB_CHANGELING_SPIDER_ORDER, FOLLOW_RETALIATE)
+ if(IDLE_RETALIATE)
to_chat(user, "We order the giant spider to remain idle and calm, only attacking if it is attacked.")
- current_order = IDLE_RETALIATE
- if(IDLE_RETALIATE)
- to_chat(user, "We order the giant spider to remain idle, but ready to attack anyone on sight.")
- current_order = IDLE_AGGRESSIVE
- handle_automated_movement()
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/handle_automated_movement() //Hacky and ugly.
- . = ..()
- var/list/around = view(src, vision_range)
- switch(current_order)
+ ai_controller.set_blackboard_key(BB_CHANGELING_SPIDER_ORDER, IDLE_RETALIATE)
if(IDLE_AGGRESSIVE)
- Find_Enemies(around)
- GLOB.move_manager.stop_looping(src)
- if(FOLLOW_AGGRESSIVE)
- Find_Enemies(around)
- for(var/mob/living/carbon/C in around)
- if(!faction_check_mob(C))
- continue
- if(Adjacent(C))
- return TRUE
- Goto(C, 0.5 SECONDS, 1)
- if(FOLLOW_RETALIATE)
- for(var/mob/living/carbon/C in around)
- if(!faction_check_mob(C))
- continue
- if(Adjacent(C))
- return TRUE
- Goto(C, 0.5 SECONDS, 1)
- if(IDLE_RETALIATE)
- GLOB.move_manager.stop_looping(src)
-
- for(var/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/H in around)
- if(faction_check_mob(H) && !attack_same && !H.attack_same)
- H.enemies |= enemies
-
- return TRUE
-
-// Bellow is the way the spiders react and retaliate when in an idle/aggresive mode.
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/ListTargets()
- if(!length(enemies))
- return list()
- var/list/see = ..()
- see &= enemies // Remove all entries that aren't in enemies
- return see
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/proc/Find_Enemies(around)
- enemies = list() // Reset enemies list, only focus on the ones around you, spiders don't have grudges
- for(var/mob/living/A in around)
- if(A == src)
- continue
- if(!isliving(A))
- continue
- var/mob/living/M = A
- if(!faction_check_mob(M))
- enemies |= M
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/attack_by(obj/item/W, mob/living/user, params)
- if(..())
- return FINISH_ATTACK
- if(W.force == 0)
- return FINISH_ATTACK
- if(!faction_check_mob(user))
- enemies |= user
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/bullet_act(obj/item/projectile/P)
- . = ..()
- if(!faction_check_mob(P.firer))
- enemies |= P.firer
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/attack_alien(mob/living/carbon/alien/user)
- . = ..()
- if(user.a_intent == INTENT_HELP)
- return
- if(!faction_check_mob(user))
- enemies |= user
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/attack_animal(mob/living/simple_animal/M)
- . = ..()
- if(M.a_intent == INTENT_HELP)
- return
- if(!faction_check_mob(M))
- enemies |= M
-
-/mob/living/simple_animal/hostile/poison/giant_spider/hunter/infestation_spider/attack_hand(mob/living/carbon/human/H)
- . = ..()
- if(H.a_intent == INTENT_HELP)
- return
- if(!faction_check_mob(H))
- enemies |= H
+ to_chat(user, "We order the giant spider to remain idle, but ready to attack anyone on sight.")
+ ai_controller.set_blackboard_key(BB_CHANGELING_SPIDER_ORDER, IDLE_AGGRESSIVE)
#undef IDLE_AGGRESSIVE
#undef FOLLOW_AGGRESSIVE
diff --git a/code/modules/events/spider_infestation.dm b/code/modules/events/spider_infestation.dm
index bcdee1f840e..03ef7c95e1a 100644
--- a/code/modules/events/spider_infestation.dm
+++ b/code/modules/events/spider_infestation.dm
@@ -19,6 +19,6 @@
var/obj/vent = pick_n_take(vents)
var/obj/structure/spider/spiderling/S = new(vent.loc)
if(prob(66))
- S.grow_as = /mob/living/simple_animal/hostile/poison/giant_spider/nurse
+ S.grow_as = /mob/living/basic/giant_spider/nurse
spawncount--
successSpawn = TRUE
diff --git a/code/modules/hydroponics/hydroponics_tray.dm b/code/modules/hydroponics/hydroponics_tray.dm
index 856ca6acd7b..8e125c69bf9 100644
--- a/code/modules/hydroponics/hydroponics_tray.dm
+++ b/code/modules/hydroponics/hydroponics_tray.dm
@@ -501,7 +501,7 @@
visible_message("The pests seem to behave oddly...")
for(var/i in 1 to 3)
var/obj/structure/spider/spiderling/S = new(get_turf(src))
- S.grow_as = /mob/living/simple_animal/hostile/poison/giant_spider/hunter
+ S.grow_as = /mob/living/basic/giant_spider/hunter
else
to_chat(user, "The pests seem to behave oddly, but quickly settle down...")
diff --git a/code/modules/mob/living/basic/hostile/giant_spider/giant_spider.dm b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider.dm
new file mode 100644
index 00000000000..925af4ab9f1
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider.dm
@@ -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("[src] begins to secrete a sticky substance.")
+ 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, "No suitable dead prey or wrappable objects found nearby.")
+ return
+
+ if(cocoon_target)
+ visible_message("[src] begins to secrete a sticky substance around [cocoon_target].")
+ 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("[src] sticks a proboscis into [L] and sucks a viscous substance out.")
+
+ 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, "There is already a cluster of eggs here!")
+ return
+ if(!fed)
+ to_chat(src, "You are too hungry to do this!")
+ return
+ visible_message("[src] begins to lay a cluster of eggs.")
+ 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!")
diff --git a/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_actions.dm b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_actions.dm
new file mode 100644
index 00000000000..1ef4cb83d1e
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_actions.dm
@@ -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()
diff --git a/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_ai.dm b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_ai.dm
new file mode 100644
index 00000000000..18aa39411cf
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/giant_spider/giant_spider_ai.dm
@@ -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
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
deleted file mode 100644
index 2c1d98f0be6..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ /dev/null
@@ -1,283 +0,0 @@
-
-//basic spider mob, these generally guard nests
-/mob/living/simple_animal/hostile/poison/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")
- emote_hear = list("chitters")
- speak_chance = 5
- turns_per_move = 5
- see_in_dark = 8
- lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE
- footstep_type = FOOTSTEP_MOB_CLAW
- butcher_results = list(/obj/item/food/monstermeat/spidermeat = 2, /obj/item/food/monstermeat/spiderleg = 8)
- response_help = "pets"
- response_disarm = "gently pushes aside"
- maxHealth = 200
- health = 200
- obj_damage = 60
- melee_damage_lower = 15
- melee_damage_upper = 20
- heat_damage_per_tick = 20 //amount of damage applied if animal's body temperature is higher than maxbodytemp
- cold_damage_per_tick = 20 //same as heat_damage_per_tick, only if the bodytemperature it's lower than minbodytemp
- faction = list("spiders")
- pass_flags = PASSTABLE
- move_to_delay = 6
- attacktext = "bites"
- attack_sound = 'sound/weapons/bite.ogg'
- gold_core_spawnable = HOSTILE_SPAWN
- var/venom_per_bite = 0 // While the /poison/ type path remains as-is for consistency reasons, we're really talking about venom, not poison.
- contains_xeno_organ = TRUE
- surgery_container = /datum/xenobiology_surgery_container/spider
-
-/mob/living/simple_animal/hostile/poison/giant_spider/Initialize(mapload)
- . = ..()
- var/datum/action/innate/web_giant_spider/web_action = new()
- web_action.Grant(src)
-
-/mob/living/simple_animal/hostile/poison/giant_spider/Destroy()
- for(var/datum/action/innate/web_giant_spider/web_action in actions)
- web_action.Remove(src)
- return ..()
-
-/mob/living/simple_animal/hostile/poison/giant_spider/AttackingTarget()
- // This is placed here, NOT on /poison, because the other subtypes of /poison/ already override AttackingTarget() completely, and as such it would do nothing but confuse people there.
- . = ..()
- 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/simple_animal/hostile/poison/giant_spider/get_spacemove_backup()
- . = ..()
- // 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
-
-//nursemaids - these create webs and eggs
-/mob/living/simple_animal/hostile/poison/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)
-
- maxHealth = 40
- health = 40
- melee_damage_lower = 5
- melee_damage_upper = 10
- gold_core_spawnable = NO_SPAWN
- venom_per_bite = 30
- var/atom/cocoon_target
- var/fed = 0
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Initialize(mapload)
- . = ..()
- var/datum/action/innate/wrap_giant_spider/wrap_action = new()
- wrap_action.Grant(src)
- var/datum/action/innate/lay_eggs_giant_spider/egg_action = new()
- egg_action.Grant(src)
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Destroy()
- for(var/datum/action/innate/wrap_giant_spider/wrap_action in actions)
- wrap_action.Remove(src)
- for(var/datum/action/innate/lay_eggs_giant_spider/egg_action in actions)
- egg_action.Remove(src)
- return ..()
-
-//hunters have the most poison and move the fastest, so they can find prey
-/mob/living/simple_animal/hostile/poison/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
- move_to_delay = 5
-
-/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_movement() //Hacky and ugly.
- . = ..()
- if(AIStatus == AI_IDLE)
- //1% chance to skitter madly away
- if(prob(1))
- stop_automated_movement = TRUE
- Goto(pick(urange(20, src, 1)), move_to_delay)
- spawn(50)
- stop_automated_movement = FALSE
- walk(src,0)
- return 1
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/GiveUp(C)
- spawn(100)
- if(cocoon_target == C && get_dist(src, cocoon_target) > 1)
- cocoon_target = null
- stop_automated_movement = FALSE
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_movement() //Hacky and ugly.
- if(..())
- var/list/can_see = view(src, 10)
- if(prob(30)) //30% chance to stop wandering and do something
- //first, check for potential food nearby to cocoon
- for(var/mob/living/C in can_see)
- if(C.stat && !istype(C, /mob/living/simple_animal/hostile/poison/giant_spider) && !C.anchored)
- cocoon_target = C
- Goto(C, move_to_delay)
- //give up if we can't reach them after 10 seconds
- GiveUp(C)
- return
- //second, spin a sticky spiderweb on this tile
- var/obj/structure/spider/stickyweb/W = locate() in get_turf(src)
- if(!W)
- create_web()
- else
- //third, lay an egg cluster there
- if(fed)
- lay_spider_eggs()
- else
- //fourthly, cocoon any nearby items so those pesky pinkskins can't use them
- for(var/obj/O in can_see)
- if(O.anchored)
- continue
-
- if(isitem(O) || isstructure(O) || ismachinery(O))
- cocoon_target = O
- stop_automated_movement = TRUE
- Goto(O, move_to_delay)
- //give up if we can't reach them after 10 seconds
- GiveUp(O)
-
- else if(cocoon_target)
- if(get_dist(src, cocoon_target) <= 1)
- wrap_target()
-
- else
- stop_automated_movement = FALSE
-
-/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/simple_animal/hostile/poison/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/simple_animal/hostile/poison/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/simple_animal/hostile/poison/giant_spider/nurse/user = owner
- user.lay_spider_eggs()
-
-/mob/living/simple_animal/hostile/poison/giant_spider/proc/create_web()
- var/T = loc
-
- visible_message("[src] begins to secrete a sticky substance.")
- stop_automated_movement = TRUE
- if(!do_after_once(src, 4 SECONDS, target = loc, attempt_cancel_message = "You stop spinning a web."))
- return
- new /obj/structure/spider/stickyweb(T)
- stop_automated_movement = FALSE
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/wrap_target()
- if(!cocoon_target)
- 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/simple_animal/hostile/poison/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, "No suitable dead prey or wrappable objects found nearby.")
- return
-
- if(cocoon_target)
- visible_message("[src] begins to secrete a sticky substance around [cocoon_target].")
- stop_automated_movement = TRUE
- walk(src, 0)
- if(!do_after_once(src, 5 SECONDS, target = cocoon_target, attempt_cancel_message = "You stop wrapping [cocoon_target]."))
- 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/simple_animal/hostile/poison/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("[src] sticks a proboscis into [L] and sucks a viscous substance out.")
-
- break
- if(large_cocoon)
- C.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3")
- cocoon_target = null
- stop_automated_movement = FALSE
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/lay_spider_eggs()
- var/obj/structure/spider/eggcluster/E = locate() in get_turf(src)
- if(E)
- to_chat(src, "There is already a cluster of eggs here!")
- return
- if(!fed)
- to_chat(src, "You are too hungry to do this!")
- return
- visible_message("[src] begins to lay a cluster of eggs.")
- stop_automated_movement = TRUE
- if(!do_after_once(src, 4 SECONDS, target = loc, attempt_cancel_message = "You stop laying eggs."))
- 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--
- stop_automated_movement = FALSE
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/araneus.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/araneus.dm
deleted file mode 100644
index cf9005914e1..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/araneus.dm
+++ /dev/null
@@ -1,21 +0,0 @@
-/mob/living/simple_animal/hostile/retaliate/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")
- icon_state = "guard"
- icon_living = "guard"
- icon_dead = "guard_dead"
- icon_gib = "guard_dead"
- mob_biotypes = MOB_ORGANIC | MOB_BUG
- turns_per_move = 8
- response_help = "pets"
- emote_hear = list("chitters")
- maxHealth = 250
- health = 250
- melee_damage_lower = 15
- melee_damage_upper = 20
- unique_pet = TRUE
- 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
-
diff --git a/code/modules/mob/mob_vars.dm b/code/modules/mob/mob_vars.dm
index 1b6ce0626dd..e3cebe253a1 100644
--- a/code/modules/mob/mob_vars.dm
+++ b/code/modules/mob/mob_vars.dm
@@ -251,4 +251,7 @@
/// Controls whether they can say some symbols.
var/speaks_ooc = FALSE
+ /// For storing what do_after's something has, key = string, value = amount of interactions of that type happening.
+ var/list/do_afters
+
new_attack_chain = TRUE
diff --git a/paradise.dme b/paradise.dme
index 78744d3a5a1..c06bc1522e0 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -638,6 +638,7 @@
#include "code\datums\elements\earhealing.dm"
#include "code\datums\elements\high_value_item.dm"
#include "code\datums\elements\hostile_machine.dm"
+#include "code\datums\elements\pet_bonus.dm"
#include "code\datums\elements\rad_insulation.dm"
#include "code\datums\elements\relay_attackers.dm"
#include "code\datums\elements\ridable.dm"
@@ -2417,6 +2418,9 @@
#include "code\modules\mob\living\basic\hostile\bear.dm"
#include "code\modules\mob\living\basic\hostile\carp.dm"
#include "code\modules\mob\living\basic\hostile\skeleton_mob.dm"
+#include "code\modules\mob\living\basic\hostile\giant_spider\giant_spider.dm"
+#include "code\modules\mob\living\basic\hostile\giant_spider\giant_spider_actions.dm"
+#include "code\modules\mob\living\basic\hostile\giant_spider\giant_spider_ai.dm"
#include "code\modules\mob\living\basic\hostile\gorilla\gorilla.dm"
#include "code\modules\mob\living\basic\hostile\gorilla\gorilla_emote.dm"
#include "code\modules\mob\living\basic\nether_mobs\blankbody.dm"
@@ -2631,7 +2635,6 @@
#include "code\modules\mob\living\simple_animal\hostile\deathsquid.dm"
#include "code\modules\mob\living\simple_animal\hostile\drakehound.dm"
#include "code\modules\mob\living\simple_animal\hostile\floorcluwne.dm"
-#include "code\modules\mob\living\simple_animal\hostile\giant_spider.dm"
#include "code\modules\mob\living\simple_animal\hostile\headslug.dm"
#include "code\modules\mob\living\simple_animal\hostile\hivebot.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
@@ -2666,7 +2669,6 @@
#include "code\modules\mob\living\simple_animal\hostile\mining\elites\herald.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining\elites\legionnaire.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining\elites\pandora.dm"
-#include "code\modules\mob\living\simple_animal\hostile\retaliate\araneus.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\combat_drone.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\fish.dm"
#include "code\modules\mob\living\simple_animal\hostile\retaliate\kangaroo.dm"
diff --git a/sound/creatures/chitter.ogg b/sound/creatures/chitter.ogg
new file mode 100644
index 00000000000..a4007bbf4e1
Binary files /dev/null and b/sound/creatures/chitter.ogg differ
diff --git a/tools/UpdatePaths/Scripts/29796_giant_spiders.txt b/tools/UpdatePaths/Scripts/29796_giant_spiders.txt
new file mode 100644
index 00000000000..4daa5b0ec81
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/29796_giant_spiders.txt
@@ -0,0 +1 @@
+/mob/living/simple_animal/hostile/poison/giant_spider/@SUBTYPES : /mob/living/basic/giant_spider/@SUBTYPES{@OLD}