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) => { {oxydam !== undefined ? ( { )} size={1} /> - ) : life_status ? ( + ) : life_status !== STAT_DEAD ? ( ) : ( @@ -162,7 +160,7 @@ const CrewTableEntry = (props, context) => { {'/'} - ) : life_status ? ( + ) : life_status !== STAT_DEAD ? ( 'Alive' ) : ( 'Dead' diff --git a/tools/UpdatePaths/Scripts/78624_simple_to_basic_legion.txt b/tools/UpdatePaths/Scripts/78624_simple_to_basic_legion.txt new file mode 100644 index 00000000000..bf397a83bb2 --- /dev/null +++ b/tools/UpdatePaths/Scripts/78624_simple_to_basic_legion.txt @@ -0,0 +1,3 @@ +/mob/living/simple_animal/hostile/asteroid/hivelord : /mob/living/basic/mining/hivelord{@OLD} +/mob/living/simple_animal/hostile/big_legion : /mob/living/basic/mining/legion/big{@OLD} +/mob/living/simple_animal/hostile/asteroid/hivelord/legion/@SUBTYPES : /mob/living/basic/mining/legion/@SUBTYPES{@OLD}