diff --git a/code/__DEFINES/ai/incursion_blackboard_defines.dm b/code/__DEFINES/ai/incursion_blackboard_defines.dm
new file mode 100644
index 00000000000..327e6a1f548
--- /dev/null
+++ b/code/__DEFINES/ai/incursion_blackboard_defines.dm
@@ -0,0 +1,2 @@
+/// The skull action used by reanimators
+#define BB_REANIMATOR_SKULL_ACTION "BB_reanimator_skull_action"
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
index d2691e9b3f2..b920cff9889 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
@@ -99,3 +99,21 @@
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl
attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches
target_key = BB_PROWL_TARGET
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/walls
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/walls
+
+/datum/ai_behavior/attack_obstructions/walls
+ can_attack_turfs = TRUE
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/prowl/walls
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches/walls
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches
+
+/datum/ai_planning_subtree/attack_obstacle_in_path/avoid_breaches/walls
+ attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches/walls
+
+/datum/ai_behavior/attack_obstructions/avoid_breaches/walls
+ can_attack_turfs = TRUE
diff --git a/code/datums/components/basic_mob_aggro_emote.dm b/code/datums/components/basic_mob_aggro_emote.dm
index 18849790378..30996ef0381 100644
--- a/code/datums/components/basic_mob_aggro_emote.dm
+++ b/code/datums/components/basic_mob_aggro_emote.dm
@@ -16,6 +16,8 @@
var/aggro_sound
/// Volume for the sound
var/aggro_volume
+ /// List of possible things to say
+ var/list/say_list
/datum/component/aggro_emote/Initialize(
target_key = BB_BASIC_MOB_CURRENT_TARGET,
@@ -26,6 +28,7 @@
emote_chance = 30,
minimum_chance = 2,
subtract_chance = 7,
+ list/say_list,
)
. = ..()
if(!isatom(parent))
@@ -40,6 +43,7 @@
src.emote_chance = emote_chance
src.minimum_chance = minimum_chance
src.subtract_chance = subtract_chance
+ src.say_list = say_list
/datum/component/aggro_emote/RegisterWithParent()
. = ..()
@@ -65,3 +69,5 @@
mob_parent.emote("me", EMOTE_VISIBLE, "[pick(emote_list)] at [new_target].")
if(aggro_sound)
playsound(mob_parent.loc, aggro_sound, aggro_volume, TRUE)
+ if(say_list)
+ mob_parent.say(pick(say_list))
diff --git a/code/datums/components/spawner.dm b/code/datums/components/spawner.dm
index 6b331752bd8..81731db2a29 100644
--- a/code/datums/components/spawner.dm
+++ b/code/datums/components/spawner.dm
@@ -47,7 +47,7 @@
if(spawn_delay > world.time)
return
spawn_delay = world.time + spawn_time
- var/chosen_mob_type = pick(mob_types)
+ var/chosen_mob_type = pickweight(mob_types)
var/mob/living/simple_animal/L = new chosen_mob_type(P.loc)
L.admin_spawned = P.admin_spawned
spawned_mobs += L
@@ -91,5 +91,8 @@
COOLDOWN_START(src, last_rally, 30 SECONDS)
for(var/mob/living/basic/rallied as anything in spawned_mobs)
+ if(rallied.ventcrawler == VENTCRAWLER_ALWAYS)
+ return // Don't rally ventcrawlers - they might have to walk the entire station!
rallied.ai_controller.cancel_actions()
- rallied.ai_controller.queue_behavior(/datum/ai_behavior/return_home/incursion_portal, BB_INCURSION_HOME_PORTAL)
+ rallied.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target)
+ rallied.ai_controller.queue_behavior(/datum/ai_behavior/return_to_portal, BB_INCURSION_HOME_PORTAL)
diff --git a/code/modules/events/demon_incursion.dm b/code/modules/events/demon_incursion.dm
index d1e0318b1fb..6fb94087921 100644
--- a/code/modules/events/demon_incursion.dm
+++ b/code/modules/events/demon_incursion.dm
@@ -111,18 +111,31 @@
spawn_time = 5 SECONDS // Short spawn time initially, it gets updated after it spawns initial mobs
max_mobs = 5 // We want a lot of mobs, but not too many
max_integrity = 200
- mob_types = list(/mob/living/basic/netherworld/migo,
- /mob/living/basic/netherworld,
- /mob/living/basic/netherworld/blankbody,
- /mob/living/basic/hellhound,
- /mob/living/basic/skeleton,
- /mob/living/basic/netherworld/faithless)
+ mob_types = list(/mob/living/basic/netherworld/migo = 6, // Generic mobs - just run at the target
+ /mob/living/basic/netherworld = 6,
+ /mob/living/basic/netherworld/faithless = 6,
+ /mob/living/basic/skeleton/incursion = 6,
+ /mob/living/basic/netherworld/blankbody = 5,
+ /mob/living/basic/hellhound/whelp = 5, // Specialized mobs - tank
+ /mob/living/basic/skeleton/incursion/security = 5, // Specialized mobs - ranged
+ /mob/living/basic/giant_spider/flesh_spider = 5, // Specialized mobs - poison/harasser
+ /mob/living/basic/skeleton/reanimator = 4, // Specialized mobs - Summoner
+ /mob/living/basic/skeleton/incursion/mobster = 1,) // Specialized mobs - ranged
icon = 'icons/obj/structures/portal.dmi'
icon_state = "portal"
light_range = 4
light_power = 2
light_color = "#780606"
spawner_type = /datum/component/spawner/demon_incursion_portal
+ /// Mob types that cannot have a special variant - pretty much anything with a unique AI controller
+ var/list/no_special_variants = list(
+ /mob/living/basic/hellhound/whelp,
+ /mob/living/basic/giant_spider/flesh_spider,
+ /mob/living/basic/skeleton/incursion/security,
+ /mob/living/basic/skeleton/incursion/mobster,
+ /mob/living/basic/skeleton/reanimator)
+ /// Chance that a mob type is special
+ var/special_chance = 15
/// The event that spawned this portal
var/datum/event/demon_incursion/linked_incursion
/// Percentage chance that a portal will spread every time spread() is called
@@ -136,10 +149,10 @@
/// Time until next portal
var/expansion_delay
/// How fast does the portal spawn mobs after the initial spawns?
- var/spawn_rate = 30 SECONDS
+ var/spawn_rate = 45 SECONDS
/// How many initial mobs does it spawn?
var/initial_spawns_min = 1
- var/initial_spawns_max = 4
+ var/initial_spawns_max = 3
/// Are we spawning initial mobs?
var/spawning_initial_mobs = TRUE
/// Current tile spread distance
@@ -217,9 +230,47 @@
var/mob/living/basic/new_mob = created_mob
if(!istype(new_mob, /mob/living/basic))
return
- if(new_mob.basic_mob_flags & DEL_ON_DEATH)
+ if(created_mob.type in no_special_variants)
return
- new_mob.AddComponent(/datum/component/incursion_mob_death)
+ if(!(new_mob.basic_mob_flags & DEL_ON_DEATH))
+ new_mob.AddComponent(/datum/component/incursion_mob_death)
+ if(!prob(special_chance))
+ return
+ var/special_type = pick("grappler", "enflamed", "hastened", "electrified", "juggernaut")
+ switch(special_type)
+ if("grappler")
+ new_mob.AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/energy/demonic_grappler, burst_shots = 1, projectile_sound = 'sound/weapons/wave.ogg')
+ new_mob.name = "grappling " + new_mob.name
+ new_mob.ai_controller = new /datum/ai_controller/basic_controller/incursion/ranged(new_mob)
+ new_mob.update_appearance(UPDATE_NAME)
+ new_mob.color = "#5494DA"
+ if("enflamed")
+ new_mob.AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/magic/fireball/small, burst_shots = 1, projectile_sound = 'sound/magic/fireball.ogg')
+ new_mob.name = "enflamed " + new_mob.name
+ new_mob.ai_controller = new /datum/ai_controller/basic_controller/incursion/ranged(new_mob)
+ new_mob.update_appearance(UPDATE_NAME)
+ new_mob.color = "#d4341f"
+ if("hastened")
+ new_mob.name = "hastened " + new_mob.name
+ new_mob.update_appearance(UPDATE_NAME)
+ new_mob.color = "#1fd437"
+ new_mob.speed = -1
+ if("electrified")
+ new_mob.AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/energy/demonic_shocker, burst_shots = 1, projectile_sound = 'sound/weapons/taser.ogg')
+ new_mob.name = "electrified " + new_mob.name
+ ADD_TRAIT(new_mob, TRAIT_SHOCKIMMUNE, "electrified")
+ new_mob.ai_controller = new /datum/ai_controller/basic_controller/incursion/ranged(new_mob)
+ new_mob.update_appearance(UPDATE_NAME)
+ new_mob.color = "#fcf7f6"
+ if("juggernaut")
+ new_mob.name = "juggernaut " + new_mob.name
+ new_mob.ai_controller = new /datum/ai_controller/basic_controller/incursion/juggernaut(new_mob)
+ new_mob.health = new_mob.health * 2
+ new_mob.maxHealth = new_mob.maxHealth * 2
+ new_mob.speed += 6
+ new_mob.environment_smash = ENVIRONMENT_SMASH_WALLS // Puny wall.
+ new_mob.update_appearance(UPDATE_NAME)
+ new_mob.color = "#292827"
/obj/structure/spawner/nether/demon_incursion/attacked_by(obj/item/attacker, mob/living/user)
. = ..()
diff --git a/code/modules/mob/living/basic/nether_mobs/blankbody.dm b/code/modules/mob/living/basic/hostile/nether_mobs/blankbody.dm
similarity index 83%
rename from code/modules/mob/living/basic/nether_mobs/blankbody.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/blankbody.dm
index 478ac1e6aa2..af631d43d21 100644
--- a/code/modules/mob/living/basic/nether_mobs/blankbody.dm
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/blankbody.dm
@@ -6,6 +6,7 @@
icon_dead = "blank-dead"
health = 100
maxHealth = 100
+ a_intent = INTENT_HARM
obj_damage = 50
melee_damage_lower = 5
melee_damage_upper = 10
@@ -17,9 +18,13 @@
death_message = "falls apart into a fine dust."
/// The body/brain of the player turned into a blank, if the blank was turned
var/mob/living/held_body
+ /// The held body's player is in control of the blank
+ var/is_original_mob = FALSE
/mob/living/basic/netherworld/blankbody/death(gibbed)
. = ..()
if(held_body)
held_body.forceMove(loc)
+ if(is_original_mob)
+ mind.transfer_to(held_body)
qdel(src)
diff --git a/code/modules/mob/living/basic/hostile/creature.dm b/code/modules/mob/living/basic/hostile/nether_mobs/creature.dm
similarity index 100%
rename from code/modules/mob/living/basic/hostile/creature.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/creature.dm
diff --git a/code/modules/mob/living/basic/nether_mobs/faithless.dm b/code/modules/mob/living/basic/hostile/nether_mobs/faithless.dm
similarity index 96%
rename from code/modules/mob/living/basic/nether_mobs/faithless.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/faithless.dm
index 0127fca53a6..6df564570e7 100644
--- a/code/modules/mob/living/basic/nether_mobs/faithless.dm
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/faithless.dm
@@ -7,7 +7,7 @@
icon_dead = "faithless_dead"
mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
melee_damage_lower = 15
- melee_damage_upper = 15
+ melee_damage_upper = 20
harm_intent_damage = 10
obj_damage = 50
attack_verb_simple = "grip"
diff --git a/code/modules/mob/living/basic/hostile/nether_mobs/flesh_spider.dm b/code/modules/mob/living/basic/hostile/nether_mobs/flesh_spider.dm
new file mode 100644
index 00000000000..461720e92fe
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/flesh_spider.dm
@@ -0,0 +1,32 @@
+// Giant evil flesh spider
+/mob/living/basic/giant_spider/flesh_spider
+ name = "flesh spider"
+ desc = "A horrifyingly grotesque mass of animated flesh shaped like a spider. A tar-like venom drips from its bony fangs."
+ icon_state = "flesh_spider"
+ icon_living = "flesh_spider"
+ icon_dead = "flesh_spider_dead"
+ mob_biotypes = MOB_ORGANIC
+ ventcrawler = VENTCRAWLER_ALWAYS
+ butcher_results = list(/obj/item/food/monstermeat/spidermeat = 4)
+ maxHealth = 70
+ health = 70
+ obj_damage = 30
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ melee_attack_cooldown_min = 1 SECONDS
+ melee_attack_cooldown_max = 2 SECONDS
+ faction = list("nether")
+ ai_controller = /datum/ai_controller/basic_controller/incursion/flesh_spider
+ venom_per_bite = 10
+ innate_actions = list()
+ gold_core_spawnable = NO_SPAWN
+
+/mob/living/basic/giant_spider/flesh_spider/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/event_tracker)
+ AddElement(/datum/element/ai_retaliate)
+
+/mob/living/basic/giant_spider/flesh_spider/event_cost()
+ . = list()
+ if(is_station_level((get_turf(src)).z) && stat != DEAD)
+ return list(ASSIGNMENT_SECURITY = 0.5, ASSIGNMENT_CREW = 1, ASSIGNMENT_MEDICAL = 0.5)
diff --git a/code/modules/mob/living/basic/hostile/hellhound.dm b/code/modules/mob/living/basic/hostile/nether_mobs/hellhound.dm
similarity index 98%
rename from code/modules/mob/living/basic/hostile/hellhound.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/hellhound.dm
index 24435082f4c..8ad0e7e1306 100644
--- a/code/modules/mob/living/basic/hostile/hellhound.dm
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/hellhound.dm
@@ -157,3 +157,8 @@
melee_damage_lower = 30
melee_damage_upper = 50
gold_core_spawnable = NO_SPAWN
+
+/mob/living/basic/hellhound/whelp
+ name = "hellhound whelp"
+ maxHealth = 150
+ health = 150
diff --git a/code/modules/mob/living/basic/hostile/hellhound_ai.dm b/code/modules/mob/living/basic/hostile/nether_mobs/hellhound_ai.dm
similarity index 96%
rename from code/modules/mob/living/basic/hostile/hellhound_ai.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/hellhound_ai.dm
index 51b5a0894f2..acd3575bbcb 100644
--- a/code/modules/mob/living/basic/hostile/hellhound_ai.dm
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/hellhound_ai.dm
@@ -4,6 +4,7 @@
)
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
+ /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/hellhound_rest,
diff --git a/code/modules/mob/living/basic/hostile/nether_mobs/incursion_ai.dm b/code/modules/mob/living/basic/hostile/nether_mobs/incursion_ai.dm
new file mode 100644
index 00000000000..6209107425a
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/incursion_ai.dm
@@ -0,0 +1,154 @@
+/// Prowls around when not attacking people
+/datum/ai_controller/basic_controller/incursion
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_AGGRO_RANGE = 6
+ )
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ interesting_dist = AI_SIMPLE_INTERESTING_DIST
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/datum/ai_controller/basic_controller/incursion/ranged
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/datum/ai_controller/basic_controller/incursion/juggernaut
+ ai_movement = /datum/ai_movement/jps
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/walls,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl/walls,
+ )
+
+/datum/ai_controller/basic_controller/incursion/ranged_distance
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_RANGED_SKIRMISH_MIN_DISTANCE = 3,
+ BB_RANGED_SKIRMISH_MAX_DISTANCE = 6,
+ BB_AGGRO_RANGE = 6,
+ )
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/ranged_skirmish/avoid_friendly,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/datum/ai_controller/basic_controller/incursion/flesh_spider
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_AGGRO_RANGE = 6,
+ BB_VENT_SEARCH_RANGE = 6,
+ )
+ ai_movement = /datum/ai_movement/jps
+
+ ai_traits = AI_FLAG_PAUSE_DURING_DO_AFTER
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ /datum/ai_planning_subtree/ventcrawl_find_target,
+ /datum/ai_planning_subtree/ventcrawl,
+ )
+
+/datum/ai_controller/basic_controller/incursion/reanimator
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_RANGED_SKIRMISH_MIN_DISTANCE = 3,
+ BB_RANGED_SKIRMISH_MAX_DISTANCE = 4,
+ BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
+ BB_AGGRO_RANGE = 6
+ )
+
+ ai_traits = AI_FLAG_PAUSE_DURING_DO_AFTER
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
+ /datum/ai_planning_subtree/conjure_skulls,
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses/human/reanimator,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/human/reanimator
+ hunt_range = 6
+
+/// Run the conjure skulls action
+/datum/ai_planning_subtree/conjure_skulls
+ /// Key where the summoning action is stored
+ var/action_key = BB_REANIMATOR_SKULL_ACTION
+ /// Key where the target is stored
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+/datum/ai_planning_subtree/conjure_skulls/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(action_key) || !controller.blackboard_key_exists(target_key))
+ return
+ var/datum/action/cooldown/mob_cooldown/summon_skulls/skull_action = controller.blackboard[action_key]
+ if(skull_action.IsAvailable())
+ controller.queue_behavior(/datum/ai_behavior/conjure_skulls, action_key, target_key)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/// Nyeh heh heh!
+/datum/ai_behavior/conjure_skulls
+ action_cooldown = 20 SECONDS // We don't want them doing this too quickly
+ required_distance = 0
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/conjure_skulls/perform(seconds_per_tick, datum/ai_controller/controller, action_key, target_key)
+ . = ..()
+ var/datum/action/cooldown/mob_cooldown/summon_skulls/skull_action = controller.blackboard[action_key]
+ var/result = skull_action.Trigger()
+ if(result)
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+/datum/ai_behavior/return_to_portal
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/return_to_portal/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ var/list/possible_turfs = get_adjacent_open_turfs(target)
+ shuffle_inplace(possible_turfs)
+ for(var/turf/possible_turf as anything in possible_turfs)
+ if(!possible_turf.is_blocked_turf())
+ set_movement_target(controller, possible_turf)
+ return TRUE
+ return FALSE
+
+/datum/ai_behavior/return_to_portal/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/modules/mob/living/basic/nether_mobs/migo.dm b/code/modules/mob/living/basic/hostile/nether_mobs/migo.dm
similarity index 100%
rename from code/modules/mob/living/basic/nether_mobs/migo.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/migo.dm
diff --git a/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm b/code/modules/mob/living/basic/hostile/nether_mobs/nether_mobs.dm
similarity index 70%
rename from code/modules/mob/living/basic/nether_mobs/nether_mobs.dm
rename to code/modules/mob/living/basic/hostile/nether_mobs/nether_mobs.dm
index 0bdd7bdf478..631a0716a01 100644
--- a/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/nether_mobs.dm
@@ -8,11 +8,11 @@
maxHealth = 80
obj_damage = 100
melee_damage_lower = 25
- melee_damage_upper = 50
+ melee_damage_upper = 30
melee_attack_cooldown_min = 1.5 SECONDS
melee_attack_cooldown_max = 2.5 SECONDS
environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
+ ai_controller = /datum/ai_controller/basic_controller/incursion
attack_verb_simple = "chomp"
attack_verb_continuous = "chomps"
attack_sound = 'sound/weapons/bladeslice.ogg'
@@ -24,17 +24,9 @@
contains_xeno_organ = TRUE
surgery_container = /datum/xenobiology_surgery_container/sweating
step_type = FOOTSTEP_MOB_SHOE
- /// The chance of it being a grappler variant
- var/grappler_chance = 20
/mob/living/basic/netherworld/Initialize(mapload)
. = ..()
- if(prob(grappler_chance))
- AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/energy/demonic_grappler, projectile_sound = 'sound/weapons/wave.ogg')
- name = "grappling " + name
- ai_controller = new /datum/ai_controller/basic_controller/simple/simple_skirmisher/prowler(src)
- update_appearance(UPDATE_NAME)
- color = "#5494DA"
AddElement(/datum/element/ai_retaliate)
AddComponent(/datum/component/event_tracker, EVENT_DEMONIC)
diff --git a/code/modules/mob/living/basic/hostile/nether_mobs/skeleton_mob.dm b/code/modules/mob/living/basic/hostile/nether_mobs/skeleton_mob.dm
new file mode 100644
index 00000000000..5a220089e06
--- /dev/null
+++ b/code/modules/mob/living/basic/hostile/nether_mobs/skeleton_mob.dm
@@ -0,0 +1,272 @@
+/mob/living/basic/skeleton
+ name = "reanimated skeleton"
+ desc = "A real bonefied skeleton, doesn't seem like it wants to socialize."
+ icon = 'icons/mob/simple_human.dmi'
+ icon_state = "skeleton"
+ icon_living = "skeleton"
+ speak_emote = list("rattles")
+ mob_biotypes = MOB_UNDEAD | MOB_HUMANOID
+ maxHealth = 40
+ health = 40
+ a_intent = INTENT_HARM
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ harm_intent_damage = 5
+ obj_damage = 50
+ melee_attack_cooldown_min = 1.5 SECONDS
+ melee_attack_cooldown_max = 2.5 SECONDS
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = 1500
+ healable = FALSE
+ attack_verb_simple = "slash"
+ attack_verb_continuous = "slashes"
+ attack_sound = 'sound/hallucinations/growl1.ogg'
+ atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAM = 0, OXY = 1)
+ unsuitable_atmos_damage = 10
+ see_in_dark = 8
+ faction = list("skeleton")
+ lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
+ basic_mob_flags = DEL_ON_DEATH
+ step_type = FOOTSTEP_MOB_SHOE
+ gold_core_spawnable = HOSTILE_SPAWN
+ loot = list(/obj/effect/decal/remains/human)
+ ai_controller = /datum/ai_controller/basic_controller/incursion
+
+/mob/living/basic/skeleton/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/ai_retaliate)
+ add_language("Galactic Common")
+ set_default_language(GLOB.all_languages["Galactic Common"])
+
+/mob/living/basic/skeleton/arctic
+ name = "undead arctic explorer"
+ desc = "The reanimated remains of some poor traveler."
+ icon_state = "arctic_skeleton"
+ icon_living = "arctic_skeleton"
+ maxHealth = 55
+ health = 55
+ weather_immunities = list("snow")
+ gold_core_spawnable = NO_SPAWN
+ melee_damage_lower = 17
+ melee_damage_upper = 20
+ death_message = "collapses into a pile of bones, its gear falling to the floor!"
+ loot = list(/obj/effect/decal/remains/human,
+ /obj/item/spear,
+ /obj/item/clothing/shoes/winterboots,
+ /obj/item/clothing/suit/hooded/wintercoat)
+
+/mob/living/basic/skeleton/warden
+ name = "skeleton warden"
+ desc = "The remains of a warden."
+ icon_state = "skeleton_warden"
+ icon_living = "skeleton_warden"
+ loot = list(/obj/effect/decal/cleanable/shreds, /mob/living/basic/skeleton/angered_warden)
+ maxHealth = 300
+ health = 300
+ death_message = null
+ gold_core_spawnable = NO_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
+
+/mob/living/basic/skeleton/warden/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
+ return TRUE
+
+/mob/living/basic/skeleton/angered_warden
+ name = "angered skeleton warden" // round 2
+ desc = "An angry skeleton."
+ icon_state = "skeleton_warden_alt"
+ icon_living = "skeleton_warden_alt"
+ attack_verb_simple = "claw"
+ attack_verb_continuous = "claws"
+ maxHealth = 200
+ health = 200
+ speed = -1
+ melee_damage_lower = 30
+ melee_damage_upper = 30
+ loot = list(/obj/effect/decal/remains/human, /obj/item/clothing/head/warden, /obj/item/card/sec_shuttle_ruin)
+ gold_core_spawnable = NO_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
+
+/mob/living/basic/skeleton/angered_warden/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
+ return TRUE
+
+/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
+ idle_behavior = null // Don't idly wander
+
+/mob/living/basic/skeleton/incursion
+ name = "reinforced skeleton"
+ desc = "It's got a bone to pick with you."
+ health = 75
+ maxHealth = 75
+ gold_core_spawnable = NO_SPAWN
+
+/mob/living/basic/skeleton/incursion/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/event_tracker)
+
+/mob/living/basic/skeleton/incursion/event_cost()
+ . = list()
+ if(is_station_level((get_turf(src)).z) && stat != DEAD)
+ return list(ASSIGNMENT_SECURITY = 0.5, ASSIGNMENT_CREW = 1, ASSIGNMENT_MEDICAL = 0.5)
+
+/mob/living/basic/skeleton/incursion/security
+ name = "skeletal officer"
+ desc = "HALT! SKELECURITY!"
+ icon_state = "skeleton_officer"
+ icon_living = "skeleton_officer"
+ health = 90
+ maxHealth = 90
+ is_ranged = TRUE
+ projectile_type = /obj/projectile/beam/laser
+ projectile_sound = 'sound/weapons/gunshots/gunshot_lascarbine.ogg'
+ ranged_cooldown = 2.5 SECONDS
+ ai_controller = /datum/ai_controller/basic_controller/incursion/ranged_distance
+
+/mob/living/basic/skeleton/incursion/security/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_BASICMOB_POST_ATTACK_RANGED, PROC_REF(cycle_lever))
+ AddComponent(/datum/component/aggro_emote, say_list = list("Halt! Security!", "Hands up!", "Get on the ground!", "Halt! Halt! Halt!", "They're resisting!", "Lethals authorized!"))
+ if(prob(20))
+ loot += pick(/obj/item/clothing/head/helmet, /obj/item/clothing/suit/armor/vest/security)
+
+/mob/living/basic/skeleton/incursion/security/proc/cycle_lever()
+ sleep(0.5 SECONDS)
+ playsound(src, 'sound/weapons/gun_interactions/lever_action.ogg', 60, TRUE)
+
+/mob/living/basic/skeleton/incursion/mobster
+ name = "skeletal mobster"
+ desc = "THEY EXAMINED THE SKELETON! RATTLE EM, BOYS!"
+ icon_state = "skeleton_mobster"
+ icon_living = "skeleton_mobster"
+ is_ranged = TRUE
+ casing_type = /obj/item/ammo_casing/skeleton_smg
+ ranged_burst_count = 6
+ ranged_burst_interval = 0.1 SECONDS
+ ranged_cooldown = 2.5 SECONDS
+ ai_controller = /datum/ai_controller/basic_controller/incursion/ranged_distance
+
+/mob/living/basic/skeleton/incursion/mobster/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/aggro_emote, say_list = list("Ice 'em!", "Rattle 'em, boys!", "I've got a bone to pick with you!", "Everyone's got a few skeletons in their closet!"))
+ if(prob(20))
+ loot += pick(/obj/item/clothing/head/fedora, /obj/item/clothing/under/suit/mafia)
+
+/obj/item/ammo_casing/skeleton_smg
+ projectile_type = /obj/projectile/bullet/skeleton_smg
+ muzzle_flash_range = MUZZLE_FLASH_RANGE_NORMAL
+
+/obj/projectile/bullet/skeleton_smg
+ damage = 5
+
+/mob/living/basic/skeleton/reanimator
+ name = "skeletal reanimator"
+ desc = "A dark necromancer from the depths of an unknowable darkness."
+ icon_state = "skeleton_reanimator"
+ icon_living = "skeleton_reanimator"
+ is_ranged = TRUE
+ projectile_type = /obj/projectile/magic/necrotic_bolt
+ ranged_burst_count = 2
+ ranged_burst_interval = 0.35 SECONDS
+ projectile_sound = 'sound/magic/magic_missile.ogg'
+ ai_controller = /datum/ai_controller/basic_controller/incursion/reanimator
+ /// List of actions the reanimator has
+ var/list/reanimator_actions = list(
+ /datum/action/cooldown/mob_cooldown/summon_skulls = BB_REANIMATOR_SKULL_ACTION,
+ )
+
+/mob/living/basic/skeleton/reanimator/Initialize(mapload)
+ . = ..()
+ grant_actions_by_list(reanimator_actions)
+
+/mob/living/basic/skeleton/reanimator/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
+ if(!ishuman(target))
+ return ..()
+ if(target.stat != DEAD)
+ return ..()
+ new /obj/effect/temp_visual/cult/rune_spawn/rune7(target.loc, 5 SECONDS, "#252525")
+ new /obj/effect/temp_visual/cult/rune_spawn/rune7/inner(target.loc, 5 SECONDS, "#252525")
+ new /obj/effect/temp_visual/cult/rune_spawn/rune7/center(target.loc, 5 SECONDS, "#252525")
+
+ // First, see if we can get the OG player. If not, choose from Dchat
+ var/mob/dead/observer/original_ghost = target.get_ghost()
+ if(original_ghost)
+ to_chat(original_ghost, "You are being revived by otherworldly forces! Return to your body if you want to be revived! (Verbs -> Ghost -> Re-enter corpse)")
+ window_flash(original_ghost.client)
+ SEND_SOUND(original_ghost, sound('sound/effects/genetics.ogg'))
+ if(do_after_once(src, 2 SECONDS, target = target, attempt_cancel_message = "You stop reanimating a corpse.", interaction_key = "reanimator_revive"))
+ sleep(3 SECONDS) // Locks the revitalizer down for 2 seconds, but gives the player 5 seconds to return
+ reanimate(target)
+
+/mob/living/basic/skeleton/reanimator/proc/reanimate(mob/living/carbon/human/H)
+ visible_message("[name] releases dark tendrils into the flesh of [H], morphing their corpse into a grotesque creature!")
+ var/mob/living/basic/netherworld/blankbody/blank = new(H.loc)
+ blank.name = "[H]"
+ blank.desc = "It's [H], but [H.p_their()] flesh has an ashy texture, and [H.p_their()] face is featureless save an eerie smile."
+ blank.faction = faction.Copy()
+ visible_message("[blank] staggers to [H.p_their()] feet!")
+ blank.held_body = H
+ H.forceMove(blank)
+ var/mob/dead/observer/ghost = H.get_ghost(TRUE)
+ if(!ghost)
+ H.mind.transfer_to(blank)
+ blank.is_original_mob = TRUE
+ return
+
+ var/mob/dead/observer/chosen_ghost
+ var/list/candidates
+ candidates = SSghost_spawns.poll_candidates("Would you like to play as a Reanimated Blank?", ROLE_SENTIENT, FALSE, poll_time = 10 SECONDS, source = /mob/living/basic/netherworld/blankbody, role_cleanname = "blank")
+ if(length(candidates))
+ chosen_ghost = pick(candidates)
+ if(chosen_ghost && blank.stat != DEAD)
+ blank.key = chosen_ghost.key
+ blank.cancel_camera()
+ dust_if_respawnable(chosen_ghost)
+ to_chat(blank, "You have been raised by the dead to serve as a footsoldier in the incursion. Strike down your foes!")
+
+/obj/projectile/magic/necrotic_bolt
+ name = "necrotic bolt"
+ damage = 10
+ damage_type = BURN
+ nodamage = 0
+ icon_state = "arcane_barrage"
+
+/datum/action/cooldown/mob_cooldown/summon_skulls
+ name = "Summon Skulls"
+ button_icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "legion_head"
+ desc = "Summon a pair of skulls to seek out and slay enemies."
+ click_to_activate = FALSE
+ melee_cooldown_time = CLICK_CD_CLICK_ABILITY
+ cooldown_time = 20 SECONDS
+ shared_cooldown = NONE
+
+/datum/action/cooldown/mob_cooldown/summon_skulls/Activate(atom/target)
+ var/mob/living/summoner = target
+ if(!istype(summoner))
+ to_chat(target, "You are unable to summon skulls!")
+ return
+
+ for(var/i in 1 to 2)
+ var/mob/living/basic/mining/hivelordbrood/reanimator/S = new(summoner.loc)
+ S.admin_spawned = summoner.admin_spawned
+ S.faction = summoner.faction.Copy()
+ S.Move(get_step(summoner.loc, pick(GLOB.alldirs)))
+ S.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, summoner.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ new /obj/effect/temp_visual/emp/pulse/cult(S.loc)
+ StartCooldown()
+
+// A fragile skull produced by Skeleton Reanimators
+/mob/living/basic/mining/hivelordbrood/reanimator
+ name = "reanimator skull"
+ desc = "A cursed skull."
+ icon_state = "legion_head"
+ icon_living = "legion_head"
+ icon_aggro = "legion_head"
+ icon_dead = "legion_head"
+ melee_damage_lower = 5
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bites"
+ speak_emote = list("chatters")
+ throw_blocked_message = "is shrugged off by"
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
diff --git a/code/modules/mob/living/basic/hostile/skeleton_mob.dm b/code/modules/mob/living/basic/hostile/skeleton_mob.dm
deleted file mode 100644
index 655cdfcf149..00000000000
--- a/code/modules/mob/living/basic/hostile/skeleton_mob.dm
+++ /dev/null
@@ -1,100 +0,0 @@
-/mob/living/basic/skeleton
- name = "reanimated skeleton"
- desc = "A real bonefied skeleton, doesn't seem like it wants to socialize."
- icon = 'icons/mob/simple_human.dmi'
- icon_state = "skeleton"
- icon_living = "skeleton"
- speak_emote = list("rattles")
- mob_biotypes = MOB_UNDEAD | MOB_HUMANOID
- maxHealth = 40
- health = 40
- a_intent = INTENT_HARM
- melee_damage_lower = 15
- melee_damage_upper = 15
- harm_intent_damage = 5
- obj_damage = 50
- melee_attack_cooldown_min = 1.5 SECONDS
- melee_attack_cooldown_max = 2.5 SECONDS
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- minimum_survivable_temperature = 0
- maximum_survivable_temperature = 1500
- healable = FALSE
- attack_verb_simple = "slash"
- attack_verb_continuous = "slashes"
- attack_sound = 'sound/hallucinations/growl1.ogg'
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAM = 0, OXY = 1)
- unsuitable_atmos_damage = 10
- see_in_dark = 8
- faction = list("skeleton")
- lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
- basic_mob_flags = DEL_ON_DEATH
- step_type = FOOTSTEP_MOB_SHOE
- gold_core_spawnable = HOSTILE_SPAWN
- loot = list(/obj/effect/decal/remains/human)
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
-
-/mob/living/basic/skeleton/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/ai_retaliate)
- AddComponent(/datum/component/event_tracker)
-
-/mob/living/basic/skeleton/event_cost()
- . = list()
- if(is_station_level((get_turf(src)).z) && stat != DEAD)
- return list(ASSIGNMENT_SECURITY = 0.5, ASSIGNMENT_CREW = 1, ASSIGNMENT_MEDICAL = 0.5)
-
-
-/mob/living/basic/skeleton/arctic
- name = "undead arctic explorer"
- desc = "The reanimated remains of some poor traveler."
- icon_state = "arctic_skeleton"
- icon_living = "arctic_skeleton"
- maxHealth = 55
- health = 55
- weather_immunities = list("snow")
- gold_core_spawnable = NO_SPAWN
- melee_damage_lower = 17
- melee_damage_upper = 20
- death_message = "collapses into a pile of bones, its gear falling to the floor!"
- loot = list(/obj/effect/decal/remains/human,
- /obj/item/spear,
- /obj/item/clothing/shoes/winterboots,
- /obj/item/clothing/suit/hooded/wintercoat)
-
-/mob/living/basic/skeleton/warden
- name = "skeleton warden"
- desc = "The remains of a warden."
- icon_state = "skeleton_warden"
- icon_living = "skeleton_warden"
- loot = list(/obj/effect/decal/cleanable/shreds, /mob/living/basic/skeleton/angered_warden)
- maxHealth = 300
- health = 300
- death_message = null
- gold_core_spawnable = NO_SPAWN
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
-
-/mob/living/basic/skeleton/warden/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- return TRUE
-
-/mob/living/basic/skeleton/angered_warden
- name = "angered skeleton warden" // round 2
- desc = "An angry skeleton."
- icon_state = "skeleton_warden_alt"
- icon_living = "skeleton_warden_alt"
- attack_verb_simple = "claw"
- attack_verb_continuous = "claws"
- maxHealth = 200
- health = 200
- speed = -1
- melee_damage_lower = 30
- melee_damage_upper = 30
- loot = list(/obj/effect/decal/remains/human, /obj/item/clothing/head/warden, /obj/item/card/sec_shuttle_ruin)
- gold_core_spawnable = NO_SPAWN
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
-
-/mob/living/basic/skeleton/angered_warden/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- return TRUE
-
-/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/skeleton_warden
- idle_behavior = null // Don't idly wander
diff --git a/code/modules/projectiles/projectile/magic_projectiles.dm b/code/modules/projectiles/projectile/magic_projectiles.dm
index ecb4bffdaf6..ae019e24eb6 100644
--- a/code/modules/projectiles/projectile/magic_projectiles.dm
+++ b/code/modules/projectiles/projectile/magic_projectiles.dm
@@ -94,6 +94,12 @@
exp_flash = 4
exp_fire= 5
+/obj/projectile/magic/fireball/small
+ name = "firebolt"
+ exp_heavy = -1
+ exp_light = 1
+ exp_fire = 3
+
/obj/projectile/magic/resurrection
name = "bolt of resurrection"
icon_state = "ion"
diff --git a/code/modules/projectiles/projectile/special_projectiles.dm b/code/modules/projectiles/projectile/special_projectiles.dm
index c51298c1574..59b86e9bea9 100644
--- a/code/modules/projectiles/projectile/special_projectiles.dm
+++ b/code/modules/projectiles/projectile/special_projectiles.dm
@@ -300,14 +300,44 @@
pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE
/obj/projectile/energy/demonic_grappler/on_hit(atom/target, blocked = 0)
- if(isliving(target))
- var/turf/source_turf = get_turf(firer)
- do_teleport(target, source_turf)
- else
- var/turf/miss_turf = get_step(target, get_dir(target, firer))
- do_teleport(firer, miss_turf)
+ var/turf/miss_turf = get_step(target, get_dir(target, firer))
+ do_teleport(firer, miss_turf)
return ..()
+/obj/projectile/energy/demonic_shocker
+ name = "demonic shocker"
+ icon_state = "electrode"
+ nodamage = 1
+ pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE
+ hitsound = 'sound/magic/lightningbolt.ogg'
+ /// Maximum amount of bounces
+ var/bounce_count = 3
+
+/obj/projectile/energy/demonic_shocker/on_hit(atom/target, blocked = 0)
+ if(!isliving(target))
+ return ..()
+ chain(target, firer)
+ return ..()
+
+/obj/projectile/energy/demonic_shocker/proc/chain(mob/living/target, mob/source)
+ var/mob/living/L = target
+ L.electrocute_act(20, firer, flags = SHOCK_NOGLOVES)
+ if(bounce_count <= 0)
+ return
+
+ var/list/possible_targets = list()
+ for(var/mob/living/M in oview(3, target))
+ if(firer == M || M.faction_check_mob(firer))
+ continue
+ possible_targets += M
+ if(!length(possible_targets))
+ return
+
+ var/next_victim = pick(possible_targets)
+ bounce_count -= 1
+ playsound(get_turf(target), 'sound/magic/lightningshock.ogg', 50, TRUE, -1)
+ chain(next_victim, target)
+
/obj/projectile/snowball
name = "snowball"
icon_state = "snowball"
diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi
index 59cd7d6f592..3426d93c47d 100644
Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ
diff --git a/icons/mob/simple_human.dmi b/icons/mob/simple_human.dmi
index c1f1a0ca035..30a46a56396 100644
Binary files a/icons/mob/simple_human.dmi and b/icons/mob/simple_human.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 0bbf9e40a70..b8e73f90489 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -162,6 +162,7 @@
#include "code\__DEFINES\ai\ai_defines.dm"
#include "code\__DEFINES\ai\blackboard_defines.dm"
#include "code\__DEFINES\ai\changeling_giant_spider_ai_defines.dm"
+#include "code\__DEFINES\ai\incursion_blackboard_defines.dm"
#include "code\__DEFINES\dcs\ai_signals.dm"
#include "code\__DEFINES\dcs\area_signals.dm"
#include "code\__DEFINES\dcs\atom_signals.dm"
@@ -2494,17 +2495,13 @@
#include "code\modules\mob\living\basic\hostile\bats.dm"
#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\creature.dm"
#include "code\modules\mob\living\basic\hostile\drakehound.dm"
#include "code\modules\mob\living\basic\hostile\feral_cat.dm"
-#include "code\modules\mob\living\basic\hostile\hellhound.dm"
-#include "code\modules\mob\living\basic\hostile\hellhound_ai.dm"
#include "code\modules\mob\living\basic\hostile\hivebot.dm"
#include "code\modules\mob\living\basic\hostile\hivebot_ai.dm"
#include "code\modules\mob\living\basic\hostile\jungle_animals.dm"
#include "code\modules\mob\living\basic\hostile\killertomato.dm"
#include "code\modules\mob\living\basic\hostile\pirate.dm"
-#include "code\modules\mob\living\basic\hostile\skeleton_mob.dm"
#include "code\modules\mob\living\basic\hostile\spiderlings.dm"
#include "code\modules\mob\living\basic\hostile\soviet.dm"
#include "code\modules\mob\living\basic\hostile\tree.dm"
@@ -2534,6 +2531,16 @@
#include "code\modules\mob\living\basic\hostile\gorilla\gorilla_emote.dm"
#include "code\modules\mob\living\basic\hostile\revenant\revenant.dm"
#include "code\modules\mob\living\basic\hostile\revenant\revenant_abilities.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\blankbody.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\creature.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\faithless.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\flesh_spider.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\hellhound.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\hellhound_ai.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\incursion_ai.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\migo.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\nether_mobs.dm"
+#include "code\modules\mob\living\basic\hostile\nether_mobs\skeleton_mob.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\goliath.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_actions.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_ai.dm"
@@ -2551,10 +2558,6 @@
#include "code\modules\mob\living\basic\mining\mining_mobs.dm"
#include "code\modules\mob\living\basic\mining\goldgrub\goldgrub.dm"
#include "code\modules\mob\living\basic\mining\goldgrub\goldgrub_ai.dm"
-#include "code\modules\mob\living\basic\nether_mobs\blankbody.dm"
-#include "code\modules\mob\living\basic\nether_mobs\faithless.dm"
-#include "code\modules\mob\living\basic\nether_mobs\migo.dm"
-#include "code\modules\mob\living\basic\nether_mobs\nether_mobs.dm"
#include "code\modules\mob\living\basic\retaliate\clown.dm"
#include "code\modules\mob\living\basic\retaliate\combat_drone.dm"
#include "code\modules\mob\living\basic\retaliate\kangaroo.dm"