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"