diff --git a/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm
new file mode 100644
index 00000000000..4dea1c2fc62
--- /dev/null
+++ b/_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm
@@ -0,0 +1,272 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"a" = (
+/turf/template_noop,
+/area/template_noop)
+"b" = (
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"c" = (
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/wall/indestructible/boss,
+/area/lavaland/surface/outdoors)
+"d" = (
+/obj/structure/flora/ash/rock/style_random,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"e" = (
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/mineral/random/high_chance/volcanic,
+/area/lavaland/surface/outdoors)
+"f" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/block,
+/obj/effect/mob_spawn/corpse/watcher/goliath_chewed,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"g" = (
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/item/reagent_containers/food/snacks/egg/watcher,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"h" = (
+/obj/structure/flora/ash/cap_shroom,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"i" = (
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/bombable_wall,
+/turf/simulated/wall/indestructible/boss,
+/area/lavaland/surface/outdoors)
+"j" = (
+/obj/effect/mob_spawn/corpse/watcher/crushed,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"k" = (
+/obj/structure/stone_tile/cracked,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"l" = (
+/obj/structure/stone_tile/slab,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"m" = (
+/obj/structure/stone_tile/slab/cracked,
+/obj/effect/mob_spawn/corpse/goliath/pierced,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"o" = (
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/mineral/random/volcanic,
+/area/lavaland/surface/outdoors)
+"p" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"q" = (
+/obj/structure/flora/ash/leaf_shroom,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"r" = (
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"t" = (
+/obj/structure/flora/ash/cacti,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"v" = (
+/obj/structure/stone_tile/surrounding/cracked,
+/obj/structure/stone_tile/center/cracked,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"w" = (
+/turf/simulated/floor/plating/lava/smooth/mapping_lava,
+/area/lavaland/surface/outdoors)
+"A" = (
+/obj/structure/flora/ash/leaf_shroom,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"F" = (
+/obj/structure/flora/ash/rock/style_random,
+/obj/effect/mapping_helpers/no_lava,
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"J" = (
+/obj/effect/mapping_helpers/no_lava,
+/turf/simulated/wall/indestructible/boss,
+/area/lavaland/surface/outdoors)
+"M" = (
+/obj/effect/mapping_helpers/no_lava,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/turf/simulated/floor/plating/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"Q" = (
+/turf/simulated/mineral/random/volcanic,
+/area/lavaland/surface/outdoors)
+"Z" = (
+/obj/effect/mapping_helpers/no_lava,
+/obj/structure/stone_tile/slab/cracked,
+/turf/space,
+/area/lavaland/surface/outdoors)
+
+(1,1,1) = {"
+a
+a
+a
+e
+e
+e
+M
+e
+o
+a
+a
+"}
+(2,1,1) = {"
+a
+o
+o
+c
+c
+J
+i
+J
+o
+o
+a
+"}
+(3,1,1) = {"
+a
+o
+J
+c
+h
+d
+b
+J
+w
+o
+o
+"}
+(4,1,1) = {"
+a
+o
+J
+v
+f
+k
+b
+w
+w
+w
+Q
+"}
+(5,1,1) = {"
+a
+o
+J
+p
+g
+l
+b
+t
+w
+w
+w
+"}
+(6,1,1) = {"
+o
+o
+J
+q
+r
+m
+b
+b
+J
+w
+w
+"}
+(7,1,1) = {"
+a
+o
+J
+c
+F
+k
+b
+A
+J
+o
+o
+"}
+(8,1,1) = {"
+a
+o
+o
+c
+c
+J
+i
+J
+J
+o
+o
+"}
+(9,1,1) = {"
+a
+a
+o
+o
+o
+J
+Z
+J
+o
+o
+a
+"}
+(10,1,1) = {"
+a
+a
+a
+a
+a
+o
+j
+o
+a
+a
+a
+"}
diff --git a/code/datums/components/corpse_description.dm b/code/datums/components/corpse_description.dm
new file mode 100644
index 00000000000..1378b56628b
--- /dev/null
+++ b/code/datums/components/corpse_description.dm
@@ -0,0 +1,32 @@
+/**
+ * Mapping component which adds examine text to a corpse that is removed on revival (in case for some reason someone revives your space ruin mob)
+ * For immersive environmental story telling
+ */
+/datum/component/corpse_description
+ /// What do we display on examine?
+ var/description_text = ""
+ /// What do we display if examined by a clown?
+ var/naive_description = ""
+
+/datum/component/corpse_description/Initialize(description_text = "", naive_description = "")
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.description_text = description_text
+ src.naive_description = naive_description
+
+/datum/component/corpse_description/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_LIVING_REVIVE, PROC_REF(signal_qdel))
+
+/datum/component/corpse_description/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_PARENT_EXAMINE, COMSIG_LIVING_REVIVE))
+
+/datum/component/corpse_description/proc/on_examined(mob/living/corpse, mob/corpse_inspector, list/examine_list)
+ SIGNAL_HANDLER
+ if(corpse.stat != DEAD || !description_text)
+ return // Why the hell you put this on them then
+ if(naive_description && HAS_TRAIT(corpse_inspector, TRAIT_COMIC_SANS))
+ examine_list += "[naive_description]"
+ return
+ examine_list += "[description_text]"
diff --git a/code/datums/elements/bombable_turf.dm b/code/datums/elements/bombable_turf.dm
new file mode 100644
index 00000000000..63060c9df85
--- /dev/null
+++ b/code/datums/elements/bombable_turf.dm
@@ -0,0 +1,41 @@
+/**
+ * Apply this to a turf (usually a wall) and it will be destroyed instantly by any explosion.
+ * Most walls can already be destroyed by explosions so this is largely for usually indestructible ones.
+ * For applying it in a map editor, use /obj/effect/mapping_helpers/bombable_wall
+ */
+/datum/element/bombable_turf
+
+/datum/element/bombable_turf/Attach(turf/target)
+ . = ..()
+ if(!iswallturf(target))
+ return ELEMENT_INCOMPATIBLE
+ var/turf/simulated/wall/W = target
+ W.explosion_block = 1
+ W.explodable = TRUE
+
+ RegisterSignal(W, COMSIG_ATOM_EX_ACT, PROC_REF(detonate))
+ RegisterSignal(W, COMSIG_TURF_CHANGE, PROC_REF(turf_changed))
+ RegisterSignal(W, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+
+ W.update_appearance(UPDATE_OVERLAYS)
+
+/datum/element/bombable_turf/Detach(turf/source)
+ UnregisterSignal(source, list(COMSIG_ATOM_EX_ACT, COMSIG_TURF_CHANGE, COMSIG_PARENT_EXAMINE))
+ source.explosion_block = initial(source.explosion_block)
+ source.update_appearance(UPDATE_OVERLAYS)
+ return ..()
+
+/// If we get blowed up, move to the next turf
+/datum/element/bombable_turf/proc/detonate(turf/source)
+ SIGNAL_HANDLER
+ source.ChangeTurf(source.baseturf)
+
+/// If this turf becomes something else we either just went off or regardless don't want this any more
+/datum/element/bombable_turf/proc/turf_changed(turf/source)
+ SIGNAL_HANDLER
+ Detach(source)
+
+/// Show a little extra on examine
+/datum/element/bombable_turf/proc/on_examined(turf/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ examine_list += "It seems to be slightly cracked..."
diff --git a/code/datums/movement_detector.dm b/code/datums/movement_detector.dm
new file mode 100644
index 00000000000..0de904dde2a
--- /dev/null
+++ b/code/datums/movement_detector.dm
@@ -0,0 +1,56 @@
+/// A datum to handle the busywork of registering signals to handle in depth tracking of a movable
+/datum/movement_detector
+ var/atom/movable/tracked
+ /// Listener is whatever callback that will increase the tracking of the movable, usually on stepped.
+ var/datum/callback/listener
+
+/datum/movement_detector/New(atom/movable/target, datum/callback/listener)
+ if(target)
+ track(target, listener)
+
+/datum/movement_detector/Destroy()
+ untrack()
+ tracked = null
+ listener = null
+ return ..()
+
+/// Sets up tracking of the given movable atom
+/datum/movement_detector/proc/track(atom/movable/target, datum/callback/listener)
+ untrack()
+ tracked = target
+ src.listener = listener
+
+ if(ismovable(target))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(move_react))
+ target = target.loc
+
+/// Stops tracking
+/datum/movement_detector/proc/untrack()
+ if(!tracked)
+ return
+ var/atom/movable/target = tracked
+ if(ismovable(target))
+ UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
+ target = target.loc
+
+/**
+ * Reacts to any movement that would cause a change in coordinates of the tracked movable atom
+ * This works by detecting movement of either the tracked object, or anything it is inside, recursively
+ */
+/datum/movement_detector/proc/move_react(atom/movable/mover, atom/oldloc, direction)
+ SIGNAL_HANDLER
+
+ var/turf/newturf = get_turf(tracked)
+
+ if(oldloc && !isturf(oldloc))
+ var/atom/target = oldloc
+ if(ismovable(target))
+ UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
+ target = target.loc
+ if(tracked.loc != newturf)
+ var/atom/target = mover.loc
+ if(ismovable(target))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(move_react), TRUE)
+ target = target.loc
+
+ listener.Invoke(tracked, mover, oldloc, direction)
diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm
index 57abc708635..239a71da0a7 100644
--- a/code/datums/ruins/lavaland.dm
+++ b/code/datums/ruins/lavaland.dm
@@ -216,3 +216,11 @@
suffix = "lavaland_surface_monster_nest.dmm"
allow_duplicates = FALSE
cost = 5
+
+/datum/map_template/ruin/lavaland/watcher_grave
+ name = "Watchers' Grave"
+ id = "watcher-grave"
+ description = "A lonely cave where an orphaned child awaits a new parent."
+ suffix = "lavaland_surface_watcher_grave.dmm"
+ cost = 5
+ allow_duplicates = FALSE
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 0b7280021ce..a2a2294907b 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -160,6 +160,18 @@
loc = T
Moved(old_loc, get_dir(old_loc, loc))
+
+/**
+ * meant for movement with zero side effects. only use for objects that are supposed to move "invisibly" (like camera mobs or ghosts)
+ * if you want something to move onto a tile with a beartrap or recycler or tripmine or mouse without that object knowing about it at all, use this
+ * most of the time you want forceMove()
+ */
+/atom/movable/proc/abstract_move(atom/new_loc)
+ var/atom/old_loc = loc
+ var/direction = get_dir(old_loc, new_loc)
+ loc = new_loc
+ Moved(old_loc, direction, TRUE)
+
/atom/movable/Move(atom/newloc, direct = 0, movetime)
if(!loc || !newloc) return 0
var/atom/oldloc = loc
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index 3eda5902247..fd263b11f9d 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -52,6 +52,7 @@
return
if(resistance_flags & INDESTRUCTIBLE)
return
+ SEND_SIGNAL(src, COMSIG_ATOM_EX_ACT, severity)
switch(severity)
if(1)
take_damage(INFINITY, BRUTE, BOMB, 0)
diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm
index 3f688289dda..a90566c998e 100644
--- a/code/game/turfs/simulated/walls.dm
+++ b/code/game/turfs/simulated/walls.dm
@@ -45,6 +45,10 @@
var/rusted = FALSE
/// Have we got a rusty overlay?
var/rusted_overlay
+ /// Are we a explodable turf?
+ var/explodable = FALSE
+ /// Do we have a explodable overlay?
+ var/explodable_overlay
/turf/simulated/wall/Initialize(mapload)
. = ..()
@@ -105,6 +109,10 @@
rusted_overlay = icon('icons/turf/overlays.dmi', pick("rust", "rust2"), pick(NORTH, SOUTH, EAST, WEST))
. += rusted_overlay
+ if(explodable && !explodable_overlay)
+ explodable_overlay = icon('icons/turf/overlays.dmi', pick("explodable"), pick(NORTH, SOUTH, EAST, WEST))
+ . += explodable_overlay
+
if(!damage)
. += dent_decals
return
@@ -185,6 +193,7 @@
new /obj/item/stack/sheet/metal(src)
/turf/simulated/wall/ex_act(severity)
+ SEND_SIGNAL(src, COMSIG_ATOM_EX_ACT, severity)
switch(severity)
if(1.0)
ChangeTurf(baseturf)
diff --git a/code/game/turfs/simulated/walls_indestructible.dm b/code/game/turfs/simulated/walls_indestructible.dm
index 9180ba87cdf..c3014461060 100644
--- a/code/game/turfs/simulated/walls_indestructible.dm
+++ b/code/game/turfs/simulated/walls_indestructible.dm
@@ -16,6 +16,7 @@
return
/turf/simulated/wall/indestructible/ex_act(severity)
+ SEND_SIGNAL(src, COMSIG_ATOM_EX_ACT, severity)
return
/turf/simulated/wall/indestructible/blob_act(obj/structure/blob/B)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index a931c24989a..f486eec63b6 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -228,6 +228,7 @@
var/old_corners = corners
BeforeChange()
+ SEND_SIGNAL(src, COMSIG_TURF_CHANGE, path, defer_change, keep_icon, ignore_air, copy_existing_baseturf)
var/old_baseturf = baseturf
changing_turf = TRUE
diff --git a/code/modules/awaymissions/mission_code/ruins/watcher_grave.dm b/code/modules/awaymissions/mission_code/ruins/watcher_grave.dm
new file mode 100644
index 00000000000..003c2b2da8d
--- /dev/null
+++ b/code/modules/awaymissions/mission_code/ruins/watcher_grave.dm
@@ -0,0 +1,258 @@
+/obj/effect/mob_spawn/corpse/goliath/pierced
+ corpse_description = "Seems to have been pierced through the heart by a Watcher spike."
+ naive_corpse_description = "It's got a pretty big boo-boo, might need one of the large plasters."
+
+/obj/effect/mob_spawn/corpse/watcher/goliath_chewed
+ corpse_description = "Prior to its death, it was badly mangled by the jaws of a Goliath."
+ naive_corpse_description = "It's all tuckered out after playing rough with a Goliath."
+
+/obj/effect/mob_spawn/corpse/watcher/crushed
+ corpse_description = "Crushed by a rockslide, it seemed to have been scraping frantically at the rocks even as it perished."
+ naive_corpse_description = "All of those rocks probably don't make a comfortable blanket."
+
+/// if greater than lively mod, jiggles faster
+#define WATCHER_EGG_LIVELY_MOD 0.75
+/// If greater than active mod, egg is active and jiggles
+#define WATCHER_EGG_ACTIVE_MOD 0.5
+
+/// Egg which hatches into a helpful pet. Or you can eat it if you want.
+/obj/item/reagent_containers/food/snacks/egg/watcher
+ name = "watcher egg"
+ desc = "A lonely egg still pulsing with life, somehow untouched by the corruption of the Necropolis."
+ icon_state = "egg_watcher"
+ icon = 'icons/obj/ruin_objects.dmi'
+ tastes = list("ocular fluid" = 6, "loneliness" = 1)
+ antable = FALSE
+ /// How far have we moved?
+ var/steps_travelled = 0
+ /// How far should we travel to hatch?
+ var/steps_to_hatch = 600
+ /// Datum used to measure our steps
+ var/datum/movement_detector/pedometer
+
+/obj/item/reagent_containers/food/snacks/egg/watcher/Initialize(mapload)
+ . = ..()
+ pedometer = new(src, CALLBACK(src, PROC_REF(on_stepped)))
+
+/obj/item/reagent_containers/food/snacks/egg/watcher/Destroy(force)
+ . = ..()
+ QDEL_NULL(pedometer)
+
+/obj/item/reagent_containers/food/snacks/egg/watcher/examine(mob/user)
+ . = ..()
+ if(steps_travelled < (steps_to_hatch * WATCHER_EGG_ACTIVE_MOD))
+ . += "Something stirs listlessly inside."
+ else if(steps_travelled < (steps_to_hatch * WATCHER_EGG_LIVELY_MOD))
+ . += "Something is moving actively inside."
+ else
+ . += "It's jiggling wildly, it's about to hatch!"
+
+
+
+/// Called when we are moved, whether inside an inventory or by ourself somehow
+/obj/item/reagent_containers/food/snacks/egg/watcher/proc/on_stepped(atom/movable/egg, atom/mover, atom/old_loc, direction)
+ var/new_loc = get_turf(egg)
+ if(isnull(new_loc) || new_loc == get_turf(old_loc))
+ return // Didn't actually go anywhere
+ steps_travelled++
+ if(steps_travelled == steps_to_hatch * WATCHER_EGG_ACTIVE_MOD) //Halfway to hatching, start jiggling.
+ jiggle()
+ return
+ if(steps_travelled < steps_to_hatch)
+ return
+ visible_message("[src] splits and unfurls into a baby Watcher!")
+ playsound(new_loc, 'sound/effects/splat.ogg', 50, TRUE)
+ new /obj/effect/decal/cleanable/greenglow(new_loc)
+ new /obj/item/watcher_hatchling(new_loc)
+ qdel(src)
+
+/// Animate the egg
+/obj/item/reagent_containers/food/snacks/egg/watcher/proc/jiggle()
+ var/animation = isturf(loc) ? rand(1, 3) : 1 // Pixel_x/y animations don't work in an inventory
+ switch(animation)
+ if(1)
+ animate(src, transform = transform.Scale(1.3), time = 1 SECONDS, easing = BOUNCE_EASING)
+ animate(transform = matrix(), time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ if(2)
+ animate(src, pixel_y = 8, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ animate(pixel_y = 4, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_y = 0, time = 0.5 SECONDS, easing = BOUNCE_EASING | EASE_IN)
+ if(3)
+ Shake(2, 0, 0.3 SECONDS)
+ var/next_jiggle = rand(5 SECONDS, 10 SECONDS) / (steps_travelled >= steps_to_hatch * WATCHER_EGG_LIVELY_MOD ? 2 : 1)
+ addtimer(CALLBACK(src, PROC_REF(jiggle)), next_jiggle, TIMER_DELETE_ME)
+
+#undef WATCHER_EGG_LIVELY_MOD
+#undef WATCHER_EGG_ACTIVE_MOD
+
+
+/// A cute pet who will occasionally attack lavaland mobs for you
+/obj/item/watcher_hatchling
+ name = "watcher hatchling"
+ desc = "A newly born watcher, apparently free of the Necropolis' corruption. Perhaps one of the last."
+ icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+ icon_state = "watcher_baby"
+ resistance_flags = LAVA_PROOF | FIRE_PROOF //No. The child will not die to lava.
+ w_class = WEIGHT_CLASS_SMALL //pocket monster. Plus doesn't work in bag.
+ /// The effect we create when out and about
+ var/obj/effect/watcher_orbiter/orbiter
+ /// Who are we orbiting?
+ var/mob/living/owner
+
+/obj/item/watcher_hatchling/attack_self(mob/user, modifiers)
+ . = ..()
+ if(!isnull(orbiter))
+ watcher_return()
+ return
+ orbiter = new (get_turf(src))
+ orbiter.follow(user)
+ owner = user
+ RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(remove_owner))
+ RegisterSignal(orbiter, COMSIG_PARENT_QDELETING, PROC_REF(our_remove_orbiter))
+
+/obj/item/watcher_hatchling/Moved(atom/old_loc, movement_dir, forced)
+ . = ..()
+ if(isnull(orbiter))
+ return
+ var/mob/holder = get(loc, /mob)
+ if(holder != owner)
+ watcher_return()
+
+/// If the guy we are orbiting is deleted but somehow we aren't
+/obj/item/watcher_hatchling/proc/remove_owner()
+ SIGNAL_HANDLER
+ UnregisterSignal(owner, COMSIG_PARENT_QDELETING)
+ owner = null
+
+/// In the more likely event that our orbiter is deleted, stop holding a reference to it
+/obj/item/watcher_hatchling/proc/our_remove_orbiter()
+ SIGNAL_HANDLER
+ orbiter = null // No need to unregister signal because we only call this when it deletes
+
+/// Get back in your ball pikachu
+/obj/item/watcher_hatchling/proc/watcher_return()
+ qdel(orbiter)
+ remove_owner()
+
+
+/// Orbiting visual which shoots at mining mobs
+/obj/effect/watcher_orbiter
+ name = "watcher hatchling"
+ icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+ icon_state = "watcher_baby"
+ layer = EDGED_TURF_LAYER // Don't render under lightbulbs
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ pixel_y = 22
+ alpha = 0
+ /// Who are we following?
+ var/atom/parent
+ /// Datum which keeps us hanging out with our parent
+ var/datum/movement_detector/tracker
+ /// Type of projectile we fire
+ var/projectile_type = /obj/item/projectile/baby_watcher_blast
+ /// Sound to make when we shoot
+ var/projectile_sound = 'sound/weapons/pierce.ogg'
+ /// Time between taking potshots at goliaths
+ var/fire_delay = 5 SECONDS
+ /// How much faster do we shoot when avenging our parent?
+ var/on_death_multiplier = 5
+ /// Time taken between shots
+ COOLDOWN_DECLARE(shot_cooldown)
+ /// Types of mobs to attack
+ var/list/target_faction = list("hostile", "terrorspiders", "mining", "boss") //Yes terror spiders are their own faction. Also fuck them.
+
+/obj/effect/watcher_orbiter/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+
+// Shuttle rotation fucks with our position, we just want to stick with our guy
+/obj/effect/watcher_orbiter/shuttleRotate(rotation, params)
+ return
+
+/obj/effect/watcher_orbiter/Destroy(force)
+ STOP_PROCESSING(SSobj, src)
+ QDEL_NULL(tracker)
+ return ..()
+
+/obj/effect/watcher_orbiter/process(seconds_per_tick)
+ if(!COOLDOWN_FINISHED(src, shot_cooldown))
+ return
+ for(var/mob/living/potential_target in oview(5, src))
+ if(!isanimal(potential_target) || potential_target.stat == DEAD)
+ continue
+ if(!faction_check(target_faction, potential_target.faction))
+ continue
+ shoot_at(potential_target)
+ return
+
+/// Take a shot
+/obj/effect/watcher_orbiter/proc/shoot_at(atom/target)
+ COOLDOWN_START(src, shot_cooldown, fire_delay)
+ var/turf/T = get_turf(src)
+ var/turf/U = get_turf(target)
+ if(!T || !U)
+ return
+ var/obj/item/projectile/O = new projectile_type(T)
+ playsound(get_turf(src), projectile_sound, 75, TRUE)
+ O.firer = parent // no hitting owner.
+ O.current = T
+ O.yo = U.y - T.y
+ O.xo = U.x - T.x
+ O.fire()
+
+/// Set ourselves up to track and orbit around a guy
+/obj/effect/watcher_orbiter/proc/follow(atom/movable/target)
+ parent = target
+ glide_size = target.glide_size
+ animate(src, pixel_y = 26, alpha = 255, time = 0.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(orbit_animation)), 0.5 SECONDS, TIMER_DELETE_ME)
+ tracker = new(target, CALLBACK(src, PROC_REF(on_parent_moved)))
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_deleted))
+ RegisterSignal(target, COMSIG_MOB_DEATH, PROC_REF(on_parent_died))
+ RegisterSignal(target, COMSIG_LIVING_REVIVE, PROC_REF(on_parent_revived))
+
+/// Do our orbiting animation
+/obj/effect/watcher_orbiter/proc/orbit_animation()
+ animate(src, pixel_y = 26, time = 1 SECONDS, loop = -1, easing = SINE_EASING, flags = ANIMATION_PARALLEL)
+ animate(pixel_y = 18, time = 1 SECONDS, easing = SINE_EASING)
+ animate(src, pixel_x = 20, time = 0.5 SECONDS, loop = -1, easing = SINE_EASING | EASE_OUT, flags = ANIMATION_PARALLEL)
+ animate(pixel_x = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+ animate(pixel_x = -20, time = 0.5 SECONDS, easing = SINE_EASING | EASE_OUT)
+ animate(pixel_x = 0, time = 0.5 SECONDS, easing = SINE_EASING | EASE_IN)
+
+/// Follow our parent
+/obj/effect/watcher_orbiter/proc/on_parent_moved(atom/movable/parent, atom/mover, atom/old_loc, direction)
+ if(parent.loc == old_loc)
+ return
+ var/turf/new_turf = get_turf(parent)
+ if(isnull(new_turf))
+ qdel(src)
+ return
+ if(loc != new_turf)
+ abstract_move(new_turf)
+
+
+/// Called if the guy we're tracking is deleted somehow
+/obj/effect/watcher_orbiter/proc/on_parent_deleted()
+ SIGNAL_HANDLER
+ parent = null
+ qdel(src)
+
+/// We must guard this corpse
+/obj/effect/watcher_orbiter/proc/on_parent_died(mob/living/parent)
+ SIGNAL_HANDLER
+ visible_message("[src] emits a piteous keening in mourning of [parent]!")
+ fire_delay /= on_death_multiplier
+
+/// Exit hyperactive mode
+/obj/effect/watcher_orbiter/proc/on_parent_revived(mob/living/parent)
+ SIGNAL_HANDLER
+ visible_message("[src] chirps happily as [parent] suddenly gasps for breath!")
+ fire_delay *= on_death_multiplier
+
+
+/// Beam fired by a baby watcher, doesn't actually do less damage than its parent
+/obj/item/projectile/baby_watcher_blast
+ name = "hatchling beam"
+ icon_state = "ice_2"
diff --git a/code/modules/awaymissions/mob_spawn.dm b/code/modules/awaymissions/mob_spawn.dm
index ed30ed2b5ff..4fe292506fe 100644
--- a/code/modules/awaymissions/mob_spawn.dm
+++ b/code/modules/awaymissions/mob_spawn.dm
@@ -2,7 +2,12 @@
//If someone can do this in a neater way, be my guest-Kor
-//To do: Allow corpses to appear mangled, bloody, etc. Allow customizing the bodies appearance (they're all bald and white right now).
+// To do: Allow customizing the bodies appearance (they're all bald and white right now).
+
+/// this mob spawn creates the corpse instantly
+#define CORPSE_INSTANT 1
+/// this mob spawn creates the corpse during GAME_STATE_PLAYING
+#define CORPSE_ROUNDSTART 2
/obj/effect/mob_spawn
name = "Unknown"
@@ -152,6 +157,8 @@
M.mind.offstation_role = offstation_role
special(M, name)
MM.name = M.real_name
+ else
+ special(M)
if(uses > 0)
uses--
if(!permanent && !uses)
@@ -614,8 +621,49 @@
icon_state = "sleeper"
flavour_text = "Moo!"
+/// these mob spawn subtypes trigger immediately (New or Initialize) and are not player controlled... since they're dead, you know?
+/obj/effect/mob_spawn/corpse
+ /// when this mob spawn should auto trigger.
+ var/spawn_when = CORPSE_INSTANT
+
+ /// what environmental storytelling script should this corpse have
+ var/corpse_description = ""
+ /// optionally different text to display if the target is a clown
+ var/naive_corpse_description = ""
+
+/obj/effect/mob_spawn/corpse/Initialize(mapload, no_spawn)
+ . = ..()
+ if(no_spawn)
+ return
+ switch(spawn_when)
+ if(CORPSE_INSTANT)
+ INVOKE_ASYNC(src, PROC_REF(create))
+ if(CORPSE_ROUNDSTART)
+ if(mapload || (SSticker && SSticker.current_state > GAME_STATE_SETTING_UP))
+ INVOKE_ASYNC(src, PROC_REF(create))
+
+/obj/effect/mob_spawn/corpse/special(mob/living/spawned_mob)
+ . = ..()
+ spawned_mob.death(TRUE)
+ if(corpse_description)
+ spawned_mob.AddComponent(/datum/component/corpse_description, corpse_description, naive_corpse_description)
+
+/obj/effect/mob_spawn/corpse/create(ckey, flavour, name, user)
+ . = ..()
+ qdel(src)
+
+/obj/effect/mob_spawn/corpse/watcher
+ mob_type = /mob/living/simple_animal/hostile/asteroid/basilisk/watcher
+ icon = 'icons/mob/lavaland/watcher.dmi'
+ icon_state = "watcher_dead"
+ pixel_x = -12
+
+/obj/effect/mob_spawn/corpse/goliath
+ mob_type = /mob/living/simple_animal/hostile/asteroid/goliath/beast
+ icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+ icon_state = "goliath_dead"
+ pixel_x = -12
-
-
-
+#undef CORPSE_INSTANT
+#undef CORPSE_ROUNDSTART
diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm
index dcefa302edc..0cf585940ed 100644
--- a/code/modules/mapping/mapping_helpers.dm
+++ b/code/modules/mapping/mapping_helpers.dm
@@ -136,6 +136,22 @@
/obj/effect/mapping_helpers/airlock/windoor
blacklist = list(/obj/machinery/door/firedoor, /obj/machinery/door/poddoor, /obj/machinery/door/unpowered, /obj/machinery/door/airlock)
+/// Apply to a wall (or floor, technically) to ensure it is instantly destroyed by any explosion, even if usually invulnerable
+/obj/effect/mapping_helpers/bombable_wall
+ name = "bombable wall helper"
+ icon_state = "explodable"
+ late = TRUE
+
+/obj/effect/mapping_helpers/bombable_wall/Initialize(mapload)
+ . = ..()
+ if(!mapload)
+ log_debug("[src] spawned outside of mapload!")
+ return
+
+ var/turf/our_turf = get_turf(src) // In case a locker ate us or something
+ our_turf.AddElement(/datum/element/bombable_turf)
+ return INITIALIZE_HINT_QDEL
+
/obj/effect/mapping_helpers/airlock/windoor/autoname
name = "windoor autoname helper"
icon_state = "windoor_autoname"
@@ -151,3 +167,4 @@
/obj/effect/mapping_helpers/airlock/windoor/autoname/desk/payload(obj/machinery/door/window/windoor)
if(windoor.dir == dir)
windoor.name = "[get_area_name(windoor, TRUE)] Desk"
+
diff --git a/config/example/config.toml b/config/example/config.toml
index cdc4ebb5fc0..bdce63a4c3e 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -688,6 +688,7 @@ active_lava_ruins = [
"_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_random_ripley.dmm",
"_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_survivalpod.dmm",
"_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_ufo_crash.dmm",
+ "_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm",
"_maps/map_files/RandomRuins/LavaRuins/lavaland_surface_xeno_nest.dmm",
]
# Budget for lavaland ruins. A higher number means more will spawn
diff --git a/icons/effects/mapping_helpers.dmi b/icons/effects/mapping_helpers.dmi
index 475724f215b..33d4606966a 100644
Binary files a/icons/effects/mapping_helpers.dmi and b/icons/effects/mapping_helpers.dmi differ
diff --git a/icons/mob/lavaland/lavaland_monsters.dmi b/icons/mob/lavaland/lavaland_monsters.dmi
index 6b9e79af434..5c89b0d2f82 100644
Binary files a/icons/mob/lavaland/lavaland_monsters.dmi and b/icons/mob/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index 545769dc774..502ad16f1c5 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/ruin_objects.dmi b/icons/obj/ruin_objects.dmi
index a170911c537..ef5cd930cb5 100644
Binary files a/icons/obj/ruin_objects.dmi and b/icons/obj/ruin_objects.dmi differ
diff --git a/icons/turf/overlays.dmi b/icons/turf/overlays.dmi
index 5048ce5b92d..17387110cff 100644
Binary files a/icons/turf/overlays.dmi and b/icons/turf/overlays.dmi differ
diff --git a/paradise.dme b/paradise.dme
index d4f740e07e7..d4d940bc2e3 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -341,6 +341,7 @@
#include "code\datums\logging.dm"
#include "code\datums\mind.dm"
#include "code\datums\mixed.dm"
+#include "code\datums\movement_detector.dm"
#include "code\datums\mutable_appearance.dm"
#include "code\datums\periodic_news.dm"
#include "code\datums\pipe_datums.dm"
@@ -368,6 +369,7 @@
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\boss_music.dm"
#include "code\datums\components\caltrop.dm"
+#include "code\datums\components\corpse_description.dm"
#include "code\datums\components\deadchat_control.dm"
#include "code\datums\components\decal.dm"
#include "code\datums\components\defibrillator.dm"
@@ -452,6 +454,7 @@
#include "code\datums\diseases\advance\symptoms\weight.dm"
#include "code\datums\diseases\advance\symptoms\youth.dm"
#include "code\datums\elements\_element.dm"
+#include "code\datums\elements\bombable_turf.dm"
#include "code\datums\elements\earhealing.dm"
#include "code\datums\elements\rad_insulation.dm"
#include "code\datums\elements\waddling.dm"
@@ -1502,6 +1505,7 @@
#include "code\modules\awaymissions\mission_code\ruins\processing_facility.dm"
#include "code\modules\awaymissions\mission_code\ruins\snowbiodome.dm"
#include "code\modules\awaymissions\mission_code\ruins\syndiecakesfactory.dm"
+#include "code\modules\awaymissions\mission_code\ruins\watcher_grave.dm"
#include "code\modules\awaymissions\mission_code\ruins\wizardcrash.dm"
#include "code\modules\buildmode\bm_mode.dm"
#include "code\modules\buildmode\buildmode.dm"