From 42dd4877288a8476a2cd0f6ef9d936071b207d3d Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:00:22 +0100 Subject: [PATCH] [MIRROR] Station Trait: Spider Infestation [MDB IGNORE] (#19813) * Station Trait: Spider Infestation (#73893) ## About The Pull Request Hate having your cables eaten by mice? Nanotrasen have heard your complaints and settled on a natural, _organic_, and eco-friendly solution. When this station trait is active, roundstart and event mouse spawns have a chance to instead be replaced with duct spiders (both will exist, it doesn't remove mice). Duct spiders are largely harmless to humans, actively hunt other maintenance creatures (such as mice), and have only one _tiny_ downside. ![image](https://user-images.githubusercontent.com/7483112/224345781-2627be98-67f2-4cab-ac40-c6c9b35ea909.png) These mobs can also sometimes be spawned by a minor scrubber clog event. As a side note, all spider basic mobs with AI (except Araneus) will now try to automatically fill a small area around them with webs. Also I made it so that mobs will ignore their random_walking behaviour if they're engaged in a `do_after`, just in case. ## Why It's Good For The Game Adds a little bit of variety to things which can slightly annoy you in maintenance. Spiders will automatically make places they live in look like spiders live there. ## Changelog :cl: add: A station trait which sometimes populates maintenance with small spiders. You can wear them as a hat if you wanted to have a spider on your head for some reason. add: Spider mobs will automatically start webbing up their environment. /:cl: * Station Trait: Spider Infestation --------- Co-authored-by: Jacquerel --- code/__DEFINES/ai.dm | 15 ++- code/__DEFINES/traits.dm | 1 + code/controllers/subsystem/minor_mapping.dm | 13 ++- .../basic_mobs/basic_subtrees/flee_target.dm | 14 ++- .../basic_subtrees/target_retaliate.dm | 16 ++- .../basic_targetting_datum.dm | 28 ++++++ .../ai/idle_behaviors/idle_random_walk.dm | 2 + code/datums/elements/tiny_mob_hunter.dm | 31 ++++++ code/datums/station_traits/neutral_traits.dm | 7 ++ code/modules/events/scrubber_clog.dm | 3 +- .../space_fauna/giant_spider/giant_spider.dm | 1 + .../giant_spider/giant_spider_ai.dm | 27 +++++- .../giant_spider/spider_abilities/web.dm | 2 +- .../giant_spider/spider_subtrees.dm | 91 ++++++++++++++++++ .../giant_spider/spider_variants.dm | 67 ++++++++++--- code/modules/movespeed/modifiers/mobs.dm | 3 + icons/mob/clothing/head/pets_head.dmi | Bin 2188 -> 2417 bytes icons/mob/simple/animal.dmi | Bin 247316 -> 249261 bytes tgstation.dme | 2 + 19 files changed, 297 insertions(+), 26 deletions(-) create mode 100644 code/datums/elements/tiny_mob_hunter.dm create mode 100644 code/modules/mob/living/basic/space_fauna/giant_spider/spider_subtrees.dm diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index f6d1768d725..78dde256424 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -221,6 +221,11 @@ ///some behaviors that check current_target also set this on deep crit mobs #define BB_BASIC_MOB_EXECUTION_TARGET "BB_basic_execution_target" +///Targetting keys for something to run away from, if you need to store this separately from current target +#define BB_BASIC_MOB_FLEE_TARGET "BB_basic_flee_target" +#define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location" +#define BB_FLEE_TARGETTING_DATUM "flee_targetting_datum" + ///List of mobs who have damaged us #define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist" @@ -238,12 +243,18 @@ ///Current partner target #define BB_BABIES_TARGET "BB_babies_target" -///Bileworm AI keys +// Bileworm AI keys #define BB_BILEWORM_SPEW_BILE "BB_bileworm_spew_bile" #define BB_BILEWORM_RESURFACE "BB_bileworm_resurface" #define BB_BILEWORM_DEVOUR "BB_bileworm_devour" -/// Fugu AI Keys +// Spider AI keys +/// Key where we store a turf to put webs on +#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" +// Fugu AI keys +/// Key where we store the inflating ability #define BB_FUGU_INFLATE "BB_fugu_inflate" diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 4ddf1d4b31f..98f0f3b3daa 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -940,6 +940,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define STATION_TRAIT_BIGGER_PODS "station_trait_bigger_pods" #define STATION_TRAIT_SMALLER_PODS "station_trait_smaller_pods" #define STATION_TRAIT_BIRTHDAY "station_trait_birthday" +#define STATION_TRAIT_SPIDER_INFESTATION "station_trait_spider_infestation" ///From the market_crash event #define MARKET_CRASH_EVENT_TRAIT "crashed_market_event" diff --git a/code/controllers/subsystem/minor_mapping.dm b/code/controllers/subsystem/minor_mapping.dm index 5b2c17cf802..73371932e04 100644 --- a/code/controllers/subsystem/minor_mapping.dm +++ b/code/controllers/subsystem/minor_mapping.dm @@ -1,4 +1,5 @@ #define PROB_MOUSE_SPAWN 98 +#define PROB_SPIDER_REPLACEMENT 50 SUBSYSTEM_DEF(minor_mapping) name = "Minor Mapping" @@ -13,15 +14,20 @@ SUBSYSTEM_DEF(minor_mapping) place_satchels() return SS_INIT_SUCCESS -/datum/controller/subsystem/minor_mapping/proc/trigger_migration(num_mice=10) +/// Spawns some critters on exposed wires, usually but not always mice +/datum/controller/subsystem/minor_mapping/proc/trigger_migration(to_spawn=10) var/list/exposed_wires = find_exposed_wires() var/turf/open/proposed_turf - while((num_mice > 0) && exposed_wires.len) + while((to_spawn > 0) && exposed_wires.len) proposed_turf = pick_n_take(exposed_wires) if (!valid_mouse_turf(proposed_turf)) continue - num_mice-- + to_spawn-- + if(HAS_TRAIT(SSstation, STATION_TRAIT_SPIDER_INFESTATION) && prob(PROB_SPIDER_REPLACEMENT)) + new /mob/living/basic/giant_spider/maintenance(proposed_turf) + return + if (prob(PROB_MOUSE_SPAWN)) new /mob/living/basic/mouse(proposed_turf) else @@ -74,3 +80,4 @@ SUBSYSTEM_DEF(minor_mapping) return shuffle(suitable) #undef PROB_MOUSE_SPAWN +#undef PROB_SPIDER_REPLACEMENT diff --git a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm index ef72743a5f5..f62bb5699c7 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm @@ -2,14 +2,24 @@ /datum/ai_planning_subtree/flee_target /// Behaviour to execute in order to flee var/flee_behaviour = /datum/ai_behavior/run_away_from_target + /// Blackboard key in which to store selected target + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// Blackboard key in which to store selected target's hiding place + var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION /datum/ai_planning_subtree/flee_target/SelectBehaviors(datum/ai_controller/controller, delta_time) . = ..() if (!controller.blackboard[BB_BASIC_MOB_FLEEING]) return - var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/datum/weakref/weak_target = controller.blackboard[target_key] var/atom/target = weak_target?.resolve() if(!target || QDELETED(target)) return - controller.queue_behavior(flee_behaviour, BB_BASIC_MOB_CURRENT_TARGET, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + controller.queue_behavior(flee_behaviour, target_key, hiding_place_key) return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here. + +/// Try to escape from your current target, without performing any other actions. +/// Reads from some fleeing-specific targetting keys rather than the current mob target. +/datum/ai_planning_subtree/flee_target/from_flee_key + target_key = BB_BASIC_MOB_FLEE_TARGET + hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm index ff92435765e..2fb4dc858f4 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm @@ -1,9 +1,23 @@ /// Sets the BB target to a mob which you can see and who has recently attacked you /datum/ai_planning_subtree/target_retaliate + /// Blackboard key which tells us how to select valid targets + var/targetting_datum_key = BB_TARGETTING_DATUM + /// Blackboard key in which to store selected target + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// Blackboard key in which to store selected target's hiding place + var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION /datum/ai_planning_subtree/target_retaliate/SelectBehaviors(datum/ai_controller/controller, delta_time) . = ..() - controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targetting_datum_key, hiding_place_key) + +/// Places a mob which you can see and who has recently attacked you into some 'run away from this' AI keys +/// Can use a different targetting datum than you use to select attack targets +/// Not required if fleeing is the only target behaviour or uses the same target datum +/datum/ai_planning_subtree/target_retaliate/to_flee + targetting_datum_key = BB_FLEE_TARGETTING_DATUM + target_key = BB_BASIC_MOB_FLEE_TARGET + hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION /** * Picks a target from a provided list of atoms who have been pissing you off diff --git a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm index db93c68a7e8..004f3d8f59b 100644 --- a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm +++ b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm @@ -82,3 +82,31 @@ /datum/targetting_datum/basic/ignore_faction/faction_check(mob/living/living_mob, mob/living/the_target) return FALSE + +/// Subtype which searches for mobs of a size relative to ours +/datum/targetting_datum/basic/of_size + /// If true, we will return mobs which are smaller than us. If false, larger. + var/find_smaller = TRUE + /// If true, we will return mobs which are the same size as us. + var/inclusive = TRUE + +/datum/targetting_datum/basic/of_size/can_attack(mob/living/owner, atom/target) + if(!isliving(target)) + return FALSE + . = ..() + if(!.) + return FALSE + + var/mob/living/mob_target = target + if(inclusive && owner.mob_size == mob_target.mob_size) + return TRUE + if(owner.mob_size > mob_target.mob_size) + return find_smaller + return !find_smaller + +// This is just using the default values but the subtype makes it clearer +/datum/targetting_datum/basic/of_size/ours_or_smaller + +/datum/targetting_datum/basic/of_size/larger + find_smaller = FALSE + inclusive = FALSE diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm index 69a236ba831..a8e3a81d128 100644 --- a/code/datums/ai/idle_behaviors/idle_random_walk.dm +++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm @@ -5,6 +5,8 @@ /datum/idle_behavior/idle_random_walk/perform_idle_behavior(delta_time, datum/ai_controller/controller) . = ..() var/mob/living/living_pawn = controller.pawn + if(LAZYLEN(living_pawn.do_afters)) + return if(DT_PROB(walk_chance, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) var/move_dir = pick(GLOB.alldirs) diff --git a/code/datums/elements/tiny_mob_hunter.dm b/code/datums/elements/tiny_mob_hunter.dm new file mode 100644 index 00000000000..b2a12e7e1ef --- /dev/null +++ b/code/datums/elements/tiny_mob_hunter.dm @@ -0,0 +1,31 @@ +/// Deals bonus brute damage to smaller mobs +/datum/element/tiny_mob_hunter + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// Will apply bonus to mobs this size or smaller + var/target_size + /// Additional damage to apply + var/bonus_damage + +/datum/element/tiny_mob_hunter/Attach(datum/target, target_size = MOB_SIZE_TINY, bonus_damage = 10) + . = ..() + if(!isanimal_or_basicmob(target)) // No post-attack signal for carbons, you can add one if you really want to put this on one + return ELEMENT_INCOMPATIBLE + + src.target_size = target_size + src.bonus_damage = bonus_damage + RegisterSignal(target, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_attacked_target)) + +/datum/element/tiny_mob_hunter/Detach(datum/target) + UnregisterSignal(target, COMSIG_HOSTILE_POST_ATTACKINGTARGET) + return ..() + +/// Applies a bonus following the attack +/datum/element/tiny_mob_hunter/proc/on_attacked_target(mob/living/hunter, atom/target) + SIGNAL_HANDLER + if (!isliving(target)) + return + var/mob/living/prey = target + if (prey.mob_size > target_size) + return + prey.apply_damage(bonus_damage, BRUTE, hunter.zone_selected) diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm index 10a9ca4412b..4ad5bc8f5f7 100644 --- a/code/datums/station_traits/neutral_traits.dm +++ b/code/datums/station_traits/neutral_traits.dm @@ -16,6 +16,13 @@ // This station trait modifies the atmosphere, which is too far past the time admins are able to revert it can_revert = FALSE +/datum/station_trait/spider_infestation + name = "Spider Infestation" + trait_type = STATION_TRAIT_NEUTRAL + weight = 5 + report_message = "We have introduced a natural countermeasure to reduce the number of rodents on board your station." + trait_to_give = STATION_TRAIT_SPIDER_INFESTATION + /datum/station_trait/unique_ai name = "Unique AI" trait_type = STATION_TRAIT_NEUTRAL diff --git a/code/modules/events/scrubber_clog.dm b/code/modules/events/scrubber_clog.dm index 4e982ff05b5..aaa80b58705 100644 --- a/code/modules/events/scrubber_clog.dm +++ b/code/modules/events/scrubber_clog.dm @@ -65,8 +65,9 @@ /datum/round_event/scrubber_clog/proc/get_mob() var/static/list/mob_list = list( - /mob/living/basic/mouse, /mob/living/basic/cockroach, + /mob/living/basic/giant_spider/maintenance, + /mob/living/basic/mouse, /mob/living/simple_animal/butterfly, ) return pick(mob_list) diff --git a/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider.dm b/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider.dm index 7cef5c6bc69..81fc08af989 100644 --- a/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider.dm +++ b/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider.dm @@ -73,6 +73,7 @@ var/datum/action/cooldown/lay_web/webbing = new web_type(src) webbing.webbing_time *= web_speed webbing.Grant(src) + ai_controller.blackboard[BB_SPIDER_WEB_ACTION] = WEAKREF(webbing) /mob/living/basic/giant_spider/Login() . = ..() diff --git a/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider_ai.dm b/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider_ai.dm index ca7b8cce21c..6838f3ba1ab 100644 --- a/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider_ai.dm +++ b/code/modules/mob/living/basic/space_fauna/giant_spider/giant_spider_ai.dm @@ -1,4 +1,4 @@ -/// For now, essentially just a Simple Hostile but room for expansion +/// 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_TARGETTING_DATUM = new /datum/targetting_datum/basic(), @@ -13,6 +13,8 @@ /datum/ai_planning_subtree/attack_obstacle_in_path, /datum/ai_planning_subtree/basic_melee_attack_subtree, /datum/ai_planning_subtree/random_speech/insect, // Space spiders are taxonomically insects not arachnids, don't DM me + /datum/ai_planning_subtree/find_unwebbed_turf, + /datum/ai_planning_subtree/spin_web, ) /// Giant spider which won't attack structures @@ -21,9 +23,11 @@ /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree, /datum/ai_planning_subtree/random_speech/insect, + /datum/ai_planning_subtree/find_unwebbed_turf, + /datum/ai_planning_subtree/spin_web, ) -/// Used by Araneus, who only attacks those who attack first +/// 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 blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(), @@ -35,3 +39,22 @@ /datum/ai_planning_subtree/basic_melee_attack_subtree, /datum/ai_planning_subtree/random_speech/insect, ) + +/// Retaliates, hunts other maintenance creatures, runs away from larger attackers, and spins webs. +/datum/ai_controller/basic_controller/giant_spider/pest + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/ours_or_smaller(), // Hunt mobs our size + BB_FLEE_TARGETTING_DATUM = new /datum/targetting_datum/basic/of_size/larger(), // Run away from mobs bigger than we are + BB_BASIC_MOB_FLEEING = TRUE, + ) + idle_behavior = /datum/idle_behavior/idle_random_walk + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate/to_flee, + /datum/ai_planning_subtree/flee_target/from_flee_key, + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/random_speech/insect, + /datum/ai_planning_subtree/find_unwebbed_turf, + /datum/ai_planning_subtree/spin_web, + ) diff --git a/code/modules/mob/living/basic/space_fauna/giant_spider/spider_abilities/web.dm b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_abilities/web.dm index 45e44a3e4f1..2ac43bb5487 100644 --- a/code/modules/mob/living/basic/space_fauna/giant_spider/spider_abilities/web.dm +++ b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_abilities/web.dm @@ -55,7 +55,7 @@ if(do_after(owner, webbing_time, target = spider_turf, interaction_key = DOAFTER_SOURCE_SPIDER) && owner.loc == spider_turf) plant_web(spider_turf, web) else - owner.balloon_alert(owner, "interrupted!") + owner?.balloon_alert(owner, "interrupted!") // Null check because we might have been interrupted via being disintegrated build_all_button_icons() /// Creates a web in the current turf diff --git a/code/modules/mob/living/basic/space_fauna/giant_spider/spider_subtrees.dm b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_subtrees.dm new file mode 100644 index 00000000000..dd11ceef92c --- /dev/null +++ b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_subtrees.dm @@ -0,0 +1,91 @@ +/// Search for a nearby location to put webs on +/datum/ai_planning_subtree/find_unwebbed_turf + +/datum/ai_planning_subtree/find_unwebbed_turf/SelectBehaviors(datum/ai_controller/controller, delta_time) + 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(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/spider = controller.pawn + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/current_target = weak_target?.resolve() + if (current_target && !(locate(/obj/structure/spider/stickyweb) in current_target)) + finish_action(controller, succeeded = FALSE) // Already got a target + return + + controller.blackboard[target_key] = null + var/turf/our_turf = get_turf(spider) + if (is_valid_web_turf(our_turf)) + controller.blackboard[target_key] = WEAKREF(our_turf) + finish_action(controller, succeeded = TRUE) + return + + var/list/potential_turfs = list() + for(var/turf/turf_in_view in oview(scan_range, our_turf)) + if (!is_valid_web_turf(turf_in_view)) + continue + potential_turfs += turf_in_view + + if (!length(potential_turfs)) + finish_action(controller, succeeded = FALSE) + return + + controller.blackboard[target_key] = WEAKREF(get_closest_atom(/turf/, potential_turfs, our_turf)) + finish_action(controller, succeeded = TRUE) + +/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 + 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/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/datum/weakref/weak_action = controller.blackboard[action_key] + var/datum/action/cooldown/using_action = weak_action?.resolve() + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/turf/target_turf = weak_target?.resolve() + if (!using_action || !target_turf) + return + 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 = 15 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/weakref/weak_action = controller.blackboard[action_key] + var/datum/action/cooldown/web_action = weak_action?.resolve() + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/turf/target_turf = weak_target?.resolve() + if (!web_action || !target_turf) + return FALSE + + set_movement_target(controller, target_turf) + return ..() + +/datum/ai_behavior/spin_web/perform(delta_time, datum/ai_controller/controller, action_key, target_key) + . = ..() + var/datum/weakref/weak_action = controller.blackboard[action_key] + var/datum/action/cooldown/web_action = weak_action?.resolve() + finish_action(controller, succeeded = web_action?.Trigger(), action_key = action_key, target_key = target_key) + +/datum/ai_behavior/spin_web/finish_action(datum/ai_controller/controller, succeeded, action_key, target_key) + controller.blackboard[target_key] = null + return ..() diff --git a/code/modules/mob/living/basic/space_fauna/giant_spider/spider_variants.dm b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_variants.dm index 67493cd1aed..e82c04f221e 100644 --- a/code/modules/mob/living/basic/space_fauna/giant_spider/spider_variants.dm +++ b/code/modules/mob/living/basic/space_fauna/giant_spider/spider_variants.dm @@ -1,5 +1,5 @@ /** - * # Spider Hunter + * ### Spider Hunter * A subtype of the giant spider which is faster, has toxin injection, but less health and damage. * This spider is only slightly slower than a human. */ @@ -19,7 +19,7 @@ menu_description = "Fast spider variant specializing in catching running prey and toxin injection, but has less health and damage." /** - * # Spider Nurse + * ### Spider Nurse * * A subtype of the giant spider which specializes in support skills. * Nurses can place down webbing in a quarter of the time that other species can and can wrap other spiders' wounds, healing them. @@ -56,7 +56,7 @@ ) /** - * # Tarantula + * ### Tarantula * * A subtype of the giant spider which specializes in pure strength and staying power. * Is slowed down when not on webbing, but can lunge to throw off attackers and possibly to stun them. @@ -97,7 +97,7 @@ charge.Trigger(target = atom_target) /** - * # Spider Viper + * ### Spider Viper * * A subtype of the giant spider which specializes in speed and poison. * Injects a deadlier toxin than other spiders, moves extremely fast, but has a limited amount of health. @@ -120,7 +120,7 @@ menu_description = "Assassin spider variant with an unmatched speed and very deadly poison, but has very low amount of health and damage." /** - * # Spider Broodmother + * ### Spider Broodmother * * A subtype of the giant spider which is the crux of a spider horde, and the way which it grows. * Has very little offensive capabilities but can lay eggs at any time to create more basic spiders. @@ -161,7 +161,7 @@ not_hivemind_talk.Grant(src) /** - * # Giant Ice Spider + * ### Giant Ice Spider * * A subtype of the giant spider which is immune to temperature damage, unlike its normal counterpart. * Currently unused in the game unless spawned by admins. @@ -176,7 +176,7 @@ menu_description = "Versatile ice spider variant for frontline combat with high health and damage. Immune to temperature damage." /** - * # Ice Nurse Spider + * ### Ice Nurse Spider * * A temperature-proof nurse spider. Also unused. */ @@ -190,7 +190,7 @@ menu_description = "Support ice spider variant specializing in healing their brethren and placing webbings very swiftly, but has very low amount of health and deals low damage. Immune to temperature damage." /** - * # Ice Hunter Spider + * ### Ice Hunter Spider * * A temperature-proof hunter with chilling venom. Also unused. */ @@ -205,7 +205,7 @@ menu_description = "Fast ice spider variant specializing in catching running prey and frost oil injection, but has less health and damage. Immune to temperature damage." /** - * # Scrawny Hunter Spider + * ### Scrawny Hunter Spider * * A hunter spider that trades damage for health, unable to smash enviroments. * Used as a minor threat in abandoned places, such as areas in maintenance or a ruin. @@ -221,7 +221,7 @@ ai_controller = /datum/ai_controller/basic_controller/giant_spider/weak /** - * # Scrawny Tarantula + * ### Scrawny Tarantula * * A weaker version of the Tarantula, unable to smash enviroments. * Used as a moderately strong but slow threat in abandoned places, such as areas in maintenance or a ruin. @@ -237,7 +237,7 @@ ai_controller = /datum/ai_controller/basic_controller/giant_spider/weak /** - * # Scrawny Nurse Spider + * ### Scrawny Nurse Spider * * A weaker version of the nurse spider with reduced health, unable to smash enviroments. * Mainly used as a weak threat in abandoned places, such as areas in maintenance or a ruin. @@ -252,7 +252,7 @@ ai_controller = /datum/ai_controller/basic_controller/giant_spider/weak /** - * # Flesh Spider + * ### Flesh Spider * * A subtype of giant spider which only occurs from changelings. * Has the base stats of a hunter, but they can heal themselves and spin webs faster. @@ -291,7 +291,7 @@ return TRUE /** - * # Viper Spider (Wizard) + * ### Viper Spider (Wizard) * * A spider form for wizards. Has the viper spider's extreme speed and strong venom, with additional health and vent crawling abilities. */ @@ -306,7 +306,7 @@ ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) /** - * # Sergeant Araneus + * ### Sergeant Araneus * * This friendly arachnid hangs out in the HoS office on some space stations. Better trained than an average officer and does not attack except in self-defence. */ @@ -327,3 +327,42 @@ AddElement(/datum/element/pet_bonus, "chitters proudly!") AddElement(/datum/element/ai_retaliate) ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) + +/** + * ### Duct Spider + * + * A less giant spider which lives in the maintenance ducts and makes them annoying to traverse. + */ +/mob/living/basic/giant_spider/maintenance + name = "duct spider" + desc = "Nanotrasen's imported solution to mice, comes with its own problems." + icon_state = "maint_spider" + icon_living = "maint_spider" + icon_dead = "maint_spider_dead" + can_be_held = TRUE + mob_size = MOB_SIZE_TINY + held_w_class = WEIGHT_CLASS_TINY + worn_slot_flags = ITEM_SLOT_HEAD + head_icon = 'icons/mob/clothing/head/pets_head.dmi' + density = FALSE + pass_flags = PASSTABLE|PASSGRILLE|PASSMOB + gold_core_spawnable = FRIENDLY_SPAWN + maxHealth = 10 + health = 10 + melee_damage_lower = 1 + melee_damage_upper = 1 + speed = 0 + player_speed_modifier = 0 + web_speed = 0.25 + menu_description = "Fragile spider variant which is not good for much other than laying webs." + response_harm_continuous = "splats" + response_harm_simple = "splat" + ai_controller = /datum/ai_controller/basic_controller/giant_spider/pest + apply_spider_antag = FALSE + +/mob/living/basic/giant_spider/maintenance/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) + AddElement(/datum/element/web_walker, /datum/movespeed_modifier/duct_spider_web) + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/tiny_mob_hunter) diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm index 6eb1020d13e..b16b7174c45 100644 --- a/code/modules/movespeed/modifiers/mobs.dm +++ b/code/modules/movespeed/modifiers/mobs.dm @@ -97,6 +97,9 @@ /datum/movespeed_modifier/tarantula_web multiplicative_slowdown = 5 +/datum/movespeed_modifier/duct_spider_web + multiplicative_slowdown = 1 + /datum/movespeed_modifier/gravity blacklisted_movetypes = FLOATING variable = TRUE diff --git a/icons/mob/clothing/head/pets_head.dmi b/icons/mob/clothing/head/pets_head.dmi index 387f51d9b553d0467af9dddcae18f2c995605d70..19ce9ec46751751b68554020426060af96f7b41a 100644 GIT binary patch delta 2176 zcmaizdpy%^AICRSPURTF9g3}+5FHSOiO69}ZmBsFa`uFrwjY+;AvJPpskS_!LMv;< z95V705}HF(PARN~d)Ulo+r8)RdA;ht=ljofU9ao)x?b<={r!Fll~~F}Y6@VGGz0_! zMb-OwiI=~MpFr+c>5RRshp+8;&FS1e?)_3NGU zTxhC$q2M5c$m^bJ+WIPz)|hud2I+}t_Cm#>yT+-O9`jBHa$U55V*%9&?)iA*ikZ1> z?~OJs@C)_MxdEcBHW{;w3c;`~2qYDJ(gE(3a(l5n3#sh7qg&mDx+R$=xx&7?XU+b5 zPM~y-F0ORJP#_ZZw;U`|U1~cx6i(va(J|upG+!E^ey`H8kQg*z9u3Rvy{V1sZnrZZl_f&xOMfDC)XinIZkC zB)5^kJs`NuyI__KQOVhm&!!XEPGdb7lL5e>6X2CJT{(X~%@G&j;HJEc!DC z{9nF6Hu3^)7Pz#`1;~j%n)WACVR=&RCQ1n)9c*XZ66W|OM`ADWYqhg<}`|o;AMd8^!SvUqmS;y zc%s~+m`J6APlY1y$iydAegTHFY5Je0>RQnaldTa`VNd~Mx8INy@%)Q(Cjm_>+msPX zzYFib!T(vk0ssuS@<4xR`P6x^fRA~HST#boQQEUul%ZNES59cEiearn7>Lse~C)C*g zg9;;U{D3;mbT*85{xJp~Xa3R2m`+FnhSkKT;>Zl>w-~XhhH?YDy6%5WVX@e5P$_n# zve%!3hejmG`0^_+%F5kGR>s1~?#98eQBf1U2BW&j^VAsAn-Ti2PFS<&9Qo$^7#<3Y z8k-+_XoDNny3rW#_c}i%iFYYpiB@-{M%=dm?*tx{y`J#eF{VsfEzO$IyLF=Et|GM= zc;U4F^s!oDjG1;=<*w){%c=jS zzFx&*l&q5z$CnlOEGwwBC?!@Uv-g2zzHo<~sx+9_6(lOvus*sA)hPGZ#YC%6SVx1X z+Xw$2^|ympp8}KFEMAJk(Dx z-g5iY*>n2m?|{{X*9RcvJhzG~{PbC~h7L;`5K4#mihTN-@&fQQzP%P3{)$1WncMZC z#Wcu|Pg@$?o2;d=L}hJsv_i`IrZavMZHxaN7uGJ(3WPodU6yOk79R4Nz_4E0V_3Km zi3O-7a!ErR44O*6G$vs)E-IQ8K?gk+RM)(0d|3V|>m*0&r?-7Q)${Ia^EW8JiS>(-un>4(bD(-sUl!zsE zjZgAQO_a+A-{Wm&+19vAdBaG+bbCbV9yWCgfsU;qX|n`+SlKEqSZ0){CBZk7(cgeh zKo&lqE;ZVjG}?K!=DS+rqNpE-I^m{wtF`jj@4?2grCDEyw>;Wj@wA34#u})f!3m4v d(fp4Ne%1_6DvDSdB|nnsE$)ad z9rQ$`9LXNWw$?h<4Hp+qNE~cgCX_Dc;F4_IsfekV*lr7n_tDK+vomH*tcHoJu3f5X z(OCk)@zG{FA?jh$HYa{{)sVhyPVQFp>sUn;rF40SeCRRGT)N+wrefIZWsxR&%3Vrg z6@mvg2M5?#>djdtM?_Qs(K1c+gQbsBixWfrLDlntmwtQP{vTgHzrK6Y4+P@QzG7^M z49j23I%Y3^MF4X|NFqNb*EFnW&_uAo@HKbWnG?{rrsuW}fnhuKd5rCd1U=`J4kOO> zugPWn$Vb(bLdGRA%qz*2ts8lDtBut0+b@Ozs@-F9!4(%hR=S_L{!1gUFnR_IifIKT%WIa|D53T~~h@!r66VyJZwA z+MbIpoaTmlGlHwUU>ElbjnCWGYeIMHDh{-mq4Z$Q`k2HQRYpQu8gdyWchu=~ZNizk zdnBHWhc;%0&#aLbbyj1G#18_T%f$h=*sG5axJ231%8HR0k&Qp^I~te4cz1iVUG*Xz z%$8FTl~$W`LV^;TV#m+@B*=vESUz4GWoq?HqP3yQV2O>%S=MI7a$Sl@(a)dW>;))X zot!~vF0@9=^J5yQMP>>5T-p8UN}&cO8m4?16k%skiXtib7Sp&-|3?w4yhh+-waPO& zgivlpoAVf4x%b)=!VoxJa+`F!Bswf8DCqT~W21lT@kmqQO2(WZv>$sWY^)FO&yR(K zfMY2RpthFC587;oT`s%u=37fG^${Jv!1{r;gX;62=%6cTPK4np_(i?k|0xcT5t6S&z5(%gc^mf zx__BE(@RK8I#s?r_tpWjZYD&l9c4MJT`=WoueMl7j~B{khDPiN(F%)f!#~zkqJ6V* zK%6&_MjRUIfmG<(ZOBB70}h7S)t$|R+`sA!wVIa%k$1hUJ-k>%E#2=-!FtiiZ>G?l z|1wcF^uL<)G_oQ4-7{9hOxDbvf!c|DegMNk@h5Ol#T-;zOeMMtjr(GPD=2(5$$l|) zq5m;;{L2L2H~P**(~q+GW@7x;o?L^%HN~;7mVuTS3+MDZb4IR-7o3<8Q*O!~c)Ymf z`GMDcZdEQjeWrXZo8@)Zw|GX`h3XQ{6en!?>e$k!J1Sz*E6DLdT8cSX*rdyIryPPr zTXo)17Q3nZ`!CrfVhAj<5M_}AySD;plUm2+FYSbs{if)EGp|&6b~{xqKDp2Zd@M$F*lw$!VN6Sw~<$Q@2zbtVLakL%k8zQDuv-a!8NQ$r34c4 zWQfZH7WmtZb=n0`!vk4Qixs>V1V-6XQ}FfPgY8}?dRrb*I$VK2biI}@UP5s(gYD}N zM=BDWCx4K})63DY_ORR?Opz?jg2zfQ)P}Q66J*q}di>K$cNld`>fIWA9UVM-Xw6Be zEjY__O_H<#Z<)r)bT;ejB95`e(uuX^5L+r7%lZ-4Mh``yy#ZG3q6yg}W@-ETRQhNh Yg5}5d+?D42d%mErT(&T-LAb>J4+qr6ApigX diff --git a/icons/mob/simple/animal.dmi b/icons/mob/simple/animal.dmi index a0928224bc85894d01c259481d3af0a3449fb94a..8875c1f27c6bbe7b174b87dcbc021516792066e1 100644 GIT binary patch delta 8786 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 Ag8%>k delta 6826 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