diff --git a/_maps/RandomRuins/SpaceRuins/meateor.dmm b/_maps/RandomRuins/SpaceRuins/meateor.dmm
new file mode 100644
index 00000000000..b9a731a08cb
--- /dev/null
+++ b/_maps/RandomRuins/SpaceRuins/meateor.dmm
@@ -0,0 +1,981 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"a" = (
+/turf/template_noop,
+/area/template_noop)
+"e" = (
+/obj/effect/mob_spawn/corpse/headcrab,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"f" = (
+/obj/effect/mob_spawn/corpse/human/monkey,
+/obj/effect/decal/cleanable/blood/tracks{
+ dir = 8
+ },
+/obj/effect/decal/cleanable/blood,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"g" = (
+/turf/closed/mineral/random,
+/area/ruin/space/meateor)
+"j" = (
+/obj/item/gun/energy/recharge/kinetic_accelerator,
+/obj/effect/decal/cleanable/blood,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"k" = (
+/obj/item/flashlight/flare,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"m" = (
+/obj/item/toy/plush/snakeplushie,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"p" = (
+/obj/structure/statue/bone/rib,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"q" = (
+/obj/effect/mob_spawn/corpse/headcrab,
+/obj/effect/decal/cleanable/xenoblood,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"r" = (
+/obj/effect/decal/cleanable/blood/tracks{
+ dir = 8
+ },
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"s" = (
+/obj/effect/mob_spawn/corpse/headcrab,
+/obj/effect/decal/cleanable/xenoblood/xgibs,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"v" = (
+/obj/structure/meateor_fluff/eyeball,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"z" = (
+/obj/effect/mob_spawn/corpse/human/tigercultist/perforated,
+/obj/item/storage/organbox/preloaded{
+ desc = "Often used, but not often cleaned.";
+ name = "abandoned lunchbox"
+ },
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"A" = (
+/turf/closed/wall/material/meat/airless,
+/area/ruin/space/meateor)
+"B" = (
+/obj/item/paper/fluff/ruins/meateor/letter,
+/obj/structure/bed/maint,
+/obj/item/reagent_containers/cup/glass/bottle/ritual_wine{
+ pixel_x = -10
+ },
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"C" = (
+/obj/effect/mob_spawn/corpse/human/miner,
+/obj/effect/decal/cleanable/blood,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"D" = (
+/obj/structure/statue/bone/rib{
+ dir = 8
+ },
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"G" = (
+/obj/structure/meateor_fluff/flesh_pod,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"L" = (
+/obj/effect/spawner/random/trash/food_packaging,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"O" = (
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"R" = (
+/obj/structure/meateor_fluff/abandoned_headcrab_egg,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"S" = (
+/obj/effect/decal/remains/human,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+"T" = (
+/turf/open/misc/asteroid/airless,
+/area/ruin/space/meateor)
+"X" = (
+/mob/living/basic/meteor_heart,
+/turf/open/floor/material/meat/airless,
+/area/ruin/space/meateor)
+
+(1,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(2,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+g
+g
+g
+g
+g
+g
+a
+a
+a
+a
+a
+a
+a
+g
+g
+g
+a
+a
+a
+a
+a
+a
+"}
+(3,1,1) = {"
+a
+a
+a
+a
+a
+g
+g
+g
+A
+A
+A
+A
+g
+g
+g
+a
+a
+a
+a
+g
+g
+g
+g
+g
+a
+a
+a
+a
+a
+"}
+(4,1,1) = {"
+a
+a
+a
+a
+g
+g
+A
+A
+A
+A
+A
+A
+A
+g
+g
+g
+g
+a
+a
+a
+A
+A
+g
+g
+a
+a
+a
+a
+a
+"}
+(5,1,1) = {"
+a
+a
+a
+a
+g
+A
+A
+A
+L
+G
+S
+G
+A
+A
+A
+A
+g
+g
+a
+a
+a
+A
+A
+g
+g
+a
+a
+a
+a
+"}
+(6,1,1) = {"
+a
+a
+a
+g
+g
+A
+B
+z
+O
+v
+O
+O
+O
+O
+G
+A
+g
+g
+g
+a
+a
+a
+A
+A
+g
+a
+a
+a
+a
+"}
+(7,1,1) = {"
+a
+a
+a
+g
+g
+A
+e
+D
+O
+O
+O
+O
+v
+D
+R
+A
+A
+g
+g
+a
+a
+a
+A
+A
+g
+a
+a
+a
+a
+"}
+(8,1,1) = {"
+a
+a
+a
+g
+A
+A
+L
+G
+O
+O
+O
+O
+O
+O
+O
+R
+A
+A
+g
+g
+a
+a
+a
+A
+g
+a
+a
+a
+a
+"}
+(9,1,1) = {"
+a
+a
+a
+g
+A
+A
+O
+O
+O
+O
+O
+O
+O
+O
+O
+G
+A
+A
+g
+g
+a
+a
+a
+g
+a
+a
+a
+a
+a
+"}
+(10,1,1) = {"
+a
+a
+g
+g
+A
+G
+O
+v
+O
+O
+X
+O
+O
+O
+O
+O
+O
+A
+A
+g
+g
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(11,1,1) = {"
+a
+a
+g
+A
+A
+O
+O
+O
+O
+O
+O
+O
+S
+O
+v
+O
+O
+A
+A
+g
+g
+a
+a
+a
+a
+a
+g
+a
+a
+"}
+(12,1,1) = {"
+a
+g
+g
+A
+A
+G
+O
+S
+O
+O
+O
+O
+O
+R
+O
+O
+O
+A
+A
+g
+g
+a
+a
+a
+a
+g
+g
+g
+a
+"}
+(13,1,1) = {"
+a
+g
+g
+A
+A
+R
+O
+p
+O
+O
+v
+O
+O
+p
+O
+G
+A
+A
+g
+g
+g
+a
+a
+a
+a
+A
+A
+g
+a
+"}
+(14,1,1) = {"
+a
+g
+g
+A
+A
+A
+O
+O
+O
+O
+O
+O
+O
+O
+R
+A
+A
+A
+g
+g
+g
+a
+a
+a
+a
+a
+A
+g
+a
+"}
+(15,1,1) = {"
+a
+g
+g
+A
+A
+A
+A
+A
+q
+A
+A
+A
+A
+A
+A
+A
+m
+A
+g
+g
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(16,1,1) = {"
+a
+g
+g
+g
+A
+A
+A
+D
+k
+C
+D
+O
+R
+D
+A
+A
+A
+A
+g
+g
+g
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(17,1,1) = {"
+a
+g
+g
+g
+A
+A
+R
+O
+j
+O
+O
+q
+v
+O
+R
+A
+A
+g
+g
+g
+g
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(18,1,1) = {"
+a
+a
+g
+g
+A
+A
+O
+O
+O
+s
+O
+O
+O
+O
+O
+A
+A
+g
+g
+g
+g
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(19,1,1) = {"
+a
+a
+g
+g
+g
+A
+R
+O
+v
+O
+O
+O
+O
+O
+G
+A
+A
+A
+A
+g
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(20,1,1) = {"
+a
+a
+g
+g
+g
+A
+A
+p
+R
+O
+p
+O
+r
+p
+A
+A
+A
+A
+T
+T
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(21,1,1) = {"
+a
+a
+a
+g
+g
+g
+A
+A
+A
+O
+A
+A
+r
+A
+A
+O
+v
+T
+T
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(22,1,1) = {"
+a
+a
+a
+a
+g
+g
+g
+A
+A
+O
+O
+O
+f
+A
+A
+O
+A
+A
+T
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
+(23,1,1) = {"
+a
+a
+a
+a
+g
+g
+g
+g
+A
+A
+A
+O
+O
+A
+A
+O
+A
+A
+A
+g
+a
+a
+A
+g
+a
+a
+a
+a
+a
+"}
+(24,1,1) = {"
+a
+a
+a
+a
+a
+g
+g
+g
+g
+g
+A
+A
+v
+O
+O
+O
+A
+A
+g
+g
+a
+a
+A
+A
+g
+a
+a
+a
+a
+"}
+(25,1,1) = {"
+a
+a
+a
+a
+a
+a
+g
+g
+g
+g
+g
+A
+A
+A
+A
+A
+A
+g
+g
+a
+a
+A
+A
+A
+g
+a
+a
+a
+a
+"}
+(26,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+g
+g
+g
+g
+A
+A
+A
+g
+g
+a
+a
+g
+g
+A
+g
+a
+a
+a
+a
+a
+"}
+(27,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+g
+g
+g
+g
+g
+g
+a
+a
+a
+a
+g
+g
+g
+a
+a
+a
+a
+a
+"}
+(28,1,1) = {"
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+a
+"}
diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm
index 78dde256424..32ed1445b55 100644
--- a/code/__DEFINES/ai.dm
+++ b/code/__DEFINES/ai.dm
@@ -226,6 +226,9 @@
#define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location"
#define BB_FLEE_TARGETTING_DATUM "flee_targetting_datum"
+///How long have we spent with no target?
+#define BB_TARGETLESS_TIME "BB_targetless_time"
+
///List of mobs who have damaged us
#define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist"
@@ -249,6 +252,12 @@
#define BB_BILEWORM_RESURFACE "BB_bileworm_resurface"
#define BB_BILEWORM_DEVOUR "BB_bileworm_devour"
+// Meteor Heart AI keys
+/// Key where we keep the spike trail ability
+#define BB_METEOR_HEART_GROUND_SPIKES "BB_meteor_ground_spikes"
+/// Key where we keep the spine traps ability
+#define BB_METEOR_HEART_SPINE_TRAPS "BB_meteor_spine_traps"
+
// Spider AI keys
/// Key where we store a turf to put webs on
#define BB_SPIDER_WEB_TARGET "BB_spider_web_target"
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm
new file mode 100644
index 00000000000..1cef3a65147
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm
@@ -0,0 +1,33 @@
+/// Disables AI after a certain amount of time spent with no target, you will have to enable the AI again somewhere else
+/datum/ai_planning_subtree/sleep_with_no_target
+ /// Behaviour to execute when sleeping
+ var/sleep_behaviour = /datum/ai_behavior/sleep_after_targetless_time
+ /// Target key to interrogate
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+/datum/ai_planning_subtree/sleep_with_no_target/SelectBehaviors(datum/ai_controller/controller, delta_time)
+ . = ..()
+ controller.queue_behavior(sleep_behaviour, BB_BASIC_MOB_CURRENT_TARGET)
+
+/// Disables AI after a certain amount of time spent with no target, you will have to enable the AI again somewhere else
+/datum/ai_behavior/sleep_after_targetless_time
+ /// Turn off AI if we spend this many seconds without a target, don't use the macro because delta_time is already in seconds
+ var/time_to_wait = 10
+
+/datum/ai_behavior/sleep_after_targetless_time/perform(delta_time, datum/ai_controller/controller, target_key)
+ var/datum/weakref/weak_target = controller.blackboard[target_key]
+ var/atom/target = weak_target?.resolve()
+ finish_action(controller, succeeded = !target, delta_time = delta_time)
+
+/datum/ai_behavior/sleep_after_targetless_time/finish_action(datum/ai_controller/controller, succeeded, delta_time)
+ . = ..()
+ if (!succeeded)
+ controller.blackboard[BB_TARGETLESS_TIME] = 0
+ return
+ controller.blackboard[BB_TARGETLESS_TIME] += delta_time
+ if (controller.blackboard[BB_TARGETLESS_TIME] > time_to_wait)
+ enter_sleep(controller)
+
+/// Disables AI, override to do additional things or something else
+/datum/ai_behavior/sleep_after_targetless_time/proc/enter_sleep(datum/ai_controller/controller)
+ controller.set_ai_status(AI_STATUS_OFF)
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 f748a877f05..1508a9a6f44 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
@@ -6,6 +6,8 @@
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Behaviour to perform using ability
var/use_ability_behaviour = /datum/ai_behavior/targeted_mob_ability
+ /// If true we terminate planning after trying to use the ability.
+ var/finish_planning = TRUE
/datum/ai_planning_subtree/targeted_mob_ability/SelectBehaviors(datum/ai_controller/controller, delta_time)
if (!ability_key)
@@ -22,4 +24,5 @@
return
controller.queue_behavior(use_ability_behaviour, ability_key, target_key)
- return SUBTREE_RETURN_FINISH_PLANNING
+ if (finish_planning)
+ return SUBTREE_RETURN_FINISH_PLANNING
diff --git a/code/datums/components/spawner.dm b/code/datums/components/spawner.dm
index 4b0122571f2..81eb38acc6a 100644
--- a/code/datums/components/spawner.dm
+++ b/code/datums/components/spawner.dm
@@ -1,30 +1,30 @@
/datum/component/spawner
/// Time to wait between spawns
var/spawn_time
- /// Maximum number of mobs we can have active at one time
- var/max_mobs
- /// Visible message to show when a mob spawns
+ /// Maximum number of atoms we can have active at one time
+ var/max_spawned
+ /// Visible message to show when something spawns
var/spawn_text
- /// List of mob types to spawn, picked randomly
- var/list/mob_types
- /// Faction to grant to mobs
+ /// List of atom types to spawn, picked randomly
+ var/list/spawn_types
+ /// Faction to grant to mobs (only applies to mobs)
var/list/faction
- /// List of weak references to mobs we have already created
- var/list/spawned_mobs = list()
+ /// List of weak references to things we have already created
+ var/list/spawned_things = list()
/// Time until we next spawn
COOLDOWN_DECLARE(spawn_delay)
-/datum/component/spawner/Initialize(mob_types = list(), spawn_time = 30 SECONDS, max_mobs = 5, faction = list(FACTION_MINING), spawn_text = "emerges from")
- if (!islist(mob_types))
- CRASH("invalid mob_types to spawn specified for spawner component!")
+/datum/component/spawner/Initialize(spawn_types = list(), spawn_time = 30 SECONDS, max_spawned = 5, faction = list(FACTION_MINING), spawn_text = null)
+ if (!islist(spawn_types))
+ CRASH("invalid spawn_types to spawn specified for spawner component!")
src.spawn_time = spawn_time
- src.mob_types = mob_types
+ src.spawn_types = spawn_types
src.faction = faction
src.spawn_text = spawn_text
- src.max_mobs = max_mobs
+ src.max_spawned = max_spawned
RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(stop_spawning))
- START_PROCESSING(SSprocessing, src)
+ START_PROCESSING((spawn_time < 2 SECONDS ? SSfastprocess : SSprocessing), src)
/datum/component/spawner/process()
try_spawn_mob()
@@ -34,44 +34,54 @@
SIGNAL_HANDLER
STOP_PROCESSING(SSprocessing, src)
- spawned_mobs = list()
+ spawned_things = list()
/// Try to create a new mob
/datum/component/spawner/proc/try_spawn_mob()
if(!COOLDOWN_FINISHED(src, spawn_delay))
return
validate_references()
- if(length(spawned_mobs) >= max_mobs)
+ if(length(spawned_things) >= max_spawned)
return
var/atom/spawner = parent
COOLDOWN_START(src, spawn_delay, spawn_time)
- var/chosen_mob_type = pick(mob_types)
- var/mob/living/created = new chosen_mob_type(spawner.loc)
+ var/chosen_mob_type = pick(spawn_types)
+ var/atom/created = new chosen_mob_type(spawner.loc)
created.flags_1 |= (spawner.flags_1 & ADMIN_SPAWNED_1)
- spawned_mobs += WEAKREF(created)
- created.faction = src.faction
- spawner.visible_message(span_danger("[created] [spawn_text] [spawner]."))
+ spawned_things += WEAKREF(created)
+ if (isliving(created))
+ var/mob/living/created_mob = created
+ created_mob.faction = src.faction
+ RegisterSignal(created, COMSIG_MOB_STATCHANGE, PROC_REF(mob_stat_changed))
- RegisterSignal(created, COMSIG_PARENT_QDELETING, PROC_REF(mob_deleted))
- RegisterSignal(created, COMSIG_MOB_STATCHANGE, PROC_REF(mob_stat_changed))
+ if (spawn_text)
+ spawner.visible_message(span_danger("[created] [spawn_text] [spawner]."))
-/// Remove weakrefs to mobs which have been killed or deleted without us picking it up somehow
+ RegisterSignal(created, COMSIG_PARENT_QDELETING, PROC_REF(on_deleted))
+
+/// Remove weakrefs to atoms which have been killed or deleted without us picking it up somehow
/datum/component/spawner/proc/validate_references()
- for (var/datum/weakref/weak_mob as anything in spawned_mobs)
- var/mob/living/previously_spawned = weak_mob.resolve()
- if (previously_spawned && previously_spawned.stat != DEAD)
+ for (var/datum/weakref/weak_thing as anything in spawned_things)
+ var/atom/previously_spawned = weak_thing.resolve()
+ if (!previously_spawned)
+ spawned_things -= weak_thing
continue
- spawned_mobs -= weak_mob
+ if (!isliving(previously_spawned))
+ continue
+ var/mob/living/spawned_mob = previously_spawned
+ if (spawned_mob.stat != DEAD)
+ continue
+ spawned_things -= weak_thing
-/// Called when a mob we spawned is deleted, remove it from the list
-/datum/component/spawner/proc/mob_deleted(mob/living/source)
+/// Called when an atom we spawned is deleted, remove it from the list
+/datum/component/spawner/proc/on_deleted(atom/source)
SIGNAL_HANDLER
- spawned_mobs -= WEAKREF(source)
+ spawned_things -= WEAKREF(source)
/// Called when a mob we spawned dies, remove it from the list and unregister signals
/datum/component/spawner/proc/mob_stat_changed(mob/living/source)
if (source.stat != DEAD)
return
- spawned_mobs -= WEAKREF(source)
+ spawned_things -= WEAKREF(source)
UnregisterSignal(source, list(COMSIG_PARENT_QDELETING, COMSIG_MOB_STATCHANGE))
diff --git a/code/datums/elements/floorloving.dm b/code/datums/elements/floorloving.dm
new file mode 100644
index 00000000000..229dea11da8
--- /dev/null
+++ b/code/datums/elements/floorloving.dm
@@ -0,0 +1,21 @@
+/// Prevents a movable atom from moving to somewhere which isn't an open turf with floor on it
+/datum/element/floor_loving
+
+/datum/element/floor_loving/Attach(datum/target)
+ . = ..()
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(attempting_move))
+
+/datum/element/floor_loving/Detach(datum/target)
+ UnregisterSignal(target, COMSIG_MOVABLE_PRE_MOVE)
+ return ..()
+
+/// Block movement to any non-floor location
+/datum/element/floor_loving/proc/attempting_move(atom/movable/parent, newloc)
+ SIGNAL_HANDLER
+ if (!isopenturf(newloc) || isspaceturf(newloc) || isopenspaceturf(newloc))
+ return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
+ if (isliving(parent))
+ var/mob/living/living_parent = parent
+ living_parent.balloon_alert(living_parent, "can't move there!")
diff --git a/code/datums/elements/frozen.dm b/code/datums/elements/frozen.dm
index ca40f9d174a..3fdf13965cf 100644
--- a/code/datums/elements/frozen.dm
+++ b/code/datums/elements/frozen.dm
@@ -20,6 +20,13 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
target_obj.add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY)
target_obj.alpha -= 25
+ if (isinternalorgan(target))
+ var/obj/item/organ/internal/organ = target
+ organ.organ_flags |= ORGAN_FROZEN
+ else if (isbodypart(target))
+ for(var/obj/item/organ/internal/organ in target_obj.contents)
+ organ.organ_flags |= ORGAN_FROZEN
+
RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(target, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(shatter_on_throw))
RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(shatter_on_throw))
@@ -32,7 +39,15 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
obj_source.name = replacetext(obj_source.name, "frozen ", "")
obj_source.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix)
obj_source.alpha += 25
- . = ..()
+
+ if (isinternalorgan(source))
+ var/obj/item/organ/internal/organ = source
+ organ.organ_flags &= ~ORGAN_FROZEN
+ else if (isbodypart(source))
+ for(var/obj/item/organ/internal/organ in obj_source.contents)
+ organ.organ_flags &= ~ORGAN_FROZEN
+
+ return ..()
///signal handler for COMSIG_OBJ_UNFREEZE that forces us to detach from the target
/datum/element/frozen/proc/on_unfreeze(datum/source)
diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm
index d19b6fcdae3..d9cf7ed668c 100644
--- a/code/datums/looping_sounds/_looping_sound.dm
+++ b/code/datums/looping_sounds/_looping_sound.dm
@@ -120,10 +120,19 @@
return
// If we have a timer, we're varying mid length, and this is happening while we're runnin mid_sounds
if(timer_id && mid_length_vary && start_time)
- updatetimedelay(timer_id, mid_length + rand(-mid_length_vary, mid_length_vary))
+ updatetimedelay(timer_id, mid_length + rand(-mid_length_vary, mid_length_vary), timer_subsystem = SSsound_loops)
if(!chance || prob(chance))
play(get_sound())
+/**
+ * Applies a new mid length to the sound
+ */
+/datum/looping_sound/proc/set_mid_length(new_mid)
+ mid_length = new_mid
+ if(!timer_id)
+ return
+ updatetimedelay(timer_id, mid_length + rand(-mid_length_vary, mid_length_vary), timer_subsystem = SSsound_loops)
+
/**
* The proc that handles actually playing the sound.
*
diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm
index d396d8e292e..d38dd6737a3 100644
--- a/code/datums/looping_sounds/item_sounds.dm
+++ b/code/datums/looping_sounds/item_sounds.dm
@@ -19,6 +19,10 @@
start_sound = list('sound/items/taperecorder/taperecorder_hiss_start.ogg' = 1)
volume = 10
+/datum/looping_sound/heartbeat
+ mid_sounds = list('sound/effects/singlebeat.ogg' = 1)
+ mid_length = 1 SECONDS
+
/datum/looping_sound/trapped_machine_beep
mid_sounds = list('sound/machines/beep.ogg' = 1)
mid_length = 10 SECONDS
diff --git a/code/datums/ruins/space.dm b/code/datums/ruins/space.dm
index 94963ebfc6f..1541a33dc48 100644
--- a/code/datums/ruins/space.dm
+++ b/code/datums/ruins/space.dm
@@ -358,6 +358,12 @@
name = "Alternate Sciences Research Center"
description = "When you're messing with the occult, who knows what you're going to get?"
+/datum/map_template/ruin/space/meateor
+ id = "meateor"
+ suffix = "meateor.dmm"
+ name = "Meateor"
+ description = "A big chunk of meat floating in space. How did it get there?"
+
/datum/map_template/ruin/space/the_faceoff
id = "the_faceoff"
suffix = "the_faceoff.dmm"
diff --git a/code/datums/storage/subtypes/organ_box.dm b/code/datums/storage/subtypes/organ_box.dm
index 997e4bf50ba..de1cb5f5071 100644
--- a/code/datums/storage/subtypes/organ_box.dm
+++ b/code/datums/storage/subtypes/organ_box.dm
@@ -1,9 +1,9 @@
/datum/storage/organ_box
-/datum/storage/organ_box/handle_enter(datum/source, obj/item/arrived)
+/datum/storage/organ_box/handle_enter(obj/item/storage/organbox/source, obj/item/arrived)
. = ..()
- if(!istype(arrived))
+ if(!istype(arrived) || !istype(source) || !source.coolant_to_spend())
return
arrived.freeze()
diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm
index 3a4a6a5e2f3..28f5a9820e8 100644
--- a/code/game/objects/items/storage/medkit.dm
+++ b/code/game/objects/items/storage/medkit.dm
@@ -550,6 +550,7 @@
for(var/i in 1 to 5)
new /obj/item/reagent_containers/pill/paxpsych(src)
+/// A box which takes in coolant and uses it to preserve organs and body parts
/obj/item/storage/organbox
name = "organ transport box"
desc = "An advanced box with an cooling mechanism that uses cryostylane or other cold reagents to keep the organs or bodyparts inside preserved."
@@ -579,56 +580,39 @@
/obj/item/storage/organbox/process(delta_time)
///if there is enough coolant var
- var/cool = FALSE
- var/amount = min(reagents.get_reagent_amount(/datum/reagent/cryostylane), 0.05 * delta_time)
- if(amount > 0)
- reagents.remove_reagent(/datum/reagent/cryostylane, amount)
- cool = TRUE
- else
- amount = min(reagents.get_reagent_amount(/datum/reagent/consumable/ice), 0.1 * delta_time)
- if(amount > 0)
- reagents.remove_reagent(/datum/reagent/consumable/ice, amount)
- cool = TRUE
- if(!cooling && cool)
- cooling = TRUE
- update_appearance()
- for(var/C in contents)
- freeze_contents(C)
+ var/using_coolant = coolant_to_spend()
+ if (isnull(using_coolant))
+ if (cooling)
+ cooling = FALSE
+ update_appearance()
+ for(var/obj/stored in contents)
+ stored.unfreeze()
return
- if(cooling && !cool)
- cooling = FALSE
- update_appearance()
- for(var/C in contents)
- unfreeze_contents(C)
+
+ var/amount_used = 0.05 * delta_time
+ if (using_coolant != /datum/reagent/cryostylane)
+ amount_used *= 2
+ reagents.remove_reagent(using_coolant, amount_used)
+
+ if(cooling)
+ return
+ cooling = TRUE
+ update_appearance()
+ for(var/obj/stored in contents)
+ stored.freeze()
+
+/// Returns which coolant we are about to use, or null if there isn't any
+/obj/item/storage/organbox/proc/coolant_to_spend()
+ if (reagents.get_reagent_amount(/datum/reagent/cryostylane))
+ return /datum/reagent/cryostylane
+ if (reagents.get_reagent_amount(/datum/reagent/consumable/ice))
+ return /datum/reagent/consumable/ice
+ return null
/obj/item/storage/organbox/update_icon_state()
icon_state = "[base_icon_state][cooling ? "-working" : null]"
return ..()
-///freezes the organ and loops bodyparts like heads
-/obj/item/storage/organbox/proc/freeze_contents(datum/source, obj/item/I)
- SIGNAL_HANDLER
- if(isinternalorgan(I))
- var/obj/item/organ/internal/int_organ = I
- int_organ.organ_flags |= ORGAN_FROZEN
- return
- if(isbodypart(I))
- var/obj/item/bodypart/B = I
- for(var/obj/item/organ/internal/int_organ in B.contents)
- int_organ.organ_flags |= ORGAN_FROZEN
-
-///unfreezes the organ and loops bodyparts like heads
-/obj/item/storage/organbox/proc/unfreeze_contents(datum/source, obj/item/I)
- SIGNAL_HANDLER
- if(isinternalorgan(I))
- var/obj/item/organ/internal/int_organ = I
- int_organ.organ_flags &= ~ORGAN_FROZEN
- return
- if(isbodypart(I))
- var/obj/item/bodypart/B = I
- for(var/obj/item/organ/internal/int_organ in B.contents)
- int_organ.organ_flags &= ~ORGAN_FROZEN
-
/obj/item/storage/organbox/attackby(obj/item/I, mob/user, params)
if(is_reagent_container(I) && I.is_open_container())
var/obj/item/reagent_containers/RC = I
@@ -659,3 +643,10 @@
user.adjust_bodytemperature(-300)
user.apply_status_effect(/datum/status_effect/freon)
return FIRELOSS
+
+/// A subtype of organ storage box which starts with a full coolant tank
+/obj/item/storage/organbox/preloaded
+
+/obj/item/storage/organbox/preloaded/Initialize(mapload)
+ . = ..()
+ reagents.add_reagent(/datum/reagent/cryostylane, reagents.maximum_volume)
diff --git a/code/game/turfs/closed/wall/misc_walls.dm b/code/game/turfs/closed/wall/misc_walls.dm
index 2d8c271359d..e9a7bce2086 100644
--- a/code/game/turfs/closed/wall/misc_walls.dm
+++ b/code/game/turfs/closed/wall/misc_walls.dm
@@ -135,3 +135,16 @@
smoothing_flags = NONE
canSmoothWith = null
smoothing_groups = null
+
+/turf/closed/wall/material/meat
+ name = "living wall"
+ baseturfs = /turf/open/floor/material/meat
+ girder_type = null
+ material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
+
+/turf/closed/wall/material/meat/Initialize(mapload)
+ . = ..()
+ set_custom_materials(list(GET_MATERIAL_REF(/datum/material/meat) = MINERAL_MATERIAL_AMOUNT))
+
+/turf/closed/wall/material/meat/airless
+ baseturfs = /turf/open/floor/material/meat/airless
diff --git a/code/game/turfs/open/floor/misc_floor.dm b/code/game/turfs/open/floor/misc_floor.dm
index 94f78e15453..8a86bbb6845 100644
--- a/code/game/turfs/open/floor/misc_floor.dm
+++ b/code/game/turfs/open/floor/misc_floor.dm
@@ -323,7 +323,19 @@
/turf/open/floor/cult/airless
initial_gas_mix = AIRLESS_ATMOS
-/// TGMC EMBLEM ///
+/turf/open/floor/material/meat
+ name = "living floor"
+ icon_state = "grey"
+ baseturfs = /turf/open/misc/asteroid
+ material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
+
+/turf/open/floor/material/meat/Initialize(mapload)
+ . = ..()
+ set_custom_materials(list(GET_MATERIAL_REF(/datum/material/meat) = MINERAL_MATERIAL_AMOUNT))
+
+/turf/open/floor/material/meat/airless
+ initial_gas_mix = AIRLESS_ATMOS
+ baseturfs = /turf/open/misc/asteroid/airless
/turf/open/floor/iron/tgmcemblem
name = "TGMC Emblem"
diff --git a/code/modules/antagonists/heretic/items/heretic_necks.dm b/code/modules/antagonists/heretic/items/heretic_necks.dm
index 341a167f51a..c46a4bf214c 100644
--- a/code/modules/antagonists/heretic/items/heretic_necks.dm
+++ b/code/modules/antagonists/heretic/items/heretic_necks.dm
@@ -42,3 +42,11 @@
name = "Piercing Eldritch Medallion"
desc = "A strange medallion. Peering through the crystalline surface, the light refracts into new and terrifying spectrums of color. You see yourself, reflected off cascading mirrors, warped into impossible shapes."
heretic_only_trait = TRAIT_XRAY_VISION
+
+// Cosmetic-only version
+/obj/item/clothing/neck/fake_heretic_amulet
+ name = "religious icon"
+ desc = "A strange medallion, which makes its wearer look like they're part of some cult."
+ icon = 'icons/obj/eldritch.dmi'
+ icon_state = "eye_medalion"
+ w_class = WEIGHT_CLASS_SMALL
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm b/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm
new file mode 100644
index 00000000000..3a3ec7569d8
--- /dev/null
+++ b/code/modules/mapfluff/ruins/spaceruin_code/meateor.dm
@@ -0,0 +1,131 @@
+/area/ruin/space/meateor
+ name = "\improper Organic Asteroid"
+ sound_environment = SOUND_AREA_SMALL_ENCLOSED
+
+/obj/item/paper/fluff/ruins/meateor/letter
+ name = "letter"
+ default_raw_text = {"We offer our sincerest congratulations, to be chosen to take this journey is an honour and privilege afforded to few.
+
While you are walking in the footsteps of the divine, don't forget about the rest of us back at the farm!
+
"}
+
+/// Tiger cultist corpse but with an exit wound
+/obj/effect/mob_spawn/corpse/human/tigercultist/perforated
+
+/obj/effect/mob_spawn/corpse/human/tigercultist/perforated/special(mob/living/carbon/human/spawned_human)
+ . = ..()
+ var/datum/wound/pierce/critical/exit_hole = new()
+ exit_hole.apply_wound(spawned_human.get_bodypart(BODY_ZONE_CHEST))
+
+/// A fun drink enjoyed by the tiger cooperative, might corrode your brain if you drink the whole bottle
+/obj/item/reagent_containers/cup/glass/bottle/ritual_wine
+ name = "ritual wine"
+ desc = "A bottle filled with liquids of a dubious nature, often enjoyed by members of the Tiger Cooperative."
+ icon_state = "winebottle"
+ list_reagents = list(
+ /datum/reagent/drug/mushroomhallucinogen = 25,
+ /datum/reagent/consumable/ethanol/ritual_wine = 25,
+ /datum/reagent/medicine/changelinghaste = 20, // This metabolises very fast
+ /datum/reagent/drug/bath_salts = 5,
+ /datum/reagent/blood = 5,
+ /datum/reagent/toxin/leadacetate = 5,
+ /datum/reagent/medicine/omnizine = 5,
+ /datum/reagent/medicine/c2/penthrite = 5,
+ /datum/reagent/consumable/vinegar = 5,
+ )
+ drink_type = NONE
+ age_restricted = FALSE
+
+/// Abstract holder object for shared behaviour
+/obj/structure/meateor_fluff
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ anchored = TRUE
+
+/obj/structure/meateor_fluff/play_attack_sound(damage_amount, damage_type, damage_flag)
+ switch(damage_type)
+ if(BRUTE)
+ if(damage_amount)
+ playsound(loc, 'sound/effects/attackblob.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ else
+ playsound(loc, 'sound/effects/meatslap.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ if(BURN)
+ playsound(loc, 'sound/effects/wounds/sizzle1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+
+/// A sort of loot box for organs, cut it open and find a prize
+/obj/structure/meateor_fluff/flesh_pod
+ name = "flesh pod"
+ desc = "A quivering pod of living meat. Something is pulsing inside."
+ icon_state = "flesh_pod"
+ max_integrity = 120
+ density = TRUE
+ /// Typepath of the organ to spawn when this is destroyed
+ var/stored_organ
+ /// Types of organ we can spawn
+ var/static/list/allowed_organs = list(
+ /obj/item/organ/internal/heart/gland/egg = 7,
+ /obj/item/organ/internal/heart/gland/plasma = 7,
+ /obj/item/organ/internal/alien/plasmavessel = 5,
+ /obj/item/organ/internal/heart/gland/chem = 5,
+ /obj/item/organ/internal/heart/gland/mindshock = 5,
+ /obj/item/organ/internal/heart/gland/spiderman = 5,
+ /obj/item/organ/internal/heart/gland/transform = 5,
+ /obj/item/organ/internal/heart/gland/slime = 4,
+ /obj/item/organ/internal/heart/gland/trauma = 4,
+ /obj/item/organ/internal/heart/carp = 3,
+ /obj/item/organ/internal/heart/rat = 3,
+ /obj/item/organ/internal/heart/gland/electric = 3,
+ /obj/item/organ/internal/monster_core/brimdust_sac = 3,
+ /obj/item/organ/internal/monster_core/regenerative_core = 3,
+ /obj/item/organ/internal/monster_core/rush_gland = 3,
+ /obj/item/organ/internal/tongue/carp = 3,
+ /obj/item/organ/internal/alien/acid = 2,
+ /obj/item/organ/internal/alien/resinspinner = 2,
+ /obj/item/organ/internal/eyes/night_vision/goliath = 2,
+ /obj/item/organ/internal/eyes/night_vision/rat = 2,
+ /obj/item/organ/internal/heart/gland/ventcrawling = 1,
+ )
+
+/obj/structure/meateor_fluff/flesh_pod/Initialize(mapload)
+ . = ..()
+ stored_organ = pick_weight(allowed_organs)
+
+/obj/structure/meateor_fluff/flesh_pod/attackby(obj/item/attacking_item, mob/user, params)
+ if (attacking_item.sharpness & SHARP_EDGED)
+ cut_open(user)
+ return
+ return ..()
+
+/// Cut the pod open and destroy it
+/obj/structure/meateor_fluff/flesh_pod/proc/cut_open(mob/user)
+ balloon_alert(user, "slicing...")
+ if (!do_after(user, 3 SECONDS, target = src))
+ return
+ take_damage(max_integrity)
+
+/obj/structure/meateor_fluff/flesh_pod/atom_destruction(damage_flag)
+ new stored_organ(loc)
+ new /obj/effect/decal/cleanable/blood(loc)
+ new /obj/structure/meateor_fluff/flesh_pod_open(loc)
+ playsound(loc, 'sound/effects/wounds/blood3.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ return ..()
+
+/obj/structure/meateor_fluff/flesh_pod_open
+ name = "flesh pod"
+ desc = "A pod of living meat, this one has been hollowed out."
+ icon_state = "flesh_pod_open"
+ max_integrity = 60
+
+/obj/structure/meateor_fluff/flesh_pod_open/atom_destruction(damage_flag)
+ new /obj/effect/gibspawner/human(loc)
+ return ..()
+
+/// Decorative fluff egg object
+/obj/structure/meateor_fluff/abandoned_headcrab_egg
+ name = "meaty eggs"
+ desc = "A mass of fleshy, egg-shaped nodes."
+ icon_state = "eggs"
+ max_integrity = 15
+
+/obj/structure/meateor_fluff/abandoned_headcrab_egg/atom_destruction(damage_flag)
+ new /obj/effect/decal/cleanable/xenoblood(loc)
+ playsound(loc, 'sound/effects/gib_step.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ return ..()
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
new file mode 100644
index 00000000000..70309716e43
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
@@ -0,0 +1,112 @@
+/// An ability which makes spikes come out of the ground towards your target
+/datum/action/cooldown/chasing_spikes
+ name = "impaling tendril"
+ desc = "Send a spiked subterranean tendril chasing after your target."
+ button_icon = 'icons/mob/simple/meteor_heart.dmi'
+ button_icon_state = "spike"
+ cooldown_time = 10 SECONDS
+ click_to_activate = TRUE
+ /// Lazy list of references to spike trails
+ var/list/active_chasers
+
+/datum/action/cooldown/chasing_spikes/Activate(atom/target)
+ . = ..()
+ playsound(owner, 'sound/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+ var/obj/effect/temp_visual/spike_chaser/chaser = new(get_turf(owner), target)
+ LAZYADD(active_chasers, WEAKREF(chaser))
+ RegisterSignal(chaser, COMSIG_PARENT_QDELETING, PROC_REF(on_chaser_destroyed))
+
+/// Remove a spike trail from our list of active trails
+/datum/action/cooldown/chasing_spikes/proc/on_chaser_destroyed(atom/chaser)
+ SIGNAL_HANDLER
+ LAZYREMOVE(active_chasers, WEAKREF(chaser))
+
+// Clean up after ourselves
+/datum/action/cooldown/chasing_spikes/Remove(mob/removed_from)
+ QDEL_LIST(active_chasers)
+ return ..()
+
+/// An invisible effect which chases a target, spawning spikes every so often.
+/obj/effect/temp_visual/spike_chaser
+ name = "spike chaser"
+ desc = "An invisible effect, how did you examine this?"
+ icon = 'icons/mob/silicon/cameramob.dmi'
+ icon_state = "marker"
+ duration = 15 SECONDS
+ invisibility = INVISIBILITY_ABSTRACT
+ /// Speed at which we chase target
+ var/move_speed = 3
+ /// What are we chasing?
+ var/datum/weakref/target
+ /// Handles chasing the target
+ var/datum/move_loop/movement
+
+/obj/effect/temp_visual/spike_chaser/Initialize(mapload, atom/target)
+ . = ..()
+ if (!target)
+ return INITIALIZE_HINT_QDEL
+
+ AddElement(/datum/element/floor_loving)
+ AddComponent(/datum/component/spawner, spawn_types = list(/obj/effect/temp_visual/emerging_ground_spike), spawn_time = 0.5 SECONDS)
+ src.target = WEAKREF(target)
+ movement = SSmove_manager.move_towards(src, chasing = target, delay = move_speed, home = TRUE, timeout = duration, flags = MOVEMENT_LOOP_START_FAST)
+
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_invalid))
+ if (isliving(target))
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_invalid))
+
+/// Destroy ourselves if the target is no longer valid
+/obj/effect/temp_visual/spike_chaser/proc/on_target_invalid()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/obj/effect/temp_visual/spike_chaser/Destroy()
+ QDEL_NULL(movement)
+ return ..()
+
+/// A spike comes out of the ground, dealing damage after a short delay
+/obj/effect/temp_visual/emerging_ground_spike
+ name = "bone spike"
+ desc = "A sharp spur of bone erupting from the ground!"
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "spike"
+ duration = 1 SECONDS
+ /// Time until we hurt people stood on us
+ var/harm_delay = 0.3 SECONDS
+ /// Amount by which to vary our position on spawn
+ var/position_variance = 8
+ /// Damage to deal on impale
+ var/impale_damage = 15
+ /// Typecache of types of mobs not to damage
+ var/list/damage_blacklist_typecache = list(
+ /mob/living/basic/meteor_heart,
+ )
+ /// Weighted list of body zones to target while standing
+ var/static/list/standing_damage_zones = list(
+ BODY_ZONE_CHEST = 1,
+ BODY_ZONE_R_LEG = 3,
+ BODY_ZONE_L_LEG = 3,
+ )
+
+/obj/effect/temp_visual/emerging_ground_spike/Initialize(mapload)
+ . = ..()
+ damage_blacklist_typecache = typecacheof(damage_blacklist_typecache)
+ pixel_x += rand(-position_variance, position_variance)
+ pixel_y += rand(-position_variance, position_variance)
+ addtimer(CALLBACK(src, PROC_REF(impale)), harm_delay, TIMER_DELETE_ME)
+
+/// Stab people who are stood on us after a delay in the shins
+/obj/effect/temp_visual/emerging_ground_spike/proc/impale()
+ if (!isturf(loc))
+ return
+ var/hit_someone = FALSE
+ for(var/mob/living/victim in loc)
+ if (is_type_in_typecache(victim, damage_blacklist_typecache))
+ continue
+ hit_someone = TRUE
+ var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
+ victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
+ if (hit_someone)
+ playsound(src, 'sound/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ else
+ playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE)
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_eyeball.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_eyeball.dm
new file mode 100644
index 00000000000..44775ea7217
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_eyeball.dm
@@ -0,0 +1,36 @@
+#define EYEBALL_BLINK_INTERVAL_MIN 10 SECONDS
+#define EYEBALL_BLINK_INTERVAL_MAX 30 SECONDS
+
+/// List of all the meteor eyeballs so we can gib them upon meteor death
+GLOBAL_LIST_EMPTY(meteor_eyeballs)
+
+/// Basically just an organic floor light
+/obj/structure/meateor_fluff/eyeball
+ name = "beady eye"
+ desc = "An eyeball growing out of the ground, gross."
+ icon_state = "eyeball"
+ max_integrity = 15
+ layer = LOW_OBJ_LAYER
+ plane = FLOOR_PLANE
+
+/obj/structure/meateor_fluff/eyeball/Initialize(mapload)
+ . = ..()
+ GLOB.meteor_eyeballs += src
+ set_light(l_range = 4, l_color = COLOR_VERY_SOFT_YELLOW)
+ blink()
+
+/// Play a blinking animation and queue it again
+/obj/structure/meateor_fluff/eyeball/proc/blink()
+ flick("eyeball_blink", src)
+ addtimer(CALLBACK(src, PROC_REF(blink)), rand(EYEBALL_BLINK_INTERVAL_MIN, EYEBALL_BLINK_INTERVAL_MAX), TIMER_DELETE_ME)
+
+/obj/structure/meateor_fluff/eyeball/atom_destruction(damage_flag)
+ new /obj/effect/gibspawner/generic(loc)
+ return ..()
+
+/obj/structure/meateor_fluff/eyeball/Destroy()
+ GLOB.meteor_eyeballs -= src
+ return ..()
+
+#undef EYEBALL_BLINK_INTERVAL_MIN
+#undef EYEBALL_BLINK_INTERVAL_MAX
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm
new file mode 100644
index 00000000000..e56bbc4aa84
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart.dm
@@ -0,0 +1,131 @@
+#define HEARTBEAT_NORMAL (1.2 SECONDS)
+#define HEARTBEAT_FAST (0.6 SECONDS)
+#define HEARTBEAT_FRANTIC (0.4 SECONDS)
+
+/mob/living/basic/meteor_heart
+ name = "meteor heart"
+ desc = "A pulsing lump of flesh and bone growing directly out of the ground."
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "heart"
+ icon_living = "heart"
+ mob_biotypes = MOB_ORGANIC
+ basic_mob_flags = DEL_ON_DEATH
+ mob_size = MOB_SIZE_HUGE
+ health = 600 // 15 PKA shots
+ maxHealth = 600
+ pressure_resistance = 200
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes"
+ response_disarm_simple = "gently push"
+ faction = list()
+ ai_controller = /datum/ai_controller/basic_controller/meteor_heart
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = 1500
+ combat_mode = TRUE
+ move_resist = INFINITY // This mob IS the floor
+ /// Action which sends a line of spikes chasing a player
+ var/datum/action/cooldown/chasing_spikes/spikes
+ /// Action which summons areas the player can't stand in
+ var/datum/action/cooldown/spine_traps/traps
+ /// Looping heartbeat sound
+ var/datum/looping_sound/heartbeat/soundloop
+
+/mob/living/basic/meteor_heart/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_IMMOBILIZED, INNATE_TRAIT)
+ AddElement(/datum/element/death_drops, list(/obj/effect/temp_visual/meteor_heart_death))
+ AddElement(/datum/element/relay_attackers)
+
+ spikes = new(src)
+ spikes.Grant(src)
+ ai_controller.blackboard[BB_METEOR_HEART_GROUND_SPIKES] = WEAKREF(spikes)
+
+ traps = new(src)
+ traps.Grant(src)
+ ai_controller.blackboard[BB_METEOR_HEART_SPINE_TRAPS] = WEAKREF(traps)
+
+ ai_controller.set_ai_status(AI_STATUS_OFF)
+
+ RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(used_ability))
+ RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(aggro))
+
+ for (var/obj/structure/meateor_fluff/body_part in view(5, src))
+ RegisterSignal(body_part, COMSIG_ATOM_DESTRUCTION, PROC_REF(aggro))
+
+ soundloop = new(src, start_immediately = FALSE)
+ soundloop.mid_length = HEARTBEAT_NORMAL
+ soundloop.pressure_affected = FALSE
+ soundloop.start()
+
+/// Called when we get mad at something, either for attacking us or attacking the nearby area
+/mob/living/basic/meteor_heart/proc/aggro()
+ if (ai_controller.ai_status == AI_STATUS_ON)
+ return
+ ai_controller.reset_ai_status()
+ if (!ai_controller.ai_status == AI_STATUS_ON)
+ return
+ icon_state = "heart_aggro"
+ soundloop.set_mid_length(HEARTBEAT_FAST)
+
+/// Called when we stop being mad
+/mob/living/basic/meteor_heart/proc/deaggro()
+ ai_controller.set_ai_status(AI_STATUS_OFF)
+ icon_state = "heart"
+ soundloop.set_mid_length(HEARTBEAT_NORMAL)
+
+/// Animate when using certain abilities
+/mob/living/basic/meteor_heart/proc/used_ability(mob/living/owner, datum/action/cooldown/ability)
+ SIGNAL_HANDLER
+ if (ability != spikes)
+ return
+ Shake(1, 0, 1.5 SECONDS)
+
+/mob/living/basic/meteor_heart/Destroy()
+ QDEL_NULL(spikes)
+ QDEL_NULL(traps)
+ QDEL_NULL(soundloop)
+ return ..()
+
+/// Dramatic death animation for the meteor heart mob
+/obj/effect/temp_visual/meteor_heart_death
+ name = "meteor heart"
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "heart_dying"
+ desc = "You've killed this innocent asteroid, I hope you feel happy."
+ duration = 3 SECONDS
+ /// Looping heartbeat sound
+ var/datum/looping_sound/heartbeat/soundloop
+
+/obj/effect/temp_visual/meteor_heart_death/Initialize(mapload)
+ . = ..()
+ playsound(src, 'sound/magic/demon_dies.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+ Shake(2, 0, 3 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(gib)), duration - 1, TIMER_DELETE_ME)
+ soundloop = new(src, start_immediately = FALSE)
+ soundloop.mid_length = HEARTBEAT_FRANTIC
+ soundloop.pressure_affected = FALSE
+ soundloop.start()
+
+/obj/effect/temp_visual/meteor_heart_death/Destroy()
+ QDEL_NULL(soundloop)
+ return ..()
+
+/// Make this place a mess
+/obj/effect/temp_visual/meteor_heart_death/proc/gib()
+ playsound(loc, 'sound/effects/attackblob.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+ var/turf/my_turf = get_turf(src)
+ new /obj/effect/gibspawner/human(my_turf)
+ for (var/obj/structure/eyeball as anything in GLOB.meteor_eyeballs)
+ if (eyeball.z != src.z)
+ continue
+ addtimer(CALLBACK(eyeball, TYPE_PROC_REF(/atom/, take_damage), eyeball.max_integrity), rand(0.5 SECONDS, 2 SECONDS)) // pop!
+ for (var/mob/murderer in range(10, src))
+ if (!murderer.client || isspaceturf(get_turf(murderer)))
+ continue
+ shake_camera(murderer, duration = 2 SECONDS, strength = 2)
+
+#undef HEARTBEAT_NORMAL
+#undef HEARTBEAT_FAST
+#undef HEARTBEAT_FRANTIC
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart_ai.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart_ai.dm
new file mode 100644
index 00000000000..7684011bd66
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/meteor_heart_ai.dm
@@ -0,0 +1,32 @@
+/// A spellcasting AI which does not move
+/datum/ai_controller/basic_controller/meteor_heart
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
+ BB_TARGETLESS_TIME = 0,
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability/ground_spikes,
+ /datum/ai_planning_subtree/use_mob_ability/spine_traps,
+ /datum/ai_planning_subtree/sleep_with_no_target/meteor_heart,
+ )
+
+/datum/ai_planning_subtree/targeted_mob_ability/ground_spikes
+ ability_key = BB_METEOR_HEART_GROUND_SPIKES
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/use_mob_ability/spine_traps
+ ability_key = BB_METEOR_HEART_SPINE_TRAPS
+
+/// After enough time with no target, deaggro and change animation state
+/datum/ai_planning_subtree/sleep_with_no_target/meteor_heart
+ sleep_behaviour = /datum/ai_behavior/sleep_after_targetless_time/meteor_heart
+
+/datum/ai_behavior/sleep_after_targetless_time/meteor_heart
+
+/datum/ai_behavior/sleep_after_targetless_time/meteor_heart/enter_sleep(datum/ai_controller/controller)
+ var/mob/living/basic/meteor_heart/heart = controller.pawn
+ if (!istype(heart))
+ return ..()
+ heart.deaggro()
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/spine_traps.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/spine_traps.dm
new file mode 100644
index 00000000000..a2a3b52b835
--- /dev/null
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/spine_traps.dm
@@ -0,0 +1,97 @@
+/// Marks several areas with thrusting spines which damage and slow people
+/datum/action/cooldown/spine_traps
+ name = "thrusting spines"
+ desc = "Mark several nearby areas with thrusting spines, which will spring up when disturbed."
+ button_icon = 'icons/mob/simple/meteor_heart.dmi'
+ button_icon_state = "spikes_stabbing"
+ cooldown_time = 15 SECONDS
+ /// Create zones at most this far away
+ var/range = 3
+ /// Don't create zones within this radius
+ var/min_range = 2
+ /// Number of zones to place
+ var/zones_to_create = 3
+
+/datum/action/cooldown/spine_traps/Activate(atom/target)
+ . = ..()
+
+ playsound(owner, 'sound/magic/demon_consume.ogg', vol = 100, falloff_exponent = 2, vary = TRUE, pressure_affected = FALSE)
+ var/list/valid_turfs = list()
+ var/turf/our_turf = get_turf(owner)
+ for (var/turf/zone_turf in orange(range, our_turf))
+ if (!is_valid_turf(zone_turf) || get_dist(zone_turf, our_turf) < min_range)
+ continue
+ valid_turfs += zone_turf
+
+ var/created = 0
+ while(length(valid_turfs) && created < zones_to_create)
+ var/turf/place_turf = pick_n_take(valid_turfs)
+ var/list/covered_turfs = place_zone(place_turf)
+ valid_turfs -= covered_turfs
+ created++
+
+/// Returns true if we can place a trap at the specified location
+/datum/action/cooldown/spine_traps/proc/is_valid_turf(turf/target_turf)
+ return !target_turf.is_blocked_turf(exclude_mobs = TRUE) && !isspaceturf(target_turf) && !isopenspaceturf(target_turf)
+
+/// Places a 3x3 area of spike traps around a central provided point, returns the list of now occupied turfs
+/datum/action/cooldown/spine_traps/proc/place_zone(turf/target_turf)
+ var/list/used_turfs = list()
+ for (var/turf/zone_turf in range(1, target_turf))
+ if (!is_valid_turf(zone_turf))
+ continue
+ new /obj/effect/temp_visual/thrusting_spines(zone_turf)
+ used_turfs += zone_turf
+ return used_turfs
+
+/obj/effect/temp_visual/thrusting_spines
+ icon = 'icons/mob/simple/meteor_heart.dmi'
+ icon_state = "spikes_idle"
+ desc = "Sharp spines lying in wait in the ground, you probably don't want to walk on those."
+ duration = 10 SECONDS
+ /// If this will trigger a trap when entered
+ var/active = FALSE
+ /// Damage to deal on activation
+ var/impale_damage = 10
+ /// Time between activations
+ COOLDOWN_DECLARE(thrust_delay)
+ /// Weighted list of body zones to target while standing
+ var/static/list/standing_damage_zones = list(
+ BODY_ZONE_CHEST = 1,
+ BODY_ZONE_R_LEG = 3,
+ BODY_ZONE_L_LEG = 3,
+ )
+
+/obj/effect/temp_visual/thrusting_spines/Initialize(mapload)
+ . = ..()
+ flick("spikes_emerge", src)
+ addtimer(CALLBACK(src, PROC_REF(ready)), 1 SECONDS, TIMER_DELETE_ME)
+ addtimer(CALLBACK(src, PROC_REF(retract)), duration - (0.5 SECONDS), TIMER_DELETE_ME)
+
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/// Called when we're ready to start impaling people
+/obj/effect/temp_visual/thrusting_spines/proc/ready()
+ active = TRUE
+
+/// Called when it is time to stop impaling people
+/obj/effect/temp_visual/thrusting_spines/proc/retract()
+ active = FALSE
+ icon_state = "spikes_submerge"
+
+/// Called when something enters our turf, if it is a non-flying mob then give it a stab
+/obj/effect/temp_visual/thrusting_spines/proc/on_entered(datum/source, atom/movable/arrived)
+ if (!active || !isliving(arrived) || (arrived.movement_type & (FLYING | FLOATING)))
+ return
+ if (!COOLDOWN_FINISHED(src, thrust_delay))
+ return
+
+ COOLDOWN_START(src, thrust_delay, 0.7 SECONDS)
+ playsound(src, 'sound/weapons/pierce.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ var/mob/living/victim = arrived
+ flick("spikes_stabbing", src)
+ var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
+ victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
diff --git a/code/modules/mob/living/simple_animal/hostile/headcrab.dm b/code/modules/mob/living/simple_animal/hostile/headcrab.dm
index 7b38c352088..e0d761e7d25 100644
--- a/code/modules/mob/living/simple_animal/hostile/headcrab.dm
+++ b/code/modules/mob/living/simple_animal/hostile/headcrab.dm
@@ -21,8 +21,10 @@
obj_damage = 0
environment_smash = ENVIRONMENT_SMASH_NONE
speak_emote = list("squeaks")
+ /// The mind to transfer to our egg when it hatches
var/datum/mind/origin
- var/egg_lain = 0
+ /// Set to true once we've implanted our egg
+ var/egg_lain = FALSE
/mob/living/simple_animal/hostile/headcrab/Initialize(mapload)
. = ..()
@@ -39,20 +41,21 @@
I.forceMove(egg)
visible_message(span_warning("[src] plants something in [victim]'s flesh!"), \
span_danger("We inject our egg into [victim]'s body!"))
- egg_lain = 1
+ egg_lain = TRUE
/mob/living/simple_animal/hostile/headcrab/AttackingTarget()
. = ..()
- if(. && !egg_lain && iscarbon(target) && !ismonkey(target))
- // Changeling egg can survive in aliens!
- var/mob/living/carbon/C = target
- if(C.stat == DEAD)
- if(HAS_TRAIT(C, TRAIT_XENO_HOST))
- to_chat(src, span_userdanger("A foreign presence repels us from this body. Perhaps we should try to infest another?"))
- return
- Infect(target)
- to_chat(src, span_userdanger("With our egg laid, our death approaches rapidly..."))
- addtimer(CALLBACK(src, PROC_REF(death)), 100)
+ if (!. || egg_lain || !iscarbon(target) || ismonkey(target))
+ return
+ var/mob/living/carbon/victim = target
+ if(victim.stat != DEAD)
+ return
+ if(HAS_TRAIT(victim, TRAIT_XENO_HOST))
+ target.balloon_alert(src, "already pregnant!") // Maybe the worst balloon alert in the codebase
+ return
+ Infect(target)
+ to_chat(src, span_userdanger("With our egg laid, our death approaches rapidly..."))
+ addtimer(CALLBACK(src, PROC_REF(death)), 10 SECONDS)
/obj/item/organ/internal/body_egg/changeling_egg
name = "changeling egg"
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
index beb3698ea8e..1bd981e2680 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
@@ -284,9 +284,9 @@
.=..()
AddComponent(\
/datum/component/spawner,\
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion),\
+ spawn_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion),\
spawn_time = 20 SECONDS,\
- max_mobs = 3,\
+ max_spawned = 3,\
spawn_text = "peels itself off from",\
faction = faction,\
)
diff --git a/code/modules/mob_spawn/corpses/mob_corpses.dm b/code/modules/mob_spawn/corpses/mob_corpses.dm
index 76d31e80d89..2e4901a2793 100644
--- a/code/modules/mob_spawn/corpses/mob_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mob_corpses.dm
@@ -86,6 +86,19 @@
id = /obj/item/card/id/advanced/chameleon
id_trim = /datum/id_trim/chameleon/operative
+/obj/effect/mob_spawn/corpse/human/tigercultist
+ name = "Tiger Cooperative Cultist"
+ outfit = /datum/outfit/tigercultcorpse
+
+/datum/outfit/tigercultcorpse
+ name = "Tiger Cooperative Corpse"
+ uniform = /obj/item/clothing/under/rank/civilian/chaplain
+ suit = /obj/item/clothing/suit/hooded/chaplain_hoodie
+ shoes = /obj/item/clothing/shoes/laceup
+ neck = /obj/item/clothing/neck/fake_heretic_amulet
+ head = /obj/item/clothing/head/hooded/chaplain_hood
+ back = /obj/item/storage/backpack/cultpack
+
/obj/effect/mob_spawn/corpse/human/pirate
name = "Pirate"
skin_tone = "caucasian1" //all pirates are white because it's easier that way
diff --git a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
index bf70f7f33b7..6796bb011f8 100644
--- a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
+++ b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm
@@ -43,3 +43,13 @@
mob_type = /mob/living/simple_animal/hostile/asteroid/goliath/beast
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "goliath_dead_helper"
+
+/// Dead headcrab for changeling-themed ruins
+/obj/effect/mob_spawn/corpse/headcrab
+ mob_type = /mob/living/simple_animal/hostile/headcrab
+ icon = 'icons/mob/simple/animal.dmi'
+ icon_state = "headcrab_dead"
+
+/obj/effect/mob_spawn/corpse/headcrab/special(mob/living/simple_animal/hostile/headcrab/crab)
+ . = ..()
+ crab.egg_lain = TRUE // Prevents using mad science to become a changeling
diff --git a/config/spaceruinblacklist.txt b/config/spaceruinblacklist.txt
index 35519bb5af1..17b380af519 100644
--- a/config/spaceruinblacklist.txt
+++ b/config/spaceruinblacklist.txt
@@ -38,6 +38,7 @@
#_maps/RandomRuins/SpaceRuins/hilbertshoteltestingsite.dmm
#_maps/RandomRuins/SpaceRuins/intactemptyship.dmm
#_maps/RandomRuins/SpaceRuins/listeningstation.dmm
+#_maps/RandomRuins/SpaceRuins/meateor.dmm
#_maps/RandomRuins/SpaceRuins/mechtransport.dmm
#_maps/RandomRuins/SpaceRuins/mrow_thats_right
#_maps/RandomRuins/SpaceRuins/old_infiltrator.dmm
diff --git a/icons/mob/simple/meteor_heart.dmi b/icons/mob/simple/meteor_heart.dmi
new file mode 100644
index 00000000000..2636a8293b8
Binary files /dev/null and b/icons/mob/simple/meteor_heart.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index d460ff00cbd..9d5b480a75a 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -763,6 +763,7 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_nearest_target_to_flee.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_target.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\sleep_with_no_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\speech_subtree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\targeted_mob_ability.dm"
@@ -1164,6 +1165,7 @@
#include "code\datums\elements\eyestab.dm"
#include "code\datums\elements\falling_hazard.dm"
#include "code\datums\elements\firestacker.dm"
+#include "code\datums\elements\floorloving.dm"
#include "code\datums\elements\footstep.dm"
#include "code\datums\elements\forced_gravity.dm"
#include "code\datums\elements\frozen.dm"
@@ -3695,6 +3697,7 @@
#include "code\modules\mapfluff\ruins\spaceruin_code\hellfactory.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\hilbertshotel.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\listeningstation.dm"
+#include "code\modules\mapfluff\ruins\spaceruin_code\meateor.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\oldstation.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\originalcontent.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\spacehotel.dm"
@@ -3849,6 +3852,11 @@
#include "code\modules\mob\living\basic\space_fauna\giant_spider\spider_abilities\lay_eggs.dm"
#include "code\modules\mob\living\basic\space_fauna\giant_spider\spider_abilities\web.dm"
#include "code\modules\mob\living\basic\space_fauna\giant_spider\spider_abilities\wrap.dm"
+#include "code\modules\mob\living\basic\space_fauna\meteor_heart\chasing_spikes.dm"
+#include "code\modules\mob\living\basic\space_fauna\meteor_heart\meteor_eyeball.dm"
+#include "code\modules\mob\living\basic\space_fauna\meteor_heart\meteor_heart.dm"
+#include "code\modules\mob\living\basic\space_fauna\meteor_heart\meteor_heart_ai.dm"
+#include "code\modules\mob\living\basic\space_fauna\meteor_heart\spine_traps.dm"
#include "code\modules\mob\living\basic\space_fauna\netherworld\blankbody.dm"
#include "code\modules\mob\living\basic\space_fauna\netherworld\creature.dm"
#include "code\modules\mob\living\basic\space_fauna\netherworld\migo.dm"