From 8976c80adc588cde67968c4a6be05593e24e29d8 Mon Sep 17 00:00:00 2001 From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Date: Thu, 31 Jul 2025 02:08:18 -0400 Subject: [PATCH] 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> --- .../map_files/RandomRuins/SpaceRuins/ussp.dmm | 4 +- _maps/map_files/stations/boxstation.dmm | 2 +- _maps/map_files/stations/cerestation.dmm | 2 +- _maps/map_files/stations/deltastation.dmm | 2 +- _maps/map_files/stations/emeraldstation.dmm | 2 +- _maps/map_files/stations/metastation.dmm | 2 +- code/__DEFINES/ai/ai_defines.dm | 6 + code/__DEFINES/ai/blackboard_defines.dm | 14 + code/__DEFINES/dcs/mob_signals.dm | 5 + code/__HELPERS/mob_helpers.dm | 19 +- code/__HELPERS/trait_helpers.dm | 3 + code/_globalvars/traits.dm | 4 +- code/datums/ai/ai_controller.dm | 37 ++- code/datums/ai/basic_mobs/basic_controller.dm | 21 ++ .../ai/idle_behaviors/idle_random_walk.dm | 3 + code/datums/ai/movement/ai_movement.dm | 2 +- code/datums/elements/pet_bonus.dm | 33 ++ code/game/objects/effects/spiders.dm | 6 +- .../changeling/powers/summon_spiders.dm | 146 +++------ code/modules/events/spider_infestation.dm | 2 +- code/modules/hydroponics/hydroponics_tray.dm | 2 +- .../hostile/giant_spider/giant_spider.dm | 221 +++++++++++++ .../giant_spider/giant_spider_actions.dm | 26 ++ .../hostile/giant_spider/giant_spider_ai.dm | 292 ++++++++++++++++++ .../simple_animal/hostile/giant_spider.dm | 283 ----------------- .../hostile/retaliate/araneus.dm | 21 -- code/modules/mob/mob_vars.dm | 3 + paradise.dme | 6 +- sound/creatures/chitter.ogg | Bin 0 -> 6985 bytes .../Scripts/29796_giant_spiders.txt | 1 + 30 files changed, 732 insertions(+), 438 deletions(-) create mode 100644 code/datums/elements/pet_bonus.dm create mode 100644 code/modules/mob/living/basic/hostile/giant_spider/giant_spider.dm create mode 100644 code/modules/mob/living/basic/hostile/giant_spider/giant_spider_actions.dm create mode 100644 code/modules/mob/living/basic/hostile/giant_spider/giant_spider_ai.dm delete mode 100644 code/modules/mob/living/simple_animal/hostile/giant_spider.dm delete mode 100644 code/modules/mob/living/simple_animal/hostile/retaliate/araneus.dm create mode 100644 sound/creatures/chitter.ogg create mode 100644 tools/UpdatePaths/Scripts/29796_giant_spiders.txt 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 0000000000000000000000000000000000000000..a4007bbf4e122a96face5fb75d6eecc6537971a9 GIT binary patch literal 6985 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 jV_`vTEbF1Nh-VUA