diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm new file mode 100644 index 00000000000..99af5bdced4 --- /dev/null +++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm @@ -0,0 +1,345 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/closed/mineral/volcanic/lava_land_surface, +/area/lavaland/surface/outdoors) +"b" = ( +/obj/structure/stone_tile/surrounding_tile, +/obj/item/food/egg/watcher, +/turf/open/misc/asteroid/basalt/wasteland/basin, +/area/lavaland/surface/outdoors) +"c" = ( +/obj/structure/flora/ash/tall_shroom, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"f" = ( +/obj/structure/stone_tile/cracked{ + pixel_x = 16 + }, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"g" = ( +/obj/structure/stone_tile/block/cracked, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"h" = ( +/obj/structure/stone_tile/cracked, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"i" = ( +/obj/structure/stone_tile/cracked{ + pixel_y = -16 + }, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"j" = ( +/obj/effect/mapping_helpers/bombable_wall, +/turf/closed/indestructible/riveted/boss/wasteland, +/area/lavaland/surface/outdoors) +"l" = ( +/turf/closed/mineral/gibtonite/volcanic, +/area/lavaland/surface/outdoors) +"m" = ( +/turf/open/chasm/lavaland, +/area/lavaland/surface/outdoors) +"o" = ( +/obj/effect/mob_spawn/corpse/goliath/pierced, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"q" = ( +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"r" = ( +/obj/structure/stone_tile/slab/cracked, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"t" = ( +/obj/structure/stone_tile/cracked{ + pixel_x = 16 + }, +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"u" = ( +/obj/structure/flora/ash/stem_shroom, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"v" = ( +/obj/effect/mob_spawn/corpse/watcher/crushed, +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"y" = ( +/obj/structure/stone_tile/block/cracked{ + pixel_y = -16 + }, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"A" = ( +/obj/structure/flora/rock/style_random, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"B" = ( +/obj/structure/stone_tile/slab, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"C" = ( +/obj/structure/flora/ash/cacti, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"D" = ( +/obj/structure/stone_tile/surrounding/cracked, +/obj/structure/stone_tile/center/cracked, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"E" = ( +/obj/structure/stone_tile/cracked, +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"I" = ( +/turf/template_noop, +/area/template_noop) +"J" = ( +/obj/structure/stone_tile/slab/cracked{ + dir = 4 + }, +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"M" = ( +/obj/structure/flora/ash/fireblossom, +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) +"R" = ( +/turf/closed/indestructible/riveted/boss/wasteland, +/area/lavaland/surface/outdoors) +"U" = ( +/obj/structure/stone_tile/slab/cracked, +/obj/effect/mob_spawn/corpse/watcher/goliath_chewed, +/turf/open/misc/asteroid/basalt/wasteland, +/area/lavaland/surface/outdoors) +"W" = ( +/turf/open/misc/asteroid/basalt/lava_land_surface, +/area/lavaland/surface/outdoors) + +(1,1,1) = {" +I +I +I +I +I +I +I +I +I +I +I +I +I +"} +(2,1,1) = {" +I +I +I +I +a +I +W +W +M +I +I +I +I +"} +(3,1,1) = {" +I +I +I +l +a +a +a +t +a +a +I +I +I +"} +(4,1,1) = {" +I +I +a +a +R +R +R +j +R +a +a +I +I +"} +(5,1,1) = {" +I +a +a +R +R +u +R +q +R +m +a +I +I +"} +(6,1,1) = {" +I +a +a +R +A +f +q +f +m +m +m +I +I +"} +(7,1,1) = {" +I +I +a +R +D +U +h +q +C +m +m +m +I +"} +(8,1,1) = {" +I +l +a +R +y +b +g +B +q +q +m +m +I +"} +(9,1,1) = {" +I +a +a +R +c +i +o +r +q +R +a +a +I +"} +(10,1,1) = {" +I +I +a +R +R +A +u +h +c +R +a +l +I +"} +(11,1,1) = {" +I +I +a +a +R +R +R +q +R +R +a +I +I +"} +(12,1,1) = {" +I +I +I +a +a +a +R +j +R +a +a +I +I +"} +(13,1,1) = {" +I +I +I +I +I +W +M +J +E +W +I +I +I +"} +(14,1,1) = {" +I +I +I +I +I +I +t +v +W +I +I +I +I +"} +(15,1,1) = {" +I +I +I +I +I +I +I +W +W +I +I +I +I +"} diff --git a/code/datums/components/temporary_description.dm b/code/datums/components/temporary_description.dm new file mode 100644 index 00000000000..1ff5e6dccdb --- /dev/null +++ b/code/datums/components/temporary_description.dm @@ -0,0 +1,44 @@ +/** + * Adds examine text to something which is removed when receiving specified signals, by default the revive signal. + * The default settings are set up to be applied to a corpse to add some kind of immersive storytelling text which goes away upon revival. + */ +/datum/component/temporary_description + /// What do we display on examine? + var/description_text = "" + /// What do we display if examined by a clown? Usually only applied if this is put on a corpse, but go nuts. + var/naive_description = "" + /// When are we removed? + var/list/removal_signals + +/datum/component/temporary_description/Initialize( + description_text = "There's something unusual about them.", + naive_description = "", + list/removal_signals = list(COMSIG_LIVING_REVIVE), +) + . = ..() + if (!isatom(parent)) + return COMPONENT_INCOMPATIBLE + if (!description_text) + stack_trace("[type] applied to [parent] with empty description, which is pointless.") + src.description_text = description_text + src.naive_description = naive_description + if (length(removal_signals)) + src.removal_signals = removal_signals + +/datum/component/temporary_description/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined)) + RegisterSignals(parent, removal_signals, PROC_REF(remove_component)) + +/datum/component/temporary_description/UnregisterFromParent() + UnregisterSignal(parent, removal_signals + COMSIG_ATOM_EXAMINE) + +/datum/component/temporary_description/proc/on_examined(atom/corpse, mob/thing_inspector, list/examine_list) + SIGNAL_HANDLER + if (naive_description && HAS_MIND_TRAIT(thing_inspector, TRAIT_NAIVE)) + examine_list += span_notice(naive_description) + return + examine_list += span_notice(description_text) + +/datum/component/temporary_description/proc/remove_component() + SIGNAL_HANDLER + qdel(src) // It wouldn't be immersive if the circumstances of my grisly death remained after I was revived diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm index 7d9af70fb57..1dcf535c329 100644 --- a/code/datums/ruins/lavaland.dm +++ b/code/datums/ruins/lavaland.dm @@ -284,6 +284,14 @@ allow_duplicates = TRUE cost = 3 +/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 + /datum/map_template/ruin/lavaland/mook_village name = "Mook Village" id = "mook_village" diff --git a/code/game/objects/items/food/_food.dm b/code/game/objects/items/food/_food.dm index 69cfdde4668..0fb85e67589 100644 --- a/code/game/objects/items/food/_food.dm +++ b/code/game/objects/items/food/_food.dm @@ -115,7 +115,7 @@ ///Set decomp_req_handle to TRUE to only make it decompose when someone picks it up. ///Requires /datum/component/germ_sensitive to detect exposure /obj/item/food/proc/make_germ_sensitive(mapload) - if(istype(src, /obj/item/food/bowled) || istype(src, /obj/item/food/canned) || !isnull(trash_type)) + if(!isnull(trash_type)) return // You don't eat the package and it protects from decomposing AddComponent(/datum/component/germ_sensitive, mapload) if(!preserved_food) diff --git a/code/game/objects/items/food/egg.dm b/code/game/objects/items/food/egg.dm index 555b16e24b6..60bbb58e3ac 100644 --- a/code/game/objects/items/food/egg.dm +++ b/code/game/objects/items/food/egg.dm @@ -28,6 +28,7 @@ GLOBAL_VAR_INIT(chicks_from_eggs, 0) ant_attracting = FALSE decomp_type = /obj/item/food/egg/rotten decomp_req_handle = TRUE //so laid eggs can actually become chickens + /// How likely is it that a chicken will come out of here if we throw it? var/chick_throw_prob = 13 /obj/item/food/egg/make_bakeable() @@ -68,24 +69,25 @@ GLOBAL_VAR_INIT(chicks_from_eggs, 0) var/turf/hit_turf = get_turf(hit_atom) new /obj/effect/decal/cleanable/food/egg_smudge(hit_turf) - //Chicken code uses this MAX_CHICKENS variable, so I figured that I'd use it again here. Even this check and the check in chicken code both use the MAX_CHICKENS variable, they use independent counter variables and thus are independent of each other. - if(GLOB.chicks_from_eggs < MAX_CHICKENS) //Roughly a 1/8 (12.5%) chance to make a chick, as in Minecraft, with a 1/256 (~0.39%) chance to make four chicks instead. - var/chance = rand(0, 255) - switch(chance) - if(0 to 30) - new /mob/living/basic/chick(hit_turf) - GLOB.chicks_from_eggs++ - visible_message(span_notice("A chick comes out of the cracked egg!")) - if(31) - var/spawned_chickens = min(4, MAX_CHICKENS - GLOB.chicks_from_eggs) // We don't want to go over the limit - visible_message(span_notice("[spawned_chickens] chicks come out of the egg! Jackpot!")) - for(var/i in 1 to spawned_chickens) - new /mob/living/basic/chick(hit_turf) - GLOB.chicks_from_eggs++ - + if (prob(chick_throw_prob)) + spawn_impact_chick(hit_turf) reagents.expose(hit_atom, TOUCH) qdel(src) +/// Spawn a baby chicken from throwing an egg +/obj/item/food/egg/proc/spawn_impact_chick(turf/spawn_turf) + var/chickens_remaining = MAX_CHICKENS - GLOB.chicks_from_eggs + if (chickens_remaining < 1) + return + var/spawned_chickens = prob(97) ? 1 : min(4, chickens_remaining) // We don't want to go over the limit + if (spawned_chickens > 1) // Chicken jackpot! + visible_message(span_notice("[spawned_chickens] chicks come out of the egg! Jackpot!")) + else + visible_message(span_notice("A chick comes out of the cracked egg!")) + for(var/i in 1 to spawned_chickens) + new /mob/living/basic/chick(spawn_turf) + GLOB.chicks_from_eggs++ + /obj/item/food/egg/attackby(obj/item/item, mob/user, params) if(istype(item, /obj/item/toy/crayon)) var/obj/item/toy/crayon/crayon = item diff --git a/code/game/objects/items/food/packaged.dm b/code/game/objects/items/food/packaged.dm index 9c23ac3f307..1dabec57cdd 100644 --- a/code/game/objects/items/food/packaged.dm +++ b/code/game/objects/items/food/packaged.dm @@ -16,6 +16,9 @@ w_class = WEIGHT_CLASS_SMALL preserved_food = TRUE +/obj/item/food/canned/make_germ_sensitive(mapload) + return // It's in a can + /obj/item/food/canned/proc/open_can(mob/user) to_chat(user, span_notice("You pull back the tab of \the [src].")) playsound(user.loc, 'sound/items/foodcanopen.ogg', 50) diff --git a/code/game/objects/items/food/soup.dm b/code/game/objects/items/food/soup.dm index 08eb86ebaa0..e1482b17a81 100644 --- a/code/game/objects/items/food/soup.dm +++ b/code/game/objects/items/food/soup.dm @@ -7,6 +7,9 @@ eatverbs = list("slurp", "sip", "inhale", "drink") venue_value = FOOD_PRICE_CHEAP +/obj/item/food/bowled/make_germ_sensitive(mapload) + return // It's in a bowl + /obj/item/food/bowled/wish name = "wish soup" desc = "I wish this was soup." diff --git a/code/game/turfs/closed/indestructible.dm b/code/game/turfs/closed/indestructible.dm index 44717faaf69..a1320946a45 100644 --- a/code/game/turfs/closed/indestructible.dm +++ b/code/game/turfs/closed/indestructible.dm @@ -308,6 +308,9 @@ INITIALIZE_IMMEDIATE(/turf/closed/indestructible/splashscreen) explosive_resistance = 50 baseturfs = /turf/closed/indestructible/riveted/boss +/turf/closed/indestructible/riveted/boss/wasteland + baseturfs = /turf/open/misc/asteroid/basalt/wasteland + /turf/closed/indestructible/riveted/boss/see_through opacity = FALSE diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm index 5e459758ee4..aa201292f72 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm @@ -60,6 +60,12 @@ if(prob(floor_variance)) icon_state = "[base_icon_state][rand(0,6)]" +/turf/open/misc/asteroid/basalt/wasteland/basin + icon_state = "wasteland_dug" + base_icon_state = "wasteland_dug" + floor_variance = 0 + dug = TRUE + /turf/closed/mineral/strong/wasteland name = "ancient dry rock" color = "#B5651D" diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm new file mode 100644 index 00000000000..75ac48c2467 --- /dev/null +++ b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm @@ -0,0 +1,263 @@ +/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." + + +#define WATCHER_EGG_LIVELY_MOD 0.75 +#define WATCHER_EGG_ACTIVE_MOD 0.5 + +/// Egg which hatches into a helpful pet. Or you can eat it if you want. +/obj/item/food/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" + chick_throw_prob = 100 + tastes = list("ocular fluid" = 6, "loneliness" = 1) + preserved_food = TRUE + /// 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/food/egg/watcher/Initialize(mapload) + . = ..() + pedometer = new(src, CALLBACK(src, PROC_REF(on_stepped))) + +/obj/item/food/egg/watcher/Destroy(force) + . = ..() + QDEL_NULL(pedometer) + +/obj/item/food/egg/watcher/spawn_impact_chick(turf/spawn_turf) + new /obj/effect/spawner/random/lavaland_mob/watcher(spawn_turf) + +/obj/item/food/egg/watcher/examine(mob/user) + return ..() + span_notice("Watch it more closely to see how it is doing...") + +/obj/item/food/egg/watcher/examine_more(mob/user) + . = ..() + if (steps_travelled < (steps_to_hatch * WATCHER_EGG_ACTIVE_MOD)) + return . + span_notice("Something stirs listlessly inside.") + if (steps_travelled < steps_to_hatch * WATCHER_EGG_LIVELY_MOD) + return . + span_notice("Something is moving actively inside.") + return . + span_boldnotice("It's jiggling wildly, it's about to hatch!") + +/// Called when we are moved, whether inside an inventory or by ourself somehow +/obj/item/food/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) + jiggle() + return + if (steps_travelled < steps_to_hatch) + return + visible_message(span_boldnotice("[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/food/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(pixelshiftx = 2, pixelshifty = 0, shake_interval = 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/simple/lavaland/lavaland_monsters.dmi' + icon_state = "watcher_baby" + w_class = WEIGHT_CLASS_SMALL + /// 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_QDELETING, PROC_REF(remove_owner)) + RegisterSignal(orbiter, COMSIG_QDELETING, PROC_REF(remove_orbiter)) + +/obj/item/watcher_hatchling/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + if (isnull(orbiter)) + return + var/mob/holder = recursive_loc_check(src, /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_QDELETING) + owner = null + +/// In the more likely event that our orbiter is deleted, stop holding a reference to it +/obj/item/watcher_hatchling/proc/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/simple/lavaland/lavaland_monsters.dmi' + icon_state = "watcher_baby" + layer = EDGED_TURF_LAYER // Don't render under lightbulbs + plane = GAME_PLANE_UPPER + 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/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(FACTION_MINING) + +/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 (!ismining(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) + fire_projectile(projectile_type, target, projectile_sound, ignore_targets = list(parent)) + +/// 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_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_glide_size_changed)) + RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_parent_deleted)) + RegisterSignal(target, COMSIG_LIVING_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) + +/// Make sure we glide at the same speed as our parent +/obj/effect/watcher_orbiter/proc/on_glide_size_changed(atom/source, new_glide_size) + SIGNAL_HANDLER + glide_size = new_glide_size + +/// 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(span_notice("[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(span_notice("[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/projectile/baby_watcher_blast + name = "hatchling beam" + icon_state = "ice_2" + damage = 10 + damage_type = BRUTE // Mining mobs don't take a lot of burn damage so we'll pretend + speed = 1 + pixel_speed_multiplier = 0.5 + +/obj/projectile/baby_watcher_blast/Initialize(mapload) + . = ..() + transform = transform.Scale(0.5) diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm index 473a87fef5b..4802ef347a0 100644 --- a/code/modules/mob/living/basic/basic.dm +++ b/code/modules/mob/living/basic/basic.dm @@ -201,7 +201,7 @@ . = ..() if(stat != DEAD) return - . += span_deadsay("Upon closer examination, [p_they()] appear[p_s()] to be [HAS_TRAIT(user.mind, TRAIT_NAIVE) ? "asleep" : "dead"].") + . += span_deadsay("Upon closer examination, [p_they()] appear[p_s()] to be [HAS_MIND_TRAIT(user, TRAIT_NAIVE) ? "asleep" : "dead"].") /mob/living/basic/proc/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) face_atom(target) diff --git a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm index 060f7e178be..ce02c6894ae 100644 --- a/code/modules/mob_spawn/corpses/nonhuman_corpses.dm +++ b/code/modules/mob_spawn/corpses/nonhuman_corpses.dm @@ -46,6 +46,13 @@ pixel_x = -12 base_pixel_x = -12 +/obj/effect/mob_spawn/corpse/watcher + mob_type = /mob/living/basic/mining/watcher + icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi' + icon_state = "watcher_dead_helper" + pixel_x = -12 + base_pixel_x = -12 + /// Dead headcrab for changeling-themed ruins /obj/effect/mob_spawn/corpse/headcrab mob_type = /mob/living/basic/headslug/beakless diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm index b282ea7d8b2..3bfff7855f0 100644 --- a/code/modules/mob_spawn/mob_spawn.dm +++ b/code/modules/mob_spawn/mob_spawn.dm @@ -281,6 +281,11 @@ ///burn damage this corpse will spawn with var/burn_damage = 0 + ///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) @@ -298,6 +303,8 @@ spawned_mob.adjustOxyLoss(oxy_damage) spawned_mob.adjustBruteLoss(brute_damage) spawned_mob.adjustFireLoss(burn_damage) + if (corpse_description) + spawned_mob.AddComponent(/datum/component/temporary_description, corpse_description, naive_corpse_description) /obj/effect/mob_spawn/corpse/create(mob/mob_possessor, newname) . = ..() diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 6bce420a02b..37d36b13f6e 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -1143,13 +1143,17 @@ #undef MUZZLE_EFFECT_PIXEL_INCREMENT /// Fire a projectile from this atom at another atom -/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer) +/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer, list/ignore_targets = list()) if (!isnull(sound)) playsound(src, sound, vol = 100, vary = TRUE) var/turf/startloc = get_turf(src) var/obj/projectile/bullet = new projectile_type(startloc) bullet.starting = startloc + var/list/ignore = list() + for (var/atom/thing as anything in ignore_targets) + ignore[thing] = TRUE + bullet.impacted += ignore bullet.firer = firer || src bullet.fired_from = src bullet.yo = target.y - startloc.y @@ -1157,3 +1161,4 @@ bullet.original = target bullet.preparePixelProjectile(target, src) bullet.fire() + return bullet diff --git a/config/lavaruinblacklist.txt b/config/lavaruinblacklist.txt index e6279517dbf..d40c4386d31 100644 --- a/config/lavaruinblacklist.txt +++ b/config/lavaruinblacklist.txt @@ -37,6 +37,7 @@ #_maps/RandomRuins/LavaRuins/lavaland_surface_survivalpod.dmm #_maps/RandomRuins/LavaRuins/lavaland_surface_tomb.dmm #_maps/RandomRuins/LavaRuins/lavaland_surface_ufo_crash.dmm +#_maps/RandomRuins/LavaRuins/lavaland_surface_watcher_grave.dmm #_maps/RandomRuins/LavaRuins/lavaland_surface_ww_vault.dmm #_maps/RandomRuins/LavaRuins/lavaland_surface_wwiioutpost.dmm #_maps/RandomRuins/LavaRuins/lavaland_surface_xeno_nest.dmm diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi index 38b78cf468f..f68e3db4a6c 100644 Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ diff --git a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi index 2be68ef4c66..808fdc59d9b 100644 Binary files a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi and b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi differ diff --git a/icons/obj/food/egg.dmi b/icons/obj/food/egg.dmi index c7661fca918..58908d82479 100644 Binary files a/icons/obj/food/egg.dmi and b/icons/obj/food/egg.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 24074b77eb8..eddfafa0683 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1115,6 +1115,7 @@ #include "code\datums\components\technoshy.dm" #include "code\datums\components\telegraph_ability.dm" #include "code\datums\components\temporary_body.dm" +#include "code\datums\components\temporary_description.dm" #include "code\datums\components\tether.dm" #include "code\datums\components\thermite.dm" #include "code\datums\components\tippable.dm" @@ -4208,6 +4209,7 @@ #include "code\modules\mapfluff\ruins\lavalandruin_code\sloth.dm" #include "code\modules\mapfluff\ruins\lavalandruin_code\surface.dm" #include "code\modules\mapfluff\ruins\lavalandruin_code\syndicate_base.dm" +#include "code\modules\mapfluff\ruins\lavalandruin_code\watcher_grave.dm" #include "code\modules\mapfluff\ruins\objects_and_mobs\ash_walker_den.dm" #include "code\modules\mapfluff\ruins\objects_and_mobs\cursed_slot_machine.dm" #include "code\modules\mapfluff\ruins\objects_and_mobs\necropolis_gate.dm"