diff --git a/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm b/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
index 1d63a1a0070..7e3b7000c42 100644
--- a/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
+++ b/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
@@ -637,7 +637,7 @@
/obj/effect/turf_decal/tile/yellow/half{
dir = 4
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/iron/co2_pressurized,
/area/ruin/space/has_grav/atmosasteroid)
"RK" = (
diff --git a/_maps/RandomRuins/SpaceRuins/prey_pod.dmm b/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
index 9089dea664c..6f53409b873 100644
--- a/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
+++ b/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
@@ -17,7 +17,7 @@
/obj/structure/chair/comfy/shuttle{
dir = 4
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"p" = (
@@ -32,7 +32,7 @@
/area/ruin/space/has_grav)
"D" = (
/obj/structure/chair/comfy/shuttle,
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"E" = (
@@ -57,7 +57,7 @@
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"S" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"T" = (
diff --git a/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm b/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm
index 5aeba75b3bd..6acec2ccc43 100644
--- a/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm
+++ b/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm
@@ -56,7 +56,7 @@
/turf/open/floor/plating/airless,
/area/ruin/space/prison_shuttle)
"q" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/misc/asteroid/airless,
/area/ruin/space/prison_shuttle)
"r" = (
@@ -84,7 +84,7 @@
/turf/open/floor/mineral/titanium/tiled/airless,
/area/ruin/space/prison_shuttle)
"w" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/plating/airless,
/area/ruin/space/prison_shuttle)
"x" = (
diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm
index b1f5d4fac30..24ecf0a19f6 100644
--- a/code/__DEFINES/ai/ai_blackboard.dm
+++ b/code/__DEFINES/ai/ai_blackboard.dm
@@ -8,6 +8,8 @@
#define BB_FOOD_TARGET "bb_food_target"
///Path we should use next time we use the JPS movement datum
#define BB_PATH_TO_USE "BB_path_to_use"
+///How close a mob must be for us to select it as a target, if that is less than how far we can maintain it as a target
+#define BB_AGGRO_RANGE "BB_aggro_range"
///Turf we want a mob to move to
#define BB_TRAVEL_DESTINATION "BB_travel_destination"
@@ -44,6 +46,8 @@
#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"
+#define BB_BASIC_MOB_FLEE_DISTANCE "BB_basic_flee_distance"
+#define DEFAULT_BASIC_FLEE_DISTANCE 9
/// Generic key for a non-specific targetted action
#define BB_TARGETTED_ACTION "BB_targetted_action"
diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm
index 927f3736832..be9a4be34cd 100644
--- a/code/__DEFINES/ai/monsters.dm
+++ b/code/__DEFINES/ai/monsters.dm
@@ -146,3 +146,10 @@
#define BB_RAPIDSEEDS_ABILITY "rapidseeds_ability"
/// key holds the tray we will beam
#define BB_BEAMABLE_HYDROPLANT_TARGET "beamable_hydroplant_target"
+
+/// Corpse we have consumed
+#define BB_LEGION_CORPSE "legion_corpse"
+/// Things our target recently said
+#define BB_LEGION_RECENT_LINES "legion_recent_lines"
+/// The creator of our legion skull
+#define BB_LEGION_BROOD_CREATOR "legion_brood_creator"
diff --git a/code/__DEFINES/mood.dm b/code/__DEFINES/mood.dm
new file mode 100644
index 00000000000..161f253b04c
--- /dev/null
+++ b/code/__DEFINES/mood.dm
@@ -0,0 +1 @@
+#define MOOD_CATEGORY_LEGION_CORE "regenerative core"
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 4ac7be4c4d0..03a3bf49ebb 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -37,6 +37,7 @@
#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
#define STASIS_ADMIN "stasis_admin"
+#define STASIS_LEGION_EATEN "stasis_eaten"
#define STASIS_NETPOD_EFFECT "stasis_netpod"
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
index 486f9a0fdb2..bd86260ee89 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
@@ -4,7 +4,7 @@
action_cooldown = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How far do we try to run? Further makes for smoother running, but potentially weirder pathfinding
- var/run_distance = 9
+ var/run_distance = DEFAULT_BASIC_FLEE_DISTANCE
/// Clear target if we finish the action unsuccessfully
var/clear_failed_targets = TRUE
@@ -12,6 +12,7 @@
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
+ run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || initial(run_distance)
if(!plot_path_away_from(controller, target))
return FALSE
return ..()
@@ -21,13 +22,12 @@
if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
return
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
- var/escaped = QDELETED(target) || !can_see(controller.pawn, target, run_distance) // If we can't see it we got away
- if (escaped)
+ if (QDELETED(target) || !can_see(controller.pawn, target, run_distance))
finish_action(controller, succeeded = TRUE, target_key = target_key, hiding_location_key = hiding_location_key)
return
- if (get_dist(controller.pawn, controller.current_movement_target) > required_distance)
- return
- if(plot_path_away_from(controller, target))
+ if (get_dist(controller.pawn, controller.current_movement_target) >= required_distance)
+ return // Still heading over
+ if (plot_path_away_from(controller, target))
return
finish_action(controller, succeeded = FALSE, target_key = target_key, hiding_location_key = hiding_location_key)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
index 9d7587c712b..0b9e31db667 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
@@ -2,6 +2,8 @@
action_cooldown = 2 SECONDS
/// How far can we see stuff?
var/vision_range = 9
+ /// Blackboard key for aggro range, uses vision range if not specified
+ var/aggro_range_key = BB_AGGRO_RANGE
/// Static typecache list of potentially dangerous objs
var/static/list/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/vehicle/sealed/mecha))
@@ -18,11 +20,13 @@
finish_action(controller, succeeded = FALSE)
return
- controller.clear_blackboard_key(target_key)
- var/list/potential_targets = hearers(vision_range, controller.pawn) - living_mob //Remove self, so we don't suicide
+ var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
- for(var/HM in typecache_filter_list(range(vision_range, living_mob), hostile_machines)) //Can we see any hostile machines?
- if(can_see(living_mob, HM, vision_range))
+ controller.clear_blackboard_key(target_key)
+ var/list/potential_targets = hearers(aggro_range, controller.pawn) - living_mob //Remove self, so we don't suicide
+
+ for(var/HM in typecache_filter_list(range(aggro_range, living_mob), hostile_machines)) //Can we see any hostile machines?
+ if(can_see(living_mob, HM, aggro_range))
potential_targets += HM
if(!potential_targets.len)
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 a4982fa660e..8d1391f7c7d 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
@@ -9,8 +9,13 @@
/datum/ai_planning_subtree/flee_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING] || !controller.blackboard_key_exists(target_key))
+ var/atom/flee_from = controller.blackboard[target_key]
+ if (!controller.blackboard[BB_BASIC_MOB_FLEEING] || QDELETED(flee_from))
return
+ var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
+ if (get_dist(controller.pawn, flee_from) >= flee_distance)
+ return
+
controller.queue_behavior(flee_behaviour, target_key, hiding_place_key)
return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here.
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
index b80a28836a0..c09e7cdbf75 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
@@ -3,7 +3,7 @@
/// Blackboard key holding atom we want to stay away from
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// How close will we allow our target to get?
- var/minimum_distance = 3
+ var/minimum_distance = 4
/// How far away will we allow our target to get?
var/maximum_distance = 6
/// How far do we look for our target?
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
index e040a566bcf..52f4a3459bf 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
@@ -22,24 +22,27 @@
emote_see = string_list(emote_see)
/datum/ai_planning_subtree/random_speech/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if(SPT_PROB(speech_chance, seconds_per_tick))
- var/audible_emotes_length = emote_hear?.len
- var/non_audible_emotes_length = emote_see?.len
- var/speak_lines_length = speak?.len
+ if(!SPT_PROB(speech_chance, seconds_per_tick))
+ return
+ speak(controller)
- var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
+/// Actually perform an action
+/datum/ai_planning_subtree/random_speech/proc/speak(datum/ai_controller/controller)
+ var/audible_emotes_length = emote_hear?.len
+ var/non_audible_emotes_length = emote_see?.len
+ var/speak_lines_length = speak?.len
- var/random_number_in_range = rand(1, total_choices_length)
+ var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
- // Sound that plays when the mob does something audible
- var/sound_to_play = length(sound) > 0 ? pick(sound) : null
+ var/random_number_in_range = rand(1, total_choices_length)
+ var/sound_to_play = length(sound) > 0 ? pick(sound) : null
- if(random_number_in_range <= audible_emotes_length)
- controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear), sound_to_play)
- else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
- controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
- else
- controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play)
+ if(random_number_in_range <= audible_emotes_length)
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear), sound_to_play)
+ else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
+ else
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play)
/datum/ai_planning_subtree/random_speech/insect
speech_chance = 5
@@ -218,7 +221,9 @@
var/list/speech_lines = controller.blackboard[BB_BASIC_MOB_SPEAK_LINES]
if(isnull(speech_lines))
return ..()
-
+
+ // Note to future developers: this behaviour a singleton so this probably doesn't work as you would expect
+ // The whole speech tree really needs to be refactored because this isn't how we use AI data these days
speak = speech_lines[BB_EMOTE_SAY] || list()
emote_see = speech_lines[BB_EMOTE_SEE] || list()
emote_hear = speech_lines[BB_EMOTE_HEAR] || list()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
index 8d75f797163..d9ee3ef0918 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
@@ -23,3 +23,6 @@
controller.queue_behavior(use_ability_behaviour, ability_key, target_key)
if (finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_planning_subtree/targeted_mob_ability/continue_planning
+ finish_planning = FALSE
diff --git a/code/datums/ai/basic_mobs/generic_controllers.dm b/code/datums/ai/basic_mobs/generic_controllers.dm
new file mode 100644
index 00000000000..208c1833add
--- /dev/null
+++ b/code/datums/ai/basic_mobs/generic_controllers.dm
@@ -0,0 +1,26 @@
+/// The most basic AI tree which just finds a guy and then runs at them to click them
+/datum/ai_controller/basic_controller/simple_hostile
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Find a target, walk at target, attack intervening obstacles
+/datum/ai_controller/basic_controller/simple_hostile_obstacles
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm
index fd6d848f2d6..4f816de4be3 100644
--- a/code/datums/ai/generic/generic_behaviors.dm
+++ b/code/datums/ai/generic/generic_behaviors.dm
@@ -307,6 +307,16 @@
playsound(living_pawn, speech_sound, 80, vary = TRUE)
finish_action(controller, TRUE)
+/datum/ai_behavior/perform_speech_radio
+
+/datum/ai_behavior/perform_speech_radio/perform(seconds_per_tick, datum/ai_controller/controller, speech, obj/item/radio/speech_radio, list/try_channels = list(RADIO_CHANNEL_COMMON))
+ var/mob/living/living_pawn = controller.pawn
+ if(!istype(living_pawn) || !istype(speech_radio) || QDELETED(speech_radio) || !length(try_channels))
+ finish_action(controller, FALSE)
+ return
+ speech_radio.talk_into(living_pawn, speech, pick(try_channels))
+ finish_action(controller, TRUE)
+
//song behaviors
/datum/ai_behavior/setup_instrument
diff --git a/code/datums/components/appearance_on_aggro.dm b/code/datums/components/appearance_on_aggro.dm
index affb926d28d..33a3d7c2e90 100644
--- a/code/datums/components/appearance_on_aggro.dm
+++ b/code/datums/components/appearance_on_aggro.dm
@@ -1,11 +1,14 @@
/**
- * component gave to basic creatures to allow them to change appearance when find a target
+ * Changes visuals of the attached mob while it has a target
*/
/datum/component/appearance_on_aggro
+ /// Blackboardey to search for a target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// Icon state to use when we have a target
+ var/aggro_state
/// path of the overlay to apply
- var/mutable_appearance/overlay_path
+ var/mutable_appearance/aggro_overlay
/// visibility of our icon when aggroed
var/alpha_on_aggro
/// visibility of our icon when deaggroed
@@ -13,20 +16,22 @@
/// do we currently have a target
var/atom/current_target
-/datum/component/appearance_on_aggro/Initialize(overlay_icon, overlay_state, alpha_on_aggro, alpha_on_deaggro)
- if(!isliving(parent))
+/datum/component/appearance_on_aggro/Initialize(aggro_state, overlay_icon, overlay_state, alpha_on_aggro, alpha_on_deaggro)
+ if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
- if(overlay_icon && overlay_state)
- src.overlay_path = mutable_appearance(overlay_icon, overlay_state)
- if(!alpha_on_aggro || !alpha_on_deaggro)
- return
+ src.aggro_state = aggro_state
src.alpha_on_aggro = alpha_on_aggro
src.alpha_on_deaggro = alpha_on_deaggro
+ if (!isnull(overlay_icon) && !isnull(overlay_state))
+ aggro_overlay = mutable_appearance(overlay_icon, overlay_state)
/datum/component/appearance_on_aggro/RegisterWithParent()
- RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(change_overlays))
RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_set_target))
RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(on_clear_target))
+ if (!isnull(aggro_state))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_icon_state_updated))
+ if (!isnull(aggro_overlay))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_overlays_updated))
/datum/component/appearance_on_aggro/UnregisterFromParent()
. = ..()
@@ -36,37 +41,42 @@
SIGNAL_HANDLER
var/atom/target = source.ai_controller.blackboard[target_key]
- if(isnull(target))
+ if (QDELETED(target))
return
+
current_target = target
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_clear_target))
- if(overlay_path)
- source.update_appearance(UPDATE_OVERLAYS)
- if(alpha_on_aggro)
+ if (!isnull(aggro_overlay) || !isnull(aggro_state))
+ source.update_appearance(UPDATE_ICON)
+ if (!isnull(alpha_on_aggro))
animate(source, alpha = alpha_on_aggro, time = 2 SECONDS)
/datum/component/appearance_on_aggro/Destroy()
- if(current_target)
+ if (!isnull(current_target))
revert_appearance(parent)
return ..()
/datum/component/appearance_on_aggro/proc/on_clear_target(atom/source)
SIGNAL_HANDLER
-
revert_appearance(parent)
/datum/component/appearance_on_aggro/proc/revert_appearance(mob/living/source)
UnregisterSignal(current_target, COMSIG_QDELETING)
current_target = null
- if(overlay_path)
- source.update_appearance(UPDATE_OVERLAYS)
- if(alpha_on_deaggro)
+ if (!isnull(aggro_overlay) || !isnull(aggro_state))
+ source.update_appearance(UPDATE_ICON)
+ if (!isnull(alpha_on_deaggro))
animate(source, alpha = alpha_on_deaggro, time = 2 SECONDS)
-/datum/component/appearance_on_aggro/proc/change_overlays(atom/source, list/overlays)
+/datum/component/appearance_on_aggro/proc/on_icon_state_updated(mob/living/source)
+ SIGNAL_HANDLER
+ if (source.stat == DEAD)
+ return
+ source.icon_state = isnull(current_target) ? initial(source.icon_state) : aggro_state
+
+/datum/component/appearance_on_aggro/proc/on_overlays_updated(atom/source, list/overlays)
SIGNAL_HANDLER
- if(isnull(current_target))
+ if (isnull(current_target))
return
-
- overlays += overlay_path
+ overlays += aggro_overlay
diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm
index d3b8bd08ded..fa5bae91868 100644
--- a/code/datums/mapgen/CaveGenerator.dm
+++ b/code/datums/mapgen/CaveGenerator.dm
@@ -55,7 +55,7 @@
/mob/living/basic/mining/basilisk = 4,
/mob/living/basic/mining/goldgrub = 1,
/mob/living/basic/mining/goliath/ancient = 5,
- /mob/living/simple_animal/hostile/asteroid/hivelord = 3,
+ /mob/living/basic/mining/hivelord = 3,
)
mob_spawn_list = expand_weights(weighted_mob_spawn_list)
mob_spawn_no_mega_list = expand_weights(weighted_mob_spawn_list - SPAWN_MEGAFAUNA)
diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
index 89a153aedd7..7d7437ccda6 100644
--- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm
+++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
@@ -4,9 +4,9 @@
weighted_mob_spawn_list = list(
+ /mob/living/basic/mining/legion/snow = 50,
/mob/living/basic/mining/lobstrosity = 15,
/mob/living/basic/mining/goldgrub = 10,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 50,
/mob/living/simple_animal/hostile/asteroid/polarbear = 30,
/mob/living/simple_animal/hostile/asteroid/wolf = 50,
/obj/structure/spawner/ice_moon = 3,
@@ -63,7 +63,7 @@
weighted_mob_spawn_list = list(
SPAWN_MEGAFAUNA = 1,
/mob/living/basic/mining/ice_whelp = 60,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 100,
+ /mob/living/basic/mining/legion/snow = 100,
/mob/living/simple_animal/hostile/asteroid/ice_demon = 100,
/obj/structure/spawner/ice_moon/demonic_portal = 6,
/obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6,
diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index a39187f3194..23a3364adc3 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -167,6 +167,9 @@
mood_change = -4
timeout = 2 MINUTES
+/datum/mood_event/healsbadman/long_term
+ timeout = 10 MINUTES
+
/datum/mood_event/jittery
description = "I'm nervous and on edge and I can't stand still!!"
mood_change = -2
diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm
index bc5026ab148..b71f66c0fa6 100644
--- a/code/datums/mutations/antenna.dm
+++ b/code/datums/mutations/antenna.dm
@@ -91,32 +91,11 @@
// chance to alert the read-ee
to_chat(cast_on, span_danger("You feel something foreign enter your mind."))
- var/list/recent_speech = list()
- var/list/say_log = list()
- var/log_source = cast_on.logging
- //this whole loop puts the read-ee's say logs into say_log in an easy to access way
- for(var/log_type in log_source)
- var/nlog_type = text2num(log_type)
- if(nlog_type & LOG_SAY)
- var/list/reversed = log_source[log_type]
- if(islist(reversed))
- say_log = reverse_range(reversed.Copy())
- break
-
- for(var/spoken_memory in say_log)
- //up to 3 random lines of speech, favoring more recent speech
- if(length(recent_speech) >= 3)
- break
- if(prob(50))
- continue
- // log messages with tags like telepathy are displayed like "(Telepathy to Ckey/(target)) "greetings"""
- // by splitting the text by using a " delimiter, we can grab JUST the greetings part
- recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
-
+ var/list/recent_speech = cast_on.copy_recent_speech(copy_amount = 3, line_chance = 50)
if(length(recent_speech))
to_chat(owner, span_boldnotice("You catch some drifting memories of their past conversations..."))
for(var/spoken_memory in recent_speech)
- to_chat(owner, span_notice("[recent_speech[spoken_memory]]"))
+ to_chat(owner, span_notice("[spoken_memory]"))
if(iscarbon(cast_on))
var/mob/living/carbon/carbon_cast_on = cast_on
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index 3122c8c1e84..bfdcdc45064 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -243,9 +243,9 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
if (jobs[trim_assignment] != null)
entry["ijob"] = jobs[trim_assignment]
- // Binary living/dead status
+ // Current status
if (sensor_mode >= SENSOR_LIVING)
- entry["life_status"] = (tracked_living_mob.stat != DEAD)
+ entry["life_status"] = tracked_living_mob.stat
// Damage
if (sensor_mode >= SENSOR_VITALS)
diff --git a/code/game/objects/effects/decals/cleanable/misc.dm b/code/game/objects/effects/decals/cleanable/misc.dm
index 6a519650d10..daaddb00ce0 100644
--- a/code/game/objects/effects/decals/cleanable/misc.dm
+++ b/code/game/objects/effects/decals/cleanable/misc.dm
@@ -212,6 +212,10 @@
icon_state += "-old"
AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 10)
+/obj/effect/decal/cleanable/vomit/old/black_bile
+ name = "black bile"
+ desc = "There's something wiggling in there..."
+ color = COLOR_DARK
/obj/effect/decal/cleanable/chem_pile
name = "chemical pile"
diff --git a/code/game/objects/effects/spawners/random/lavaland_mobs.dm b/code/game/objects/effects/spawners/random/lavaland_mobs.dm
index 8aa20cc90e7..7b4bec1f6a1 100644
--- a/code/game/objects/effects/spawners/random/lavaland_mobs.dm
+++ b/code/game/objects/effects/spawners/random/lavaland_mobs.dm
@@ -10,9 +10,9 @@
/mob/living/basic/mining/brimdemon = 1,
/mob/living/basic/mining/goldgrub = 1,
/mob/living/basic/mining/goliath = 1,
+ /mob/living/basic/mining/legion = 1,
/mob/living/basic/mining/lobstrosity/lava = 1,
/mob/living/basic/mining/watcher = 1,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 1,
)
/// Spawns random watcher variants during map generation
@@ -46,6 +46,6 @@
desc = "Chance to spawn a rare shiny version."
icon_state = "legion"
loot = list(
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 19,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf = 1,
+ /mob/living/basic/mining/legion = 19,
+ /mob/living/basic/mining/legion/dwarf = 1,
)
diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm
index 10f7a56c7f9..7393c20758e 100644
--- a/code/game/objects/structures/icemoon/cave_entrance.dm
+++ b/code/game/objects/structures/icemoon/cave_entrance.dm
@@ -102,7 +102,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
mob_types = list(/mob/living/basic/mining/ice_whelp)
/obj/structure/spawner/ice_moon/demonic_portal/snowlegion
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow/portal)
+ mob_types = list(/mob/living/basic/mining/legion/snow/spawner_made)
/obj/effect/collapsing_demonic_portal
name = "collapsing demonic portal"
diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm
index 542df563a1f..22f7f0422cd 100644
--- a/code/game/objects/structures/lavaland/necropolis_tendril.dm
+++ b/code/game/objects/structures/lavaland/necropolis_tendril.dm
@@ -23,7 +23,7 @@
mob_types = list(/mob/living/basic/mining/goliath)
/obj/structure/spawner/lavaland/legion
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril)
+ mob_types = list(/mob/living/basic/mining/legion/spawner_made)
/obj/structure/spawner/lavaland/icewatcher
mob_types = list(/mob/living/basic/mining/watcher/icewing)
diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm
index db04c82815f..a4f40d49c8d 100644
--- a/code/game/objects/structures/spawner.dm
+++ b/code/game/objects/structures/spawner.dm
@@ -80,8 +80,8 @@
/mob/living/basic/mining/basilisk,
/mob/living/basic/mining/goldgrub,
/mob/living/basic/mining/goliath/ancient,
+ /mob/living/basic/mining/legion,
/mob/living/basic/wumborian_fugu,
- /mob/living/simple_animal/hostile/asteroid/hivelord,
)
faction = list(FACTION_MINING)
@@ -98,7 +98,7 @@
/obj/structure/spawner/mining/hivelord
name = "hivelord den"
desc = "A den housing a nest of hivelords."
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord)
+ mob_types = list(/mob/living/basic/mining/hivelord)
/obj/structure/spawner/mining/basilisk
name = "basilisk den"
diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm
index c2ae89933d1..cee0f0da5b9 100644
--- a/code/modules/antagonists/changeling/powers/absorb.dm
+++ b/code/modules/antagonists/changeling/powers/absorb.dm
@@ -95,31 +95,14 @@
//Some of target's recent speech, so the changeling can attempt to imitate them better.
//Recent as opposed to all because rounds tend to have a LOT of text.
- var/list/recent_speech = list()
- var/list/say_log = list()
- var/log_source = target.logging
- for(var/log_type in log_source)
- var/nlog_type = text2num(log_type)
- if(nlog_type & LOG_SAY)
- var/list/reversed = log_source[log_type]
- if(islist(reversed))
- say_log = reverse_range(reversed.Copy())
- break
-
- if(LAZYLEN(say_log) > LING_ABSORB_RECENT_SPEECH)
- recent_speech = say_log.Copy(say_log.len-LING_ABSORB_RECENT_SPEECH+1,0) //0 so len-LING_ARS+1 to end of list
- else
- for(var/spoken_memory in say_log)
- if(recent_speech.len >= LING_ABSORB_RECENT_SPEECH)
- break
- recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
+ var/list/recent_speech = target.copy_recent_speech()
if(recent_speech.len)
changeling.antag_memory += "Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!
"
to_chat(owner, span_boldnotice("Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!"))
for(var/spoken_memory in recent_speech)
- changeling.antag_memory += "\"[recent_speech[spoken_memory]]\"
"
- to_chat(owner, span_notice("\"[recent_speech[spoken_memory]]\""))
+ changeling.antag_memory += "\"[spoken_memory]\"
"
+ to_chat(owner, span_notice("\"[spoken_memory]\""))
changeling.antag_memory += "We have no more knowledge of [target]'s speech patterns.
"
to_chat(owner, span_boldnotice("We have no more knowledge of [target]'s speech patterns."))
diff --git a/code/modules/antagonists/changeling/powers/panacea.dm b/code/modules/antagonists/changeling/powers/panacea.dm
index 034248e1a55..68689ee65d4 100644
--- a/code/modules/antagonists/changeling/powers/panacea.dm
+++ b/code/modules/antagonists/changeling/powers/panacea.dm
@@ -13,7 +13,9 @@
..()
var/list/bad_organs = list(
user.get_organ_by_type(/obj/item/organ/internal/body_egg),
- user.get_organ_by_type(/obj/item/organ/internal/zombie_infection))
+ user.get_organ_by_type(/obj/item/organ/internal/legion_tumour),
+ user.get_organ_by_type(/obj/item/organ/internal/zombie_infection),
+ )
for(var/o in bad_organs)
var/obj/item/organ/O = o
diff --git a/code/modules/mining/equipment/monster_organs/regenerative_core.dm b/code/modules/mining/equipment/monster_organs/regenerative_core.dm
index cb224438c00..98758d5a369 100644
--- a/code/modules/mining/equipment/monster_organs/regenerative_core.dm
+++ b/code/modules/mining/equipment/monster_organs/regenerative_core.dm
@@ -36,7 +36,7 @@
/// Log applications and apply moodlet.
/obj/item/organ/internal/monster_core/regenerative_core/apply_to(mob/living/target, mob/user)
- target.add_mood_event("regenerative core", /datum/mood_event/healsbadman)
+ target.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman)
if (target != user)
target.visible_message(span_notice("[user] forces [target] to apply [src]... Black tendrils entangle and reinforce [target.p_them()]!"))
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "other"))
diff --git a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
index e2c67af2467..c88178135dc 100644
--- a/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
+++ b/code/modules/mob/living/basic/jungle/mega_arachnid/mega_arachnid_ai.dm
@@ -2,6 +2,7 @@
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_BASIC_MOB_FLEEING = TRUE,
+ BB_BASIC_MOB_FLEE_DISTANCE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
@@ -47,7 +48,6 @@
/datum/ai_behavior/run_away_from_target/mega_arachnid
clear_failed_targets = FALSE
- run_distance = 5
///only engage in melee combat against cuffed targets, otherwise keep throwing restraints at them
/datum/ai_planning_subtree/basic_melee_attack_subtree/mega_arachnid
diff --git a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
index c662870393c..45bfd74d23b 100644
--- a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
+++ b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm
@@ -75,6 +75,7 @@
/datum/ai_controller/basic_controller/basilisk
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_AGGRO_RANGE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
index 95df44a8326..b99e254853e 100644
--- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
+++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm
@@ -44,6 +44,8 @@
COOLDOWN_DECLARE(ability_animation_cooldown)
/// Our base tentacles ability
var/datum/action/cooldown/mob_cooldown/goliath_tentacles/tentacles
+ /// Our long-ranged tentacles ability
+ var/datum/action/cooldown/mob_cooldown/tentacle_grasp/tentacle_line
/// Things we want to eat off the floor (or a plate, we're not picky)
var/static/list/goliath_foods = list(/obj/item/food/grown/ash_flora, /obj/item/food/bait/worm)
@@ -70,9 +72,9 @@
var/datum/action/cooldown/mob_cooldown/tentacle_burst/melee_tentacles = new (src)
melee_tentacles.Grant(src)
AddComponent(/datum/component/revenge_ability, melee_tentacles, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], max_range = 1, target_self = TRUE)
- var/datum/action/cooldown/mob_cooldown/tentacle_grasp/ranged_tentacles = new (src)
- ranged_tentacles.Grant(src)
- AddComponent(/datum/component/revenge_ability, ranged_tentacles, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], min_range = 2, max_range = 9)
+ tentacle_line = new (src)
+ tentacle_line.Grant(src)
+ AddComponent(/datum/component/revenge_ability, tentacle_line, targetting = ai_controller.blackboard[BB_TARGETTING_DATUM], min_range = 2, max_range = 9)
tentacles_ready()
RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(used_ability))
@@ -82,6 +84,7 @@
/mob/living/basic/mining/goliath/Destroy()
QDEL_NULL(tentacles)
+ QDEL_NULL(tentacle_line)
return ..()
/mob/living/basic/mining/goliath/examine(mob/user)
@@ -167,6 +170,12 @@
. = ..()
faction = new_friend.faction.Copy()
+/mob/living/basic/mining/goliath/RangedAttack(atom/atom_target, modifiers)
+ tentacles?.Trigger(target = atom_target)
+
+/mob/living/basic/mining/goliath/ranged_secondary_attack(atom/atom_target, modifiers)
+ tentacle_line?.Trigger(target = atom_target)
+
/// Legacy Goliath mob with different sprites, largely the same behaviour
/mob/living/basic/mining/goliath/ancient
name = "ancient goliath"
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
new file mode 100644
index 00000000000..256ab1fbd8f
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm
@@ -0,0 +1,114 @@
+/// Mob which retreats and spawns annoying sub-mobs to attack you
+/mob/living/basic/mining/hivelord
+ name = "hivelord"
+ desc = "A levitating swarm of tiny creatures which act as a single individual. When threatened or hunting they rapidly replicate additional short-lived bodies."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "hivelord"
+ icon_living = "hivelord"
+ // icon_aggro = "hivelord_alert"
+ icon_dead = "hivelord_dead"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC
+ speed = 2
+ maxHealth = 75
+ health = 75
+ melee_damage_lower = 0
+ melee_damage_upper = 0
+ attack_verb_continuous = "weakly tackles"
+ attack_verb_simple = "weakly tackles"
+ speak_emote = list("telepathically cries")
+ attack_sound = 'sound/weapons/pierce.ogg'
+ throw_blocked_message = "passes between the bodies of the"
+ obj_damage = 0
+ pass_flags = PASSTABLE
+ ai_controller = /datum/ai_controller/basic_controller/hivelord
+ /// Mobs to spawn when we die, varedit this to be recursive to give the players a fun surprise
+ var/death_spawn_type = /mob/living/basic/hivelord_brood
+ /// Action which spawns worms
+ var/datum/action/cooldown/mob_cooldown/hivelord_spawn/spawn_brood
+
+/mob/living/basic/mining/hivelord/Initialize(mapload)
+ . = ..()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core)
+ AddElement(/datum/element/relay_attackers)
+ AddElement(/datum/element/death_drops, death_loot)
+ AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY, dead_state = "hivelord_dead") // They writhe so much.
+ AddComponent(/datum/component/appearance_on_aggro, aggro_state = "hivelord_alert")
+ spawn_brood = new(src)
+ spawn_brood.Grant(src)
+ ai_controller.set_blackboard_key(BB_TARGETTED_ACTION, spawn_brood)
+
+/mob/living/basic/mining/hivelord/Destroy()
+ QDEL_NULL(spawn_brood)
+ return ..()
+
+/mob/living/basic/mining/hivelord/death(gibbed)
+ . = ..()
+ var/list/safe_turfs = RANGE_TURFS(1, src) - get_turf(src)
+ for (var/turf/check_turf as anything in safe_turfs)
+ if (check_turf.is_blocked_turf(exclude_mobs = TRUE))
+ safe_turfs -= check_turf
+
+ var/turf/our_turf = get_turf(src)
+ for (var/i in 1 to 3)
+ if (!length(safe_turfs))
+ return
+ var/turf/land_turf = pick_n_take(safe_turfs)
+ var/obj/effect/temp_visual/hivebrood_spawn/forecast = new(land_turf)
+ forecast.create_from(death_spawn_type, our_turf, CALLBACK(src, PROC_REF(complete_spawn), land_turf))
+
+/// Spawns a worm on the specified turf
+/mob/living/basic/mining/hivelord/proc/complete_spawn(turf/spawn_turf)
+ var/mob/living/brood = new death_spawn_type(spawn_turf)
+ brood.faction = faction
+ brood.ai_controller?.set_blackboard_key(ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ brood.dir = get_dir(src, spawn_turf)
+
+/mob/living/basic/mining/hivelord/RangedAttack(atom/atom_target, modifiers)
+ spawn_brood?.Trigger(target = atom_target)
+
+/// Attack worms spawned by the hivelord
+/mob/living/basic/hivelord_brood
+ name = "hivelord brood"
+ desc = "Short-lived attack form of the hivelord. One isn't much of a threat, but..."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "hivelord_brood"
+ icon_living = "hivelord_brood"
+ icon_dead = "hivelord_brood"
+ icon_gib = "syndicate_gib"
+ friendly_verb_continuous = "chirrups near"
+ friendly_verb_simple = "chirrup near"
+ mob_size = MOB_SIZE_SMALL
+ basic_mob_flags = DEL_ON_DEATH
+ pass_flags = PASSTABLE | PASSMOB
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ faction = list(FACTION_MINING)
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ speed = 1.5
+ maxHealth = 1
+ health = 1
+ melee_damage_lower = 2
+ melee_damage_upper = 2
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ speak_emote = list("telepathically cries")
+ attack_sound = 'sound/weapons/bite.ogg'
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ obj_damage = 0
+ density = FALSE
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile
+
+/mob/living/basic/hivelord_brood/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT)
+ AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/swarming)
+ AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY)
+ addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
+
+/mob/living/basic/hivelord_brood/death(gibbed)
+ if (!gibbed)
+ new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
+ return ..()
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm
new file mode 100644
index 00000000000..fd7983de397
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord_ai.dm
@@ -0,0 +1,14 @@
+/// Basically just keep away and shit out worms
+/datum/ai_controller/basic_controller/hivelord
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
+ BB_AGGRO_RANGE = 5, // Only get mad at people nearby
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ )
diff --git a/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
new file mode 100644
index 00000000000..3fee2a003f3
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/hivelord/spawn_hivelord_brood.dm
@@ -0,0 +1,124 @@
+/// Spawns a little worm nearby
+/datum/action/cooldown/mob_cooldown/hivelord_spawn
+ name = "Spawn Brood"
+ desc = "Release an attack form to an adjacent square to attack your target or anyone nearby."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "hivelord_brood"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = TRUE
+ cooldown_time = 2 SECONDS
+ melee_cooldown_time = 0
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
+ shared_cooldown = NONE
+ /// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
+ var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// What are we actually spawning?
+ var/spawn_type = /mob/living/basic/hivelord_brood
+ /// Do we automatically fire with no cooldown when damaged?
+ var/trigger_on_hit = TRUE
+ /// Minimum time between triggering on hit
+ var/on_hit_delay = 1 SECONDS
+ /// Delay between triggering on hit
+ COOLDOWN_DECLARE(on_hit_cooldown)
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Grant(mob/granted_to)
+ . = ..()
+ if (isnull(owner))
+ return
+ if (trigger_on_hit)
+ RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Remove(mob/removed_from)
+ UnregisterSignal(removed_from, COMSIG_ATOM_WAS_ATTACKED)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/Activate(atom/target)
+ . = ..()
+ if (!spawn_brood(target, target_turf = get_turf(target)))
+ StartCooldown(0.5 SECONDS)
+ return
+ StartCooldown()
+
+/// Called when someone whacks us
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/on_attacked(atom/victim, atom/attacker, attack_flags)
+ SIGNAL_HANDLER
+ if (!trigger_on_hit || !(attack_flags & ATTACKER_DAMAGING_ATTACK) || !COOLDOWN_FINISHED(src, on_hit_cooldown))
+ return
+ COOLDOWN_START(src, on_hit_cooldown, on_hit_delay)
+ spawn_brood(attacker, target_turf = get_step_away(owner, attacker), feedback = FALSE)
+
+/// Spawn a funny little worm
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/spawn_brood(target, turf/target_turf, feedback = TRUE)
+ var/ai_target = isliving(target) ? target : null
+ if (isnull(ai_target))
+ ai_target = owner.ai_controller?.blackboard[ai_target_key]
+
+ var/dir_to_target = get_dir(owner, target_turf)
+ var/list/target_turfs = list()
+ for(var/i in -1 to 1)
+ var/turn_amount = rand(-1, 1) * 45
+ var/test_dir = turn(dir_to_target, turn_amount)
+ var/turf/test_turf = get_step(owner, test_dir)
+ if (test_turf.is_blocked_turf(exclude_mobs = TRUE))
+ continue
+ target_turfs += test_turf
+
+ if (!length(target_turfs))
+ if (feedback)
+ owner.balloon_alert(owner, "no room!")
+ StartCooldown(0.5 SECONDS)
+ return FALSE
+
+ var/turf/land_turf = pick(target_turfs)
+ var/obj/effect/temp_visual/hivebrood_spawn/forecast = new(land_turf)
+ forecast.create_from(spawn_type, get_turf(owner), CALLBACK(src, PROC_REF(complete_spawn), land_turf, ai_target))
+ StartCooldown()
+
+ return TRUE
+
+/// Actually create a mob
+/datum/action/cooldown/mob_cooldown/hivelord_spawn/proc/complete_spawn(turf/spawn_turf, target)
+ var/mob/living/brood = new spawn_type(spawn_turf)
+ brood.faction = owner.faction
+ brood.ai_controller?.set_blackboard_key(ai_target_key, target)
+ brood.dir = get_dir(owner, spawn_turf)
+
+#define BROOD_ARC_Y_OFFSET 8
+#define BROOD_ARC_ROTATION 45
+
+/// Fast animation to show a worm spawning
+/obj/effect/temp_visual/hivebrood_spawn
+ name = "brood spawn"
+ duration = 0.3 SECONDS
+ alpha = 0
+
+/// Set up our visuals and start a timer for a callback
+/obj/effect/temp_visual/hivebrood_spawn/proc/create_from(mob/living/spawn_type, turf/spawn_from, datum/callback/on_completed)
+ addtimer(on_completed, duration, TIMER_DELETE_ME)
+
+ var/turf/my_turf = get_turf(src)
+ dir = get_dir(spawn_from, my_turf)
+ var/move_x = (my_turf.x - spawn_from.x) * world.icon_size
+ var/move_y = (my_turf.y - spawn_from.y) * world.icon_size
+ pixel_x = -move_x
+ pixel_y = -move_y
+
+ icon = initial(spawn_type.icon)
+ icon_state = initial(spawn_type.icon_state)
+
+
+ animate(src, pixel_x = 0, time = duration)
+ animate(src, pixel_y = BROOD_ARC_Y_OFFSET - (move_y * 0.5), time = duration * 0.5, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = duration * 0.5, easing = SINE_EASING | EASE_IN)
+ animate(src, alpha = 255, time = duration * 0.5, flags = ANIMATION_PARALLEL)
+
+ if (dir & (NORTH | EAST))
+ transform = matrix().Turn(-BROOD_ARC_ROTATION)
+ animate(src, transform = matrix(), time = duration, flags = ANIMATION_PARALLEL)
+ else
+ transform = matrix().Turn(BROOD_ARC_ROTATION)
+ animate(src, transform = matrix(), time = duration, flags = ANIMATION_PARALLEL)
+
+#undef BROOD_ARC_Y_OFFSET
+#undef BROOD_ARC_ROTATION
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion.dm b/code/modules/mob/living/basic/lavaland/legion/legion.dm
new file mode 100644
index 00000000000..7c6bd0fd170
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion.dm
@@ -0,0 +1,158 @@
+/**
+ * Avoids players while throwing skulls at them.
+ * Legion skulls heal allies, bite enemies, and infest dying humans to make more legions.
+ */
+/mob/living/basic/mining/legion
+ name = "legion"
+ desc = "You can still see what was once a human under the shifting mass of corruption."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion"
+ icon_living = "legion"
+ icon_dead = "legion"
+ icon_gib = "syndicate_gib"
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ basic_mob_flags = DEL_ON_DEATH
+ speed = 3
+ maxHealth = 75
+ health = 75
+ obj_damage = 60
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ attack_verb_continuous = "lashes out at"
+ attack_verb_simple = "lash out at"
+ speak_emote = list("gurgles")
+ attack_sound = 'sound/weapons/pierce.ogg'
+ throw_blocked_message = "bounces harmlessly off of"
+ crusher_loot = /obj/item/crusher_trophy/legion_skull
+ death_message = "wails in chorus and dissolves into quivering flesh."
+ ai_controller = /datum/ai_controller/basic_controller/legion
+ /// What kind of mob do we spawn?
+ var/brood_type = /mob/living/basic/legion_brood
+ /// What kind of corpse spawner do we leave behind on death?
+ var/corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested
+ /// Who is inside of us?
+ var/mob/living/stored_mob
+
+/mob/living/basic/mining/legion/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/death_drops, get_loot_list())
+ AddElement(/datum/element/content_barfer)
+
+ var/datum/action/cooldown/mob_cooldown/skull_launcher/skull_launcher = new(src)
+ skull_launcher.Grant(src)
+ skull_launcher.spawn_type = brood_type
+ ai_controller.blackboard[BB_TARGETTED_ACTION] = skull_launcher
+
+/// Create what we want to drop on death, in proc form so we can always return a static list
+/mob/living/basic/mining/legion/proc/get_loot_list()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
+ return death_loot
+
+/mob/living/basic/mining/legion/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != stored_mob)
+ return
+ ai_controller.clear_blackboard_key(BB_LEGION_CORPSE)
+ stored_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_LEGION_EATEN)
+ stored_mob.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman/long_term) // This will still probably mostly be gone before you are alive
+ stored_mob = null
+
+/mob/living/basic/mining/legion/death(gibbed)
+ if (isnull(stored_mob))
+ new corpse_type(loc)
+ return ..()
+
+/// Put a corpse in this guy
+/mob/living/basic/mining/legion/proc/consume(mob/living/consumed)
+ new /obj/effect/gibspawner/generic(consumed.loc)
+ gender = consumed.gender
+ name = consumed.real_name
+ consumed.investigate_log("has been killed by hivelord infestation.", INVESTIGATE_DEATHS)
+ consumed.death()
+ consumed.extinguish_mob()
+ consumed.fully_heal(HEAL_DAMAGE)
+ consumed.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_LEGION_EATEN)
+ consumed.forceMove(src)
+ ai_controller?.set_blackboard_key(BB_LEGION_CORPSE, consumed)
+ ai_controller?.set_blackboard_key(BB_LEGION_RECENT_LINES, consumed.copy_recent_speech(line_chance = 80))
+ stored_mob = consumed
+ visible_message(span_warning("[src] staggers to [p_their()] feet!"))
+ if (prob(75))
+ return
+ // Congratulations you have won a special prize: cancer
+ var/obj/item/organ/internal/legion_tumour/cancer = new()
+ cancer.Insert(consumed, special = TRUE, drop_if_replaced = FALSE)
+
+/// A Legion which only drops skeletons instead of corpses which might have fun loot, so it cannot be farmed
+/mob/living/basic/mining/legion/spawner_made
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred
+
+
+/// Like a Legion but it's an adorable snowman
+/mob/living/basic/mining/legion/snow
+ name = "snow legion"
+ desc = "You can vaguely see what was once a human under the densely packed snow. Cute, but macabre."
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "snowlegion"
+ icon_living = "snowlegion"
+ // icon_aggro = "snowlegion_alive"
+ icon_dead = "snowlegion"
+ brood_type = /mob/living/basic/legion_brood/snow
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/snow
+
+/mob/living/basic/mining/legion/snow/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/appearance_on_aggro, aggro_state = "snowlegion_alive") // Surprise! I was real!
+
+/// As Snow Legion but spawns corpses which don't have any exciting loot
+/mob/living/basic/mining/legion/snow/spawner_made
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/skeleton
+
+
+/// Like a Legion but shorter and faster
+/mob/living/basic/mining/legion/dwarf
+ name = "dwarf legion"
+ desc = "You can still see what was once a rather small human under the shifting mass of corruption."
+ icon_state = "dwarf_legion"
+ icon_living = "dwarf_legion"
+ icon_dead = "dwarf_legion"
+ maxHealth = 60
+ health = 60
+ speed = 2
+ crusher_drop_chance = 20
+ corpse_type = /obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
+
+
+/// Like a Legion but larger and spawns regular Legions, not currently used anywhere and very soulful
+/mob/living/basic/mining/legion/large
+ name = "myriad"
+ desc = "A legion of legions, a dead end to whatever form the Necropolis was attempting to create."
+ icon = 'icons/mob/simple/lavaland/64x64megafauna.dmi'
+ icon_state = "legion"
+ icon_living = "legion"
+ icon_dead = "legion"
+ health_doll_icon = "legion"
+ speed = 5
+ health = 450
+ maxHealth = 450
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ obj_damage = 30
+ pixel_x = -16
+ sentience_type = SENTIENCE_BOSS
+
+/mob/living/basic/mining/legion/large/Initialize(mapload)
+ . = ..()
+ AddComponent(\
+ /datum/component/spawner,\
+ spawn_types = list(/mob/living/basic/mining/legion),\
+ spawn_time = 20 SECONDS,\
+ max_spawned = 3,\
+ spawn_text = "peels itself off from",\
+ faction = faction,\
+ )
+
+/// Create what we want to drop on death, in proc form so we can always return a static list
+/mob/living/basic/mining/legion/large/get_loot_list()
+ var/static/list/death_loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 4)
+ return death_loot
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
new file mode 100644
index 00000000000..6b3525cb32a
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm
@@ -0,0 +1,77 @@
+/// Keep away and launch skulls at every opportunity, prioritising injured allies
+/datum/ai_controller/basic_controller/legion
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ BB_BASIC_MOB_FLEEING = TRUE,
+ BB_AGGRO_RANGE = 5, // Unobservant
+ BB_BASIC_MOB_FLEE_DISTANCE = 6,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/random_speech/legion,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/flee_target/legion,
+ )
+
+/// Chase and attack whatever we are targetting, if it's friendly we will heal them
+/datum/ai_controller/basic_controller/legion_brood
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead/legion,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Target nearby friendlies if they are hurt (and are not themselves Legions)
+/datum/targetting_datum/basic/attack_until_dead/legion
+
+/datum/targetting_datum/basic/attack_until_dead/legion/faction_check(mob/living/living_mob, mob/living/the_target)
+ if (!living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly))
+ return FALSE
+ if (istype(the_target, living_mob.type))
+ return TRUE
+ var/atom/created_by = living_mob.ai_controller.blackboard[BB_LEGION_BROOD_CREATOR]
+ if (!QDELETED(created_by) && istype(the_target, created_by.type))
+ return TRUE
+ return the_target.stat == DEAD || the_target.health >= the_target.maxHealth
+
+/// Don't run away from friendlies
+/datum/ai_planning_subtree/flee_target/legion
+
+/datum/ai_planning_subtree/flee_target/legion/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/target = controller.blackboard[target_key]
+ if (QDELETED(target) || target.faction_check_mob(controller.pawn))
+ return // Only flee if we have a hostile target
+ return ..()
+
+/// Make spooky sounds, if we have a corpse inside then impersonate them
+/datum/ai_planning_subtree/random_speech/legion
+ speech_chance = 1
+ speak = list("Come...", "Legion...", "Why...?")
+ emote_hear = list("groans.", "wails.", "whimpers.")
+ emote_see = list("twitches.", "shudders.")
+ /// Stuff to specifically say into a radio
+ var/list/radio_speech = list("Come...", "Why...?")
+
+/datum/ai_planning_subtree/random_speech/legion/speak(datum/ai_controller/controller)
+ var/mob/living/carbon/human/victim = controller.blackboard[BB_LEGION_CORPSE]
+ if (QDELETED(victim) || prob(30))
+ return ..()
+
+ var/list/remembered_speech = controller.blackboard[BB_LEGION_RECENT_LINES] || list()
+
+ if (length(remembered_speech) && prob(50)) // Don't spam the radio
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(remembered_speech))
+ return
+
+ var/obj/item/radio/mob_radio = locate() in victim.contents
+ if (QDELETED(mob_radio))
+ return ..() // No radio, just talk funny
+ controller.queue_behavior(/datum/ai_behavior/perform_speech_radio, pick(radio_speech + remembered_speech), mob_radio, list(RADIO_CHANNEL_SUPPLY, RADIO_CHANNEL_COMMON))
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
new file mode 100644
index 00000000000..bc21bd0e506
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm
@@ -0,0 +1,99 @@
+/// A spooky skull which heals lavaland mobs, attacks miners, and infests their bodies
+/mob/living/basic/legion_brood
+ name = "legion"
+ desc = "One of many."
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ icon_living = "legion_head"
+ icon_dead = "legion_head"
+ icon_gib = "syndicate_gib"
+ basic_mob_flags = DEL_ON_DEATH
+ mob_size = MOB_SIZE_SMALL
+ pass_flags = PASSTABLE | PASSMOB
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ faction = list(FACTION_MINING)
+ unsuitable_atmos_damage = 0
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ friendly_verb_continuous = "chatters near"
+ friendly_verb_simple = "chatter near"
+ maxHealth = 1
+ health = 1
+ melee_damage_lower = 12
+ melee_damage_upper = 12
+ obj_damage = 0
+ attack_verb_continuous = "bites"
+ attack_verb_simple = "bite"
+ attack_vis_effect = ATTACK_EFFECT_BITE
+ speak_emote = list("echoes") // who the fuck speaking as this mob it dies 10 seconds after it spawns
+ attack_sound = 'sound/weapons/pierce.ogg'
+ density = FALSE
+ ai_controller = /datum/ai_controller/basic_controller/legion_brood
+ /// Reference to a guy who made us
+ var/mob/living/created_by
+
+/mob/living/basic/legion_brood/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT)
+ AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/swarming)
+ AddComponent(/datum/component/clickbox, icon_state = "sphere", max_scale = 2)
+ addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
+
+/mob/living/basic/legion_brood/death(gibbed)
+ if (!gibbed)
+ new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
+ return ..()
+
+/mob/living/basic/legion_brood/melee_attack(mob/living/target, list/modifiers, ignore_cooldown)
+ if (ishuman(target) && target.stat > SOFT_CRIT)
+ infest(target)
+ return
+ if (isliving(target) && faction_check_mob(target) && !istype(target, created_by?.type))
+ visible_message(span_warning("[src] melds with [target]'s flesh!"))
+ target.apply_status_effect(/datum/status_effect/regenerative_core)
+ new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
+ death()
+ return
+ return ..()
+
+/// Turn the targetted mob into one of us
+/mob/living/basic/legion_brood/proc/infest(mob/living/target)
+ visible_message(span_warning("[name] burrows into the flesh of [target]!"))
+ var/spawn_type = get_legion_type(target)
+ var/mob/living/basic/mining/legion/new_legion = new spawn_type(loc)
+ new_legion.consume(target)
+ new_legion.faction = faction.Copy()
+ qdel(src)
+
+/// Returns the kind of legion we make out of the target
+/mob/living/basic/legion_brood/proc/get_legion_type(mob/living/target)
+ if (HAS_TRAIT(target, TRAIT_DWARF))
+ return /mob/living/basic/mining/legion/dwarf
+ return /mob/living/basic/mining/legion
+
+/// Sets someone as our creator, mostly so you can't use skulls to heal yourself
+/mob/living/basic/legion_brood/proc/assign_creator(mob/living/creator, copy_full_faction = TRUE)
+ if (copy_full_faction)
+ faction = creator.faction.Copy()
+ else
+ faction |= REF(creator)
+ created_by = creator
+ ai_controller?.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
+ RegisterSignal(creator, COMSIG_QDELETING, PROC_REF(creator_destroyed))
+
+/// Reference handling
+/mob/living/basic/legion_brood/proc/creator_destroyed()
+ SIGNAL_HANDLER
+ created_by = null
+
+/// Like the Legion's summoned skull but funnier (it's snow now)
+/mob/living/basic/legion_brood/snow
+ name = "snow legion"
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "snowlegion_head"
+ icon_living = "snowlegion_head"
+ icon_dead = "snowlegion_head"
+
+/mob/living/basic/legion_brood/snow/get_legion_type(mob/living/target)
+ return /mob/living/basic/mining/legion/snow
diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm b/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm
new file mode 100644
index 00000000000..078af57de2a
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/legion_tumour.dm
@@ -0,0 +1,159 @@
+/// Left behind when a legion infects you, for medical enrichment
+/obj/item/organ/internal/legion_tumour
+ name = "legion tumour"
+ desc = "A mass of pulsing flesh and dark tendrils, containing the power to regenerate flesh at a terrible cost."
+ failing_desc = "pulses and writhes with horrible life, reaching towards you with its tendrils!"
+ icon = 'icons/obj/medical/organs/mining_organs.dmi'
+ icon_state = "legion_remains"
+ zone = BODY_ZONE_CHEST
+ slot = ORGAN_SLOT_PARASITE_EGG
+ decay_factor = STANDARD_ORGAN_DECAY * 3 // About 5 minutes outside of a host
+ /// What stage of growth the corruption has reached.
+ var/stage = 0
+ /// We apply this status effect periodically or when used on someone
+ var/applied_status = /datum/status_effect/regenerative_core
+ /// How long have we been in this stage?
+ var/elapsed_time = 0 SECONDS
+ /// How long does it take to advance one stage?
+ var/growth_time = 80 SECONDS // Long enough that if you go back to lavaland without realising it you're not totally fucked
+ /// What kind of mob will we transform into?
+ var/spawn_type = /mob/living/basic/mining/legion
+ /// Spooky sounds to play as you start to turn
+ var/static/list/spooky_sounds = list(
+ 'sound/voice/lowHiss1.ogg',
+ 'sound/voice/lowHiss2.ogg',
+ 'sound/voice/lowHiss3.ogg',
+ 'sound/voice/lowHiss4.ogg',
+ )
+
+/obj/item/organ/internal/legion_tumour/Initialize(mapload)
+ . = ..()
+ animate_pulse()
+
+/obj/item/organ/internal/legion_tumour/apply_organ_damage(damage_amount, maximum, required_organ_flag)
+ var/was_failing = organ_flags & ORGAN_FAILING
+ . = ..()
+ if (was_failing != (organ_flags & ORGAN_FAILING))
+ animate_pulse()
+
+/obj/item/organ/internal/legion_tumour/set_organ_damage(damage_amount, required_organ_flag)
+ . = ..()
+ animate_pulse()
+
+/// Do a heartbeat animation depending on if we're failing or not
+/obj/item/organ/internal/legion_tumour/proc/animate_pulse()
+ animate(src, transform = matrix()) // Stop any current animation
+
+ var/speed_divider = organ_flags & ORGAN_FAILING ? 2 : 1
+
+ animate(src, transform = matrix().Scale(1.1), time = 0.5 SECONDS / speed_divider, easing = SINE_EASING | EASE_OUT, loop = -1, flags = ANIMATION_PARALLEL)
+ animate(transform = matrix(), time = 0.5 SECONDS / speed_divider, easing = SINE_EASING | EASE_IN)
+ animate(transform = matrix(), time = 2 SECONDS / speed_divider)
+
+/obj/item/organ/internal/legion_tumour/Remove(mob/living/carbon/egg_owner, special)
+ . = ..()
+ stage = 0
+ elapsed_time = 0
+
+/obj/item/organ/internal/legion_tumour/attack(mob/living/target, mob/living/user, params)
+ if (try_apply(target, user))
+ qdel(src)
+ return
+ return ..()
+
+/// Smear it on someone like a regen core, why not. Make sure they're alive though.
+/obj/item/organ/internal/legion_tumour/proc/try_apply(mob/living/target, mob/user)
+ if(!user.Adjacent(target) || !isliving(target))
+ return FALSE
+
+ if (target.stat <= SOFT_CRIT && !(organ_flags & ORGAN_FAILING))
+ target.add_mood_event(MOOD_CATEGORY_LEGION_CORE, /datum/mood_event/healsbadman)
+ target.apply_status_effect(applied_status)
+
+ if (target != user)
+ target.visible_message(span_notice("[user] splatters [target] with [src]... Disgusting tendrils pull [target.p_their()] wounds shut!"))
+ else
+ to_chat(user, span_notice("You smear [src] on yourself. Disgusting tendrils pull your wounds closed."))
+ return TRUE
+
+ if (!ishuman(target))
+ return FALSE
+
+ target.visible_message(span_boldwarning("[user] splatters [target] with [src]... and it springs into horrible life!"))
+ var/mob/living/basic/legion_brood/skull = new(target.loc)
+ skull.melee_attack(target)
+ return TRUE
+
+/obj/item/organ/internal/legion_tumour/on_life(seconds_per_tick, times_fired)
+ . = ..()
+ if (QDELETED(src) || QDELETED(owner))
+ return
+
+ if (stage >= 2)
+ if(SPT_PROB(stage / 5, seconds_per_tick))
+ to_chat(owner, span_notice("You feel a bit better."))
+ owner.apply_status_effect(applied_status) // It's not all bad!
+ if(SPT_PROB(1, seconds_per_tick))
+ owner.emote("twitch")
+
+ switch(stage)
+ if(2, 3)
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(owner, span_danger("Your chest spasms!"))
+ if(SPT_PROB(1, seconds_per_tick))
+ to_chat(owner, span_danger("You feel weak."))
+ if(SPT_PROB(1, seconds_per_tick))
+ SEND_SOUND(owner, sound(pick(spooky_sounds)))
+ if(SPT_PROB(2, seconds_per_tick))
+ owner.vomit()
+ if(4, 5)
+ if(SPT_PROB(2, seconds_per_tick))
+ to_chat(owner, span_danger("Something flexes under your skin."))
+ if(SPT_PROB(2, seconds_per_tick))
+ if (prob(40))
+ SEND_SOUND(owner, sound('sound/voice/ghost_whisper.ogg'))
+ else
+ SEND_SOUND(owner, sound(pick(spooky_sounds)))
+ if(SPT_PROB(3, seconds_per_tick))
+ owner.vomit(vomit_type = /obj/effect/decal/cleanable/vomit/old/black_bile)
+ if (prob(50))
+ var/turf/check_turf = get_step(owner.loc, owner.dir)
+ var/atom/land_turf = (check_turf.is_blocked_turf()) ? owner.loc : check_turf
+ var/mob/living/basic/legion_brood/child = new(land_turf)
+ child.assign_creator(owner, copy_full_faction = FALSE)
+
+ if(SPT_PROB(3, seconds_per_tick))
+ to_chat(owner, span_danger("Your muscles ache."))
+ owner.take_bodypart_damage(3)
+
+ if (stage == 5)
+ if (SPT_PROB(10, seconds_per_tick))
+ infest()
+ return
+
+ elapsed_time += seconds_per_tick SECONDS * ((organ_flags & ORGAN_FAILING) ? 3 : 1) // Let's call it "matured" rather than failed
+ if (elapsed_time < growth_time)
+ return
+ stage++
+ elapsed_time = 0
+ if (stage == 5)
+ to_chat(owner, span_bolddanger("Something is moving under your skin!"))
+
+/// Consume our host
+/obj/item/organ/internal/legion_tumour/proc/infest()
+ if (QDELETED(src) || QDELETED(owner))
+ return
+ owner.visible_message(span_boldwarning("Black tendrils burst from [owner]'s flesh, covering them in amorphous flesh!"))
+ var/mob/living/basic/mining/legion/new_legion = new spawn_type(owner.loc)
+ new_legion.consume(owner)
+ qdel(src)
+
+/obj/item/organ/internal/legion_tumour/on_find(mob/living/finder)
+ . = ..()
+ to_chat(finder, span_warning("There's an enormous tumour in [owner]'s [zone]!"))
+ if(stage < 4)
+ to_chat(finder, span_notice("Its tendrils seem to twitch towards the light."))
+ return
+ to_chat(finder, span_notice("Its pulsing tendrils reach all throughout the body."))
+ if(prob(stage * 2))
+ infest()
diff --git a/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
new file mode 100644
index 00000000000..1ffcafecd56
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/legion/spawn_legions.dm
@@ -0,0 +1,109 @@
+/// Spawns a little worm nearby
+/datum/action/cooldown/mob_cooldown/skull_launcher
+ name = "Launch Legion"
+ desc = "Propel a living piece of your body to a distant location."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "legion_head"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = TRUE
+ cooldown_time = 2 SECONDS
+ melee_cooldown_time = 0
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
+ shared_cooldown = NONE
+ /// If a mob is not clicked directly, inherit targetting data from this blackboard key and setting it upon this target key
+ var/ai_target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// What are we actually spawning?
+ var/spawn_type = /mob/living/basic/legion_brood
+ /// How far can we fire?
+ var/max_range = 7
+
+/datum/action/cooldown/mob_cooldown/skull_launcher/Activate(atom/target)
+ var/turf/target_turf = get_turf(target)
+
+ if (get_dist(owner, target_turf) > max_range)
+ target_turf = get_ranged_target_turf_direct(owner, target_turf, max_range)
+
+ if (target_turf.is_blocked_turf())
+ var/list/near_turfs = RANGE_TURFS(1, target_turf) - target_turf
+ for (var/turf/check_turf as anything in near_turfs)
+ if (check_turf.is_blocked_turf())
+ near_turfs -= check_turf
+ if (length(near_turfs))
+ target_turf = pick(near_turfs)
+ else if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ owner.balloon_alert(owner, "no room!")
+ StartCooldown(0.5 SECONDS)
+ return
+
+ var/ai_target = isliving(target) ? target : null
+ if (isnull(ai_target))
+ ai_target = owner.ai_controller?.blackboard[ai_target_key]
+
+ var/target_dir = get_dir(owner, target)
+
+ var/obj/effect/temp_visual/legion_skull_depart/launch = new(get_turf(owner))
+ launch.set_appearance(spawn_type)
+ launch.dir = target_dir
+ new /obj/effect/temp_visual/legion_brood_indicator(target_turf)
+ var/obj/effect/temp_visual/legion_skull_land/land = new(target_turf)
+ land.dir = target_dir
+ land.set_appearance(spawn_type, CALLBACK(src, PROC_REF(spawn_skull), target_turf, ai_target))
+ StartCooldown()
+
+/// Actually create a mob
+/datum/action/cooldown/mob_cooldown/skull_launcher/proc/spawn_skull(turf/spawn_location, target)
+ var/mob/living/basic/legion_brood/brood = new spawn_type(spawn_location)
+ if (istype(brood))
+ brood.assign_creator(owner)
+ brood.ai_controller?.set_blackboard_key(ai_target_key, target)
+ brood.dir = get_dir(owner, spawn_location)
+ if (!isnull(target))
+ brood.face_atom(target)
+ else
+ brood.dir = get_dir(owner, spawn_location)
+
+
+/// Animation for launching a skull
+/obj/effect/temp_visual/legion_skull_depart
+ name = "legion brood launch"
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ duration = 0.25 SECONDS
+
+/// Copy appearance from the passed atom type
+/obj/effect/temp_visual/legion_skull_depart/proc/set_appearance(atom/spawned_type)
+ icon = initial(spawned_type.icon)
+ icon_state = initial(spawned_type.icon_state)
+ animate(src, alpha = 0, pixel_y = 72, time = duration)
+
+/// Animation for landing a skull
+/obj/effect/temp_visual/legion_skull_land
+ name = "legion brood land"
+ duration = 0.5 SECONDS
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "legion_head"
+ alpha = 0
+ pixel_y = 72
+
+/// Copy appearance from the passed atom type and store what to do on animation complete
+/obj/effect/temp_visual/legion_skull_land/proc/set_appearance(atom/spawned_type, datum/callback/on_completed)
+ icon = initial(spawned_type.icon)
+ icon_state = initial(spawned_type.icon_state)
+ animate(src, alpha = 0, pixel_y = 72, time = duration / 2)
+ animate(alpha = 255, pixel_y = 0, time = duration / 2)
+ addtimer(on_completed, duration, TIMER_DELETE_ME)
+
+/// A skull is going to be here! Oh no!
+/obj/effect/temp_visual/legion_brood_indicator
+ name = "legion brood land"
+ duration = 0.75 SECONDS
+ layer = BELOW_MOB_LAYER
+ plane = GAME_PLANE
+ icon = 'icons/mob/telegraphing/telegraph.dmi'
+ icon_state = "skull"
+
+/obj/effect/temp_visual/legion_brood_indicator/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 255, time = 0.5 SECONDS)
+ animate(alpha = 0, time = 0.25 SECONDS)
diff --git a/code/modules/mob/living/basic/lavaland/mining.dm b/code/modules/mob/living/basic/lavaland/mining.dm
index 3570936d2f5..0b6c4f321b6 100644
--- a/code/modules/mob/living/basic/lavaland/mining.dm
+++ b/code/modules/mob/living/basic/lavaland/mining.dm
@@ -9,6 +9,10 @@
unsuitable_atmos_damage = 0
minimum_survivable_temperature = 0
maximum_survivable_temperature = INFINITY
+ // Pale purple, should be red enough to see stuff on lavaland
+ lighting_cutoff_red = 25
+ lighting_cutoff_green = 15
+ lighting_cutoff_blue = 35
/// Message to output if throwing damage is absorbed
var/throw_blocked_message = "bounces off"
/// What crusher trophy this mob drops, if any
diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
index 1f310ac229f..a25234817f3 100644
--- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm
@@ -9,9 +9,9 @@
planning_subtrees = list(
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
/datum/ai_planning_subtree/use_mob_ability/gaze,
/datum/ai_planning_subtree/ranged_skirmish/watcher,
- /datum/ai_planning_subtree/maintain_distance,
)
/datum/ai_planning_subtree/use_mob_ability/gaze
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
index 5a7bb075f19..d49932fb704 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm
@@ -26,22 +26,9 @@
lighting_cutoff_green = 15
lighting_cutoff_blue = 40
- ai_controller = /datum/ai_controller/basic_controller/blankbody
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
/mob/living/basic/blankbody/Initialize(mapload)
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_NETHER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 0)
AddComponent(/datum/component/health_scaling_effects, min_health_attack_modifier_lower = 8, min_health_attack_modifier_upper = 14)
-
-/datum/ai_controller/basic_controller/blankbody
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- )
-
- ai_movement = /datum/ai_movement/basic_avoidance
- idle_behavior = /datum/idle_behavior/idle_random_walk
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
index b38ada0f6e1..cdde6ad05e4 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm
@@ -27,7 +27,7 @@
lighting_cutoff_green = 25
lighting_cutoff_blue = 15
- ai_controller = /datum/ai_controller/basic_controller/creature
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
/mob/living/basic/creature/Initialize(mapload)
. = ..()
@@ -101,16 +101,3 @@
exit_jaunt(cast_on)
return
enter_jaunt(cast_on)
-
-/datum/ai_controller/basic_controller/creature
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- )
-
- ai_movement = /datum/ai_movement/basic_avoidance
- idle_behavior = /datum/idle_behavior/idle_random_walk
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
index 18dca95013e..3f445ea1261 100644
--- a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
+++ b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm
@@ -28,7 +28,7 @@
lighting_cutoff_green = 15
lighting_cutoff_blue = 50
- ai_controller = /datum/ai_controller/basic_controller/migo
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles
var/static/list/migo_sounds
/// Odds migo will dodge
var/dodge_prob = 10
@@ -71,16 +71,3 @@
. = Move(get_step(loc,pick(cdir, ccdir)))
if(!.)//Can't dodge there so we just carry on
. = Move(moving_to, move_direction)
-
-/datum/ai_controller/basic_controller/migo
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
- )
-
- ai_movement = /datum/ai_movement/basic_avoidance
- idle_behavior = /datum/idle_behavior/idle_random_walk
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
index 3b32fbb4ce7..519e8ba1a73 100644
--- a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
+++ b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm
@@ -110,8 +110,7 @@
faction = list(FACTION_STICKMAN)
melee_damage_lower = 1
melee_damage_upper = 5
-
- ai_controller = /datum/ai_controller/basic_controller/wizard_copy
+ ai_controller = /datum/ai_controller/basic_controller/simple_hostile
/mob/living/basic/paper_wizard/copy/Initialize(mapload)
. = ..()
@@ -141,18 +140,6 @@
new /obj/effect/temp_visual/small_smoke/halfsecond(get_turf(src))
qdel(src) //I see through your ruse!
-/datum/ai_controller/basic_controller/wizard_copy
- blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
- )
-
- ai_movement = /datum/ai_movement/basic_avoidance
- idle_behavior = /datum/idle_behavior/idle_random_walk
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
-
//fancy effects
/obj/effect/temp_visual/paper_scatter
name = "scattering paper"
diff --git a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
index 57b9da542b7..50ec85e342c 100644
--- a/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
+++ b/code/modules/mob/living/basic/space_fauna/spider/young_spider/young_spider.dm
@@ -30,6 +30,7 @@
/datum/ai_controller/basic_controller/young_spider
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
+ BB_BASIC_MOB_FLEE_DISTANCE = 6,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -46,6 +47,3 @@
/datum/ai_planning_subtree/find_unwebbed_turf,
/datum/ai_planning_subtree/spin_web,
)
-
-/datum/ai_behavior/run_away_from_target/young_spider
- run_distance = 6
diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm
index 9e8f1b8df55..46e175c5323 100644
--- a/code/modules/mob/living/basic/vermin/mouse.dm
+++ b/code/modules/mob/living/basic/vermin/mouse.dm
@@ -381,6 +381,7 @@
BB_CURRENT_HUNTING_TARGET = null, // cheese
BB_LOW_PRIORITY_HUNTING_TARGET = null, // cable
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), // Use this to find people to run away from
+ BB_BASIC_MOB_FLEE_DISTANCE = 3,
)
ai_traits = STOP_MOVING_WHEN_PULLED
@@ -400,9 +401,6 @@
)
/// Don't look for anything to run away from if you are distracted by being adjacent to cheese
-/datum/ai_planning_subtree/flee_target/mouse
- flee_behaviour = /datum/ai_behavior/run_away_from_target/mouse
-
/datum/ai_planning_subtree/flee_target/mouse
/datum/ai_planning_subtree/flee_target/mouse/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
@@ -411,11 +409,6 @@
return // We see some cheese, which is more important than our life
return ..()
-/datum/ai_planning_subtree/flee_target/mouse/select
-
-/datum/ai_behavior/run_away_from_target/mouse
- run_distance = 3 // Mostly exists in small tunnels, don't get ahead of yourself
-
/// AI controller for rats, slightly more complex than mice becuase they attack people
/datum/ai_controller/basic_controller/mouse/rat
blackboard = list(
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
index 745a48c948c..828a78ccfb1 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
@@ -436,7 +436,7 @@ Difficulty: Hard
/mob/living/simple_animal/hostile/megafauna/hierophant/CanAttack(atom/the_target)
. = ..()
- if(istype(the_target, /mob/living/simple_animal/hostile/asteroid/hivelordbrood)) //ignore temporary targets in favor of more permanent targets
+ if(istype(the_target, /mob/living/basic/legion_brood)) //ignore temporary targets in favor of more permanent targets
return FALSE
/mob/living/simple_animal/hostile/megafauna/hierophant/GiveTarget(new_target)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
index 05695daf59a..777cb3b878f 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/legion.dm
@@ -141,10 +141,9 @@
///Attack proc. Spawns a singular legion skull.
/mob/living/simple_animal/hostile/megafauna/legion/proc/create_legion_skull()
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new(loc)
- A.GiveTarget(target)
- A.friends = friends
- A.faction = faction
+ var/mob/living/basic/legion_brood/minion = new(loc)
+ minion.assign_creator(src)
+ minion.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] = target
//CHARGE
@@ -210,7 +209,7 @@
var/mob/living/living_target = target
switch(living_target.stat)
if(UNCONSCIOUS, HARD_CRIT)
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/legion = new(loc)
+ var/mob/living/basic/legion_brood/legion = new(loc)
legion.infest(living_target)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
index a705e11465b..ec6c843080c 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
@@ -328,10 +328,9 @@
/obj/item/crusher_trophy/legionnaire_spine/on_mark_detonation(mob/living/target, mob/living/user)
if(!prob(bonus_value) || target.stat == DEAD)
return
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/A = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(user.loc)
- A.GiveTarget(target)
- A.friends += user
- A.faction = user.faction.Copy()
+ var/mob/living/basic/legion_brood/minion = new (user.loc)
+ minion.assign_creator(user)
+ minion.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] = target
/obj/item/crusher_trophy/legionnaire_spine/attack_self(mob/user)
if(!isliving(user))
@@ -342,9 +341,9 @@
to_chat(LivingUser, "You need to wait longer to use this again.")
return
LivingUser.visible_message(span_boldwarning("[LivingUser] shakes the [src] and summons a legion skull!"))
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/LegionSkull = new /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion(LivingUser.loc)
- LegionSkull.friends += LivingUser
- LegionSkull.faction = LivingUser.faction.Copy()
+
+ var/mob/living/basic/legion_brood/minion = new (LivingUser.loc)
+ minion.assign_creator(LivingUser)
next_use_time = world.time + 4 SECONDS
#undef LEGIONNAIRE_CHARGE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
deleted file mode 100644
index 20ac9642e01..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
+++ /dev/null
@@ -1,332 +0,0 @@
-/mob/living/simple_animal/hostile/asteroid/hivelord
- name = "hivelord"
- desc = "A levitating swarm of tiny creatures which act as a single individual. When threatened or hunting they rapidly replicate additional short-lived bodies."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "hivelord"
- icon_living = "hivelord"
- icon_aggro = "hivelord_alert"
- icon_dead = "hivelord_dead"
- icon_gib = "syndicate_gib"
- mob_biotypes = MOB_ORGANIC
- move_to_delay = 14
- ranged = 1
- vision_range = 5
- aggro_vision_range = 9
- speed = 3
- maxHealth = 75
- health = 75
- harm_intent_damage = 5
- melee_damage_lower = 0
- melee_damage_upper = 0
- attack_verb_continuous = "weakly tackles"
- attack_verb_simple = "weakly tackles"
- speak_emote = list("telepathically cries")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "passes between the bodies of the"
- ranged_cooldown = 0
- ranged_cooldown_time = 20
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- retreat_distance = 3
- minimum_distance = 3
- pass_flags = PASSTABLE
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core)
- var/brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood
- var/has_clickbox = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/Initialize(mapload)
- . = ..()
- if(has_clickbox)
- AddComponent(/datum/component/clickbox, icon_state = "hivelord", max_scale = INFINITY, dead_state = "hivelord_dead") //they writhe so much.
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/OpenFire(the_target)
- if(world.time < ranged_cooldown)
- return
- var/mob/living/simple_animal/hostile/asteroid/hivelordbrood/brood = new brood_type(src.loc)
- brood.flags_1 |= (flags_1 & ADMIN_SPAWNED_1)
- brood.GiveTarget(target)
- brood.friends = friends
- brood.faction = faction.Copy()
- ranged_cooldown = world.time + ranged_cooldown_time
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/AttackingTarget()
- OpenFire()
- return TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/death(gibbed)
- mouse_opacity = MOUSE_OPACITY_ICON
- return ..()
-
-//A fragile but rapidly produced creature
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood
- name = "hivelord brood"
- desc = "Short-lived attack form of the hivelord. One isn't much of a threat, but..."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "hivelord_brood"
- icon_living = "hivelord_brood"
- icon_aggro = "hivelord_brood"
- icon_dead = "hivelord_brood"
- icon_gib = "syndicate_gib"
- move_to_delay = 1
- friendly_verb_continuous = "buzzes near"
- friendly_verb_simple = "buzz near"
- vision_range = 10
- speed = 3
- maxHealth = 1
- health = 1
- harm_intent_damage = 5
- melee_damage_lower = 2
- melee_damage_upper = 2
- attack_verb_continuous = "slashes"
- attack_verb_simple = "slash"
- speak_emote = list("telepathically cries")
- attack_sound = 'sound/weapons/pierce.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- throw_message = "falls right through the strange body of the"
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- pass_flags = PASSTABLE | PASSMOB
- density = FALSE
- del_on_death = 1
- var/clickbox_state = "hivelord"
- var/clickbox_max_scale = INFINITY
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/Initialize(mapload)
- . = ..()
- addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
- AddElement(/datum/element/simple_flying)
- AddComponent(/datum/component/swarming)
- AddComponent(/datum/component/clickbox, icon_state = clickbox_state, max_scale = clickbox_max_scale)
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/death(gibbed)
- if (!gibbed)
- new /obj/effect/temp_visual/hive_spawn_wither(get_turf(src), /* copy_from = */ src)
- return ..()
-
-//Legion
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion
- name = "legion"
- desc = "You can still see what was once a human under the shifting mass of corruption."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "legion"
- icon_living = "legion"
- icon_aggro = "legion"
- icon_dead = "legion"
- icon_gib = "syndicate_gib"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- mouse_opacity = MOUSE_OPACITY_ICON
- obj_damage = 60
- melee_damage_lower = 15
- melee_damage_upper = 15
- attack_verb_continuous = "lashes out at"
- attack_verb_simple = "lash out at"
- speak_emote = list("echoes")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "bounces harmlessly off of"
- crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion
- del_on_death = 1
- stat_attack = HARD_CRIT
- robust_searching = 1
- has_clickbox = FALSE
- var/dwarf_mob = FALSE
- var/snow_legion = FALSE
- var/mob/living/carbon/human/stored_mob
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/content_barfer)
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf
- name = "dwarf legion"
- desc = "You can still see what was once a rather small human under the shifting mass of corruption."
- icon_state = "dwarf_legion"
- icon_living = "dwarf_legion"
- icon_aggro = "dwarf_legion"
- icon_dead = "dwarf_legion"
- maxHealth = 60
- health = 60
- speed = 2 //faster!
- crusher_drop_mod = 20
- dwarf_mob = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/death(gibbed)
- visible_message(span_warning("The skulls on [src] wail in anger as they flee from their dying host!"))
- if (!isnull(stored_mob))
- stored_mob = null
- return ..()
-
- // We didn't contain a real body so spawn a random one
- var/turf/our_turf = get_turf(src)
- if(our_turf)
- if(from_spawner)
- new /obj/effect/mob_spawn/corpse/human/charredskeleton(our_turf)
- else if(dwarf_mob)
- new /obj/effect/mob_spawn/corpse/human/legioninfested/dwarf(our_turf)
- else if(snow_legion)
- new /obj/effect/mob_spawn/corpse/human/snowlegioninfested(our_turf)
- else
- new /obj/effect/mob_spawn/corpse/human/legioninfested(our_turf)
- return ..()
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril
- from_spawner = TRUE
-
-//Legion skull
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion
- name = "legion"
- desc = "One of many."
- icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
- icon_state = "legion_head"
- icon_living = "legion_head"
- icon_aggro = "legion_head"
- icon_dead = "legion_head"
- icon_gib = "syndicate_gib"
- friendly_verb_continuous = "buzzes near"
- friendly_verb_simple = "buzz near"
- vision_range = 10
- maxHealth = 1
- health = 5
- harm_intent_damage = 5
- melee_damage_lower = 12
- melee_damage_upper = 12
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- attack_vis_effect = ATTACK_EFFECT_BITE
- speak_emote = list("echoes")
- attack_sound = 'sound/weapons/pierce.ogg'
- throw_message = "is shrugged off by"
- del_on_death = TRUE
- stat_attack = HARD_CRIT
- robust_searching = 1
- clickbox_state = "sphere"
- clickbox_max_scale = 2
- var/can_infest_dead = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- . = ..()
- if(stat == DEAD || !isturf(loc))
- return
- for(var/mob/living/carbon/human/victim in range(src, 1)) //Only for corpse right next to/on same tile
- switch(victim.stat)
- if(UNCONSCIOUS, HARD_CRIT)
- infest(victim)
- return //This will qdelete the legion.
- if(DEAD)
- if(can_infest_dead)
- infest(victim)
- return //This will qdelete the legion.
-
-///Create a legion at the location of a corpse. Exists so that legion subtypes can override it with their own type of legion.
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/proc/make_legion(mob/living/carbon/human/H)
- if(HAS_TRAIT(H, TRAIT_DWARF)) //dwarf legions aren't just fluff!
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf(H.loc)
- else
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion(H.loc)
-
-///Create a new legion using the supplied human H
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/proc/infest(mob/living/carbon/human/H)
- visible_message(span_warning("[name] burrows into the flesh of [H]!"))
- var/mob/living/simple_animal/hostile/asteroid/hivelord/legion/L = make_legion(H)
- visible_message(span_warning("[L] staggers to [L.p_their()] feet!"))
- H.investigate_log("has been killed by hivelord infestation.", INVESTIGATE_DEATHS)
- H.death()
- H.adjustBruteLoss(1000)
- L.stored_mob = H
- H.forceMove(L)
- qdel(src)
-
-//Advanced Legion is slightly tougher to kill and can raise corpses (revive other legions)
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/advanced
- stat_attack = DEAD
- maxHealth = 120
- health = 120
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced
- icon_state = "dwarf_legion"
- icon_living = "dwarf_legion"
- icon_aggro = "dwarf_legion"
- icon_dead = "dwarf_legion"
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced
- stat_attack = DEAD
- can_infest_dead = TRUE
-
-//Legion that spawns Legions
-/mob/living/simple_animal/hostile/big_legion
- name = "legion"
- desc = "One of many."
- icon = 'icons/mob/simple/lavaland/64x64megafauna.dmi'
- icon_state = "legion"
- icon_living = "legion"
- icon_dead = "legion"
- health_doll_icon = "legion"
- health = 450
- maxHealth = 450
- melee_damage_lower = 20
- melee_damage_upper = 20
- anchored = FALSE
- AIStatus = AI_ON
- stop_automated_movement = FALSE
- wander = TRUE
- maxbodytemp = INFINITY
- layer = MOB_LAYER
- del_on_death = TRUE
- sentience_type = SENTIENCE_BOSS
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 5)
- move_to_delay = 14
- vision_range = 5
- aggro_vision_range = 9
- speed = 3
- faction = list(FACTION_MINING)
- weather_immunities = list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE)
- obj_damage = 30
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- // Purple, but bright cause we're gonna need to spot mobs on lavaland
- lighting_cutoff_red = 35
- lighting_cutoff_green = 20
- lighting_cutoff_blue = 45
-
-
-/mob/living/simple_animal/hostile/big_legion/Initialize(mapload)
- .=..()
- AddComponent(\
- /datum/component/spawner,\
- spawn_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion),\
- spawn_time = 20 SECONDS,\
- max_spawned = 3,\
- spawn_text = "peels itself off from",\
- faction = faction,\
- )
-
-// Snow Legion
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow
- name = "snow legion"
- desc = "You can still see what was once a human under the shifting snowy mass, clearly decorated by a clown."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "snowlegion"
- icon_living = "snowlegion"
- icon_aggro = "snowlegion_alive"
- icon_dead = "snowlegion"
- crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/internal/monster_core/regenerative_core/legion)
- brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
- snow_legion = TRUE
-
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow/make_legion(mob/living/carbon/human/H)
- return new /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow(H.loc)
-
-/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow/portal
- from_spawner = TRUE
-
-// Snow Legion skull
-/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow
- name = "snow legion"
- desc = "One of many."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "snowlegion_head"
- icon_living = "snowlegion_head"
- icon_aggro = "snowlegion_head"
- icon_dead = "snowlegion_head"
- weather_immunities = list(TRAIT_SNOWSTORM_IMMUNE)
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index c5b240b481f..0e0e208c596 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -537,3 +537,33 @@
"[key_name(src)] manually changed selected zone",
data,
)
+
+/**
+ * Returns an associative list of the logs of a certain amount of lines spoken recently by this mob
+ * copy_amount - number of lines to return
+ * line_chance - chance to return a line, if you don't want just the most recent x lines
+ */
+/mob/proc/copy_recent_speech(copy_amount = LING_ABSORB_RECENT_SPEECH, line_chance = 100)
+ var/list/recent_speech = list()
+ var/list/say_log = list()
+ var/log_source = logging
+ for(var/log_type in log_source)
+ var/nlog_type = text2num(log_type)
+ if(nlog_type & LOG_SAY)
+ var/list/reversed = log_source[log_type]
+ if(islist(reversed))
+ say_log = reverse_range(reversed.Copy())
+ break
+
+ for(var/spoken_memory in say_log)
+ if(recent_speech.len >= copy_amount)
+ break
+ if(!prob(line_chance))
+ continue
+ recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
+
+ var/list/raw_lines = list()
+ for (var/key as anything in recent_speech)
+ raw_lines += recent_speech[key]
+
+ return raw_lines
diff --git a/code/modules/mob_spawn/corpses/mining_corpses.dm b/code/modules/mob_spawn/corpses/mining_corpses.dm
index c0ea4b6af42..8b7ad474b16 100644
--- a/code/modules/mob_spawn/corpses/mining_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mining_corpses.dm
@@ -16,18 +16,21 @@
//Legion infested mobs
-//dwarf type which spawns dwarfy versions
-/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
-
-/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf/special(mob/living/carbon/human/spawned_human)
- . = ..()
- spawned_human.dna.add_mutation(/datum/mutation/human/dwarfism)
-
-//main type, rolls a pool of legion victims
+/// Mob spawner used by Legion to spawn costumed bodies
/obj/effect/mob_spawn/corpse/human/legioninfested
brute_damage = 1000
/obj/effect/mob_spawn/corpse/human/legioninfested/Initialize(mapload)
+ outfit = select_outfit()
+ return ..()
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ var/obj/item/organ/internal/legion_tumour/cancer = new()
+ cancer.Insert(spawned_human, special = TRUE, drop_if_replaced = FALSE)
+
+/// Returns the outfit worn by our corpse
+/obj/effect/mob_spawn/corpse/human/legioninfested/proc/select_outfit()
var/corpse_theme = pick_weight(list(
"Miner" = 64,
"Clown" = 5,
@@ -40,26 +43,36 @@
"Shadow",
)) = 4,
))
+
switch(corpse_theme)
if("Miner")
- outfit = /datum/outfit/consumed_miner
+ return /datum/outfit/consumed_miner
if("Ashwalker")
- outfit = /datum/outfit/consumed_ashwalker
+ return /datum/outfit/consumed_ashwalker
if("Golem")
- outfit = /datum/outfit/consumed_golem
+ return /datum/outfit/consumed_golem
if("Clown")
- outfit = /datum/outfit/consumed_clown
+ return /datum/outfit/consumed_clown
if("Cultist")
- outfit = /datum/outfit/consumed_cultist
+ return /datum/outfit/consumed_cultist
if("Dame")
- outfit = /datum/outfit/consumed_dame
+ return /datum/outfit/consumed_dame
if("Operative")
- outfit = /datum/outfit/syndicatecommandocorpse/lessenedgear
+ return /datum/outfit/syndicatecommandocorpse/lessenedgear
if("Shadow")
- outfit = /datum/outfit/consumed_shadowperson
- . = ..()
+ return /datum/outfit/consumed_shadowperson
-/obj/effect/mob_spawn/corpse/human/snowlegioninfested/Initialize(mapload)
+/// Corpse spawner used by dwarf legions to make small corpses
+/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/dwarf/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ spawned_human.dna.add_mutation(/datum/mutation/human/dwarfism)
+
+/// Corpse spawner used by snow legions with alternate costumes
+/obj/effect/mob_spawn/corpse/human/legioninfested/snow
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/snow/select_outfit()
var/corpse_theme = pick_weight(list(
"Miner" = 64,
"Clown" = 5,
@@ -72,24 +85,49 @@
"Shadow",
)) = 4,
))
+
switch(corpse_theme)
if("Miner")
- outfit = /datum/outfit/consumed_miner
+ return /datum/outfit/consumed_miner
if("Settler")
- outfit = /datum/outfit/consumed_ice_settler
+ return /datum/outfit/consumed_ice_settler
if("Heremoth")
- outfit = /datum/outfit/consumed_heremoth
+ return /datum/outfit/consumed_heremoth
if("Clown")
- outfit = /datum/outfit/consumed_clown
+ return /datum/outfit/consumed_clown
if("Cultist")
- outfit = /datum/outfit/consumed_cultist
+ return /datum/outfit/consumed_cultist
if("Golem")
- outfit = /datum/outfit/consumed_golem
+ return /datum/outfit/consumed_golem
if("Operative")
- outfit = /datum/outfit/syndicatecommandocorpse/lessenedgear
+ return /datum/outfit/syndicatecommandocorpse/lessenedgear
if("Shadow")
- outfit = /datum/outfit/consumed_shadowperson
+ return /datum/outfit/consumed_shadowperson
+
+/// Creates a dead legion-infested skeleton
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton
+ name = "legion-infested skeleton"
+ mob_name = "skeleton"
+ mob_species = /datum/species/skeleton
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/select_outfit()
+ return null
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/special(mob/living/carbon/human/spawned_human)
. = ..()
+ spawned_human.gender = NEUTER
+
+/// Creates a dead and burned legion-infested skeleton
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred
+ name = "charred legion-infested skeleton"
+ mob_name = "charred skeleton"
+ brute_damage = 0
+ burn_damage = 1000
+
+/obj/effect/mob_spawn/corpse/human/legioninfested/skeleton/charred/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ spawned_human.color = "#454545"
+
/datum/outfit/consumed_miner
name = "Legion-Consumed Miner"
diff --git a/code/modules/procedural_mapping/mapGenerators/asteroid.dm b/code/modules/procedural_mapping/mapGenerators/asteroid.dm
index ab2bc6f2ca4..bf6c84ebf88 100644
--- a/code/modules/procedural_mapping/mapGenerators/asteroid.dm
+++ b/code/modules/procedural_mapping/mapGenerators/asteroid.dm
@@ -22,7 +22,7 @@
spawnableAtoms = list(
/mob/living/basic/mining/basilisk = 10,
/mob/living/basic/mining/goliath/ancient = 10,
- /mob/living/simple_animal/hostile/asteroid/hivelord = 10,
+ /mob/living/basic/mining/hivelord = 10,
)
diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm
index 04103648fda..4a1e3cde555 100644
--- a/code/modules/surgery/organs/_organ.dm
+++ b/code/modules/surgery/organs/_organ.dm
@@ -57,6 +57,8 @@
var/list/organ_traits
/// Status Effects that are given to the holder of the organ.
var/list/organ_effects
+ /// String displayed when the organ has decayed.
+ var/failing_desc = "has decayed for too long, and has turned a sickly color. It probably won't work without repairs."
// Players can look at prefs before atoms SS init, and without this
// they would not be able to see external organs, such as moth wings.
@@ -242,10 +244,7 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
. += span_notice("It should be inserted in the [parse_zone(zone)].")
if(organ_flags & ORGAN_FAILING)
- if(IS_ROBOTIC_ORGAN(src))
- . += span_warning("[src] seems to be broken.")
- return
- . += span_warning("[src] has decayed for too long, and has turned a sickly color. It probably won't work without repairs.")
+ . += span_warning("[src] [failing_desc]")
return
if(damage > high_threshold)
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
index 0a7332c0dd8..f0578832969 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm
@@ -4,6 +4,7 @@
desc = "A state-of-the-art implant that improves a baseline's functionality."
visual = FALSE
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
var/implant_color = "#FFFFFF"
var/implant_overlay
diff --git a/code/modules/surgery/organs/internal/ears/_ears.dm b/code/modules/surgery/organs/internal/ears/_ears.dm
index ea642879430..7d2b0f3c549 100644
--- a/code/modules/surgery/organs/internal/ears/_ears.dm
+++ b/code/modules/surgery/organs/internal/ears/_ears.dm
@@ -105,6 +105,7 @@
desc = "A basic cybernetic organ designed to mimic the operation of ears."
damage_multiplier = 0.9
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
/obj/item/organ/internal/ears/cybernetic/upgraded
name = "cybernetic ears"
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index a6a749f2621..5a6702738be 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -292,6 +292,7 @@
icon_state = "cybernetic_eyeballs"
desc = "Your vision is augmented."
organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
/obj/item/organ/internal/eyes/robotic/emp_act(severity)
. = ..()
@@ -630,7 +631,7 @@
if(QDELETED(eye_owner) || !ishuman(eye_owner)) //Other carbon mobs don't have eye color.
return
-
+
if(!eye.on)
eye_icon_state = initial(eye_icon_state)
overlay_ignore_lighting = FALSE
diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm
index ce4b948c653..e553f8f1307 100644
--- a/code/modules/surgery/organs/internal/heart/_heart.dm
+++ b/code/modules/surgery/organs/internal/heart/_heart.dm
@@ -203,6 +203,7 @@
base_icon_state = "heart-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD*0.75 //This also hits defib timer, so a bit higher than its less important counterparts
+ failing_desc = "seems to be broken."
var/dose_available = FALSE
var/rid = /datum/reagent/medicine/epinephrine
diff --git a/code/modules/surgery/organs/internal/liver/_liver.dm b/code/modules/surgery/organs/internal/liver/_liver.dm
index afeb8decde4..4eb5c665db6 100755
--- a/code/modules/surgery/organs/internal/liver/_liver.dm
+++ b/code/modules/surgery/organs/internal/liver/_liver.dm
@@ -245,6 +245,7 @@
/obj/item/organ/internal/liver/cybernetic
name = "basic cybernetic liver"
desc = "A very basic device designed to mimic the functions of a human liver. Handles toxins slightly worse than an organic liver."
+ failing_desc = "seems to be broken."
icon_state = "liver-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD*0.5
diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm
index 5e4e0648067..71dc305ac5f 100644
--- a/code/modules/surgery/organs/internal/lungs/_lungs.dm
+++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm
@@ -837,6 +837,7 @@
/obj/item/organ/internal/lungs/cybernetic
name = "basic cybernetic lungs"
desc = "A basic cybernetic version of the lungs found in traditional humanoid entities."
+ failing_desc = "seems to be broken."
icon_state = "lungs-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD * 0.5
diff --git a/code/modules/surgery/organs/internal/stomach/_stomach.dm b/code/modules/surgery/organs/internal/stomach/_stomach.dm
index c817e7209db..63fefca156c 100644
--- a/code/modules/surgery/organs/internal/stomach/_stomach.dm
+++ b/code/modules/surgery/organs/internal/stomach/_stomach.dm
@@ -288,6 +288,7 @@
/obj/item/organ/internal/stomach/cybernetic
name = "basic cybernetic stomach"
desc = "A basic device designed to mimic the functions of a human stomach"
+ failing_desc = "seems to be broken."
icon_state = "stomach-c"
organ_flags = ORGAN_ROBOTIC
maxHealth = STANDARD_ORGAN_THRESHOLD * 0.5
diff --git a/code/modules/surgery/organs/internal/tongue/_tongue.dm b/code/modules/surgery/organs/internal/tongue/_tongue.dm
index 94f9abafa14..b53bcc5404e 100644
--- a/code/modules/surgery/organs/internal/tongue/_tongue.dm
+++ b/code/modules/surgery/organs/internal/tongue/_tongue.dm
@@ -536,6 +536,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
/obj/item/organ/internal/tongue/robot
name = "robotic voicebox"
desc = "A voice synthesizer that can interface with organic lifeforms."
+ failing_desc = "seems to be broken."
organ_flags = ORGAN_ROBOTIC
icon_state = "tonguerobot"
say_mod = "states"
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index ad1eacc3e6d..824011366a7 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -64,22 +64,10 @@
/mob/living/simple_animal/hostile/asteroid/gutlunch/grublunch,
/mob/living/simple_animal/hostile/asteroid/gutlunch/gubbuck,
/mob/living/simple_animal/hostile/asteroid/gutlunch/guthen,
- /mob/living/simple_animal/hostile/asteroid/hivelord,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/advanced,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow/portal,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/advanced,
- /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow,
/mob/living/simple_animal/hostile/asteroid/ice_demon,
/mob/living/simple_animal/hostile/asteroid/polarbear,
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/asteroid/wolf,
- /mob/living/simple_animal/hostile/big_legion,
/mob/living/simple_animal/hostile/construct,
/mob/living/simple_animal/hostile/construct/artificer,
/mob/living/simple_animal/hostile/construct/artificer/angelic,
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index 13c37dca594..38b78cf468f 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/mob/telegraphing/telegraph.dmi b/icons/mob/telegraphing/telegraph.dmi
index d5e03419cd8..de525ead4ee 100644
Binary files a/icons/mob/telegraphing/telegraph.dmi and b/icons/mob/telegraphing/telegraph.dmi differ
diff --git a/icons/obj/medical/organs/mining_organs.dmi b/icons/obj/medical/organs/mining_organs.dmi
index f3fc298284b..172f94001ff 100644
Binary files a/icons/obj/medical/organs/mining_organs.dmi and b/icons/obj/medical/organs/mining_organs.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 47ebc295e06..9ef3c06fffd 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -150,6 +150,7 @@
#include "code\__DEFINES\mod.dm"
#include "code\__DEFINES\modular_computer.dm"
#include "code\__DEFINES\monkeys.dm"
+#include "code\__DEFINES\mood.dm"
#include "code\__DEFINES\move_force.dm"
#include "code\__DEFINES\movement.dm"
#include "code\__DEFINES\movespeed_modification.dm"
@@ -799,6 +800,7 @@
#include "code\datums\ai\bane\bane_controller.dm"
#include "code\datums\ai\bane\bane_subtrees.dm"
#include "code\datums\ai\basic_mobs\base_basic_controller.dm"
+#include "code\datums\ai\basic_mobs\generic_controllers.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\climb_tree.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\find_mineable_wall.dm"
@@ -4377,6 +4379,14 @@
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_ai.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\goliath_trophy.dm"
#include "code\modules\mob\living\basic\lavaland\goliath\tentacle.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\hivelord.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\hivelord_ai.dm"
+#include "code\modules\mob\living\basic\lavaland\hivelord\spawn_hivelord_brood.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_ai.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_brood.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\legion_tumour.dm"
+#include "code\modules\mob\living\basic\lavaland\legion\spawn_legions.dm"
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity.dm"
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_ai.dm"
#include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_trophy.dm"
@@ -4702,7 +4712,6 @@
#include "code\modules\mob\living\simple_animal\hostile\megafauna\wendigo.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\curse_blob.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\gutlunch.dm"
-#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\hivelord.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\ice_demon.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\mining_mobs.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\polarbear.dm"
diff --git a/tgui/packages/tgui/interfaces/CrewConsole.js b/tgui/packages/tgui/interfaces/CrewConsole.js
index 58e99dd9c9e..937ff9c1baf 100644
--- a/tgui/packages/tgui/interfaces/CrewConsole.js
+++ b/tgui/packages/tgui/interfaces/CrewConsole.js
@@ -13,14 +13,8 @@ const HEALTH_COLOR_BY_LEVEL = [
'#801308',
];
-const HEALTH_ICON_BY_LEVEL = [
- 'heart',
- 'heart',
- 'heart',
- 'heart',
- 'heartbeat',
- 'skull',
-];
+const STAT_LIVING = 0;
+const STAT_DEAD = 4;
const jobIsHead = (jobId) => jobId % 10 === 0;
@@ -49,6 +43,16 @@ const jobToColor = (jobId) => {
return COLORS.department.other;
};
+const statToIcon = (life_status) => {
+ switch (life_status) {
+ case STAT_LIVING:
+ return 'heart';
+ case STAT_DEAD:
+ return 'skull';
+ }
+ return 'heartbeat';
+};
+
const healthToAttribute = (oxy, tox, burn, brute, attributeList) => {
const healthSum = oxy + tox + burn + brute;
const level = Math.min(Math.max(Math.ceil(healthSum / 25), 0), 5);
@@ -129,13 +133,7 @@ const CrewTableEntry = (props, context) => {