diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index 90982a00ec..b85a33ec3c 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -294,3 +294,8 @@
description = "I hate when my shoes come untied!\n"
mood_change = -3
timeout = 1 MINUTES
+
+/datum/mood_event/sacrifice_bad
+ description = "Those darn savages!\n"
+ mood_change = -5
+ timeout = 2 MINUTES
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index b73756c2b1..39b3212c68 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -205,3 +205,8 @@
/datum/mood_event/cleared_stomach
description = "Feels nice to get that out of the way!\n"
mood_change = 3
+
+/datum/mood_event/sacrifice_good
+ description = "The gods are pleased with this offering!\n"
+ mood_change = 5
+ timeout = 3 MINUTES
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index 1c6c444fb4..67ba43dc4f 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -778,6 +778,24 @@ GLOBAL_LIST_EMPTY(possible_items_special)
target_amount = count
update_explanation_text()
*/
+/datum/objective/protect_object
+ name = "protect object"
+ var/obj/protect_target
+
+/datum/objective/protect_object/proc/set_target(obj/O)
+ protect_target = O
+ update_explanation_text()
+
+/datum/objective/protect_object/update_explanation_text()
+ . = ..()
+ if(protect_target)
+ explanation_text = "Protect \the [protect_target] at all costs."
+ else
+ explanation_text = "Free objective."
+
+/datum/objective/protect_object/check_completion()
+ return !QDELETED(protect_target)
+
//Changeling Objectives
/datum/objective/absorb
diff --git a/code/game/objects/structures/ghost_role_spawners.dm b/code/game/objects/structures/ghost_role_spawners.dm
index be92782c74..0077ad82da 100644
--- a/code/game/objects/structures/ghost_role_spawners.dm
+++ b/code/game/objects/structures/ghost_role_spawners.dm
@@ -1,5 +1,5 @@
//Objects that spawn ghosts in as a certain role when they click on it, i.e. away mission bartenders.
-
+#define spawnOverride TRUE
//Preserved terrarium/seed vault: Spawns in seed vault structures in lavaland. Ghosts become plantpeople and are advised to begin growing plants in the room near them.
/obj/effect/mob_spawn/human/seed_vault
name = "preserved terrarium"
@@ -36,6 +36,44 @@
//Ash walker eggs: Spawns in ash walker dens in lavaland. Ghosts become unbreathing lizards that worship the Necropolis and are advised to retrieve corpses to create more ash walkers.
+/obj/structure/ash_walker_eggshell
+ name = "ash walker egg"
+ desc = "A man-sized yellow egg, spawned from some unfathomable creature. A humanoid silhouette lurks within. The egg shell looks resistant to temperature but otherwise rather brittle."
+ icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+ icon_state = "large_egg"
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | FREEZE_PROOF
+ max_integrity = 80
+ var/obj/effect/mob_spawn/human/ash_walker/egg
+
+/obj/structure/ash_walker_eggshell/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0) //lifted from xeno eggs
+ switch(damage_type)
+ if(BRUTE)
+ if(damage_amount)
+ playsound(loc, 'sound/effects/attackblob.ogg', 100, TRUE)
+ else
+ playsound(src, 'sound/weapons/tap.ogg', 50, TRUE)
+ if(BURN)
+ if(damage_amount)
+ playsound(loc, 'sound/items/welder.ogg', 100, TRUE)
+
+/obj/structure/ash_walker_eggshell/attack_ghost(mob/user) //Pass on ghost clicks to the mob spawner
+ if(egg)
+ egg.attack_ghost(user)
+ . = ..()
+
+/obj/structure/ash_walker_eggshell/Destroy()
+ if(!egg)
+ return ..()
+ var/mob/living/carbon/human/yolk = new /mob/living/carbon/human/(get_turf(src))
+ yolk.fully_replace_character_name(null,random_unique_lizard_name(gender))
+ yolk.set_species(/datum/species/lizard/ashwalker)
+ yolk.underwear = "Nude"
+ yolk.equipOutfit(/datum/outfit/ashwalker)//this is an authentic mess we're making
+ yolk.update_body()
+ yolk.gib()
+ QDEL_NULL(egg)
+ return ..()
+
/obj/effect/mob_spawn/human/ash_walker
name = "ash walker egg"
desc = "A man-sized yellow egg, spawned from some unfathomable creature. A humanoid silhouette lurks within."
@@ -55,12 +93,25 @@
You have seen lights in the distance... they foreshadow the arrival of outsiders to your domain. \
Ensure your nest remains protected at all costs."
assignedrole = "Ash Walker"
+ var/datum/team/ashwalkers/team
+ var/obj/structure/ash_walker_eggshell/eggshell
+
+/obj/effect/mob_spawn/human/ash_walker/Destroy()
+ eggshell = null
+ return ..()
+
+/obj/effect/mob_spawn/human/ash_walker/allow_spawn(mob/user, silent = FALSE)
+ if(!(user.key in team.players_spawned) || spawnOverride)//one per person unless you get a bonus spawn
+ return TRUE
+ to_chat(user, span_warning("You have exhausted your usefulness to the Necropolis."))
+ return FALSE
/obj/effect/mob_spawn/human/ash_walker/special(mob/living/new_spawn)
new_spawn.real_name = random_unique_lizard_name(gender)
if(is_mining_level(z))
to_chat(new_spawn, "Drag the corpses of men and beasts to your nest. It will absorb them to create more of your kind. Glory to the Necropolis!")
to_chat(new_spawn, "You can expand the weather proof area provided by your shelters by using the 'New Area' key near the bottom right of your HUD.")
+ to_chat(new_spawn, "Dragging injured ashwalkers to the tentacle or using the sleep verb next to it youself causes the body to remade whole after a short delay!")
else
to_chat(new_spawn, "You have been born outside of your natural home! Whether you decide to return home, or make due with your new home is your own decision.")
@@ -72,10 +123,18 @@
H.undershirt = "Nude"
H.socks = "Nude"
H.update_body()
+ new_spawn.mind.add_antag_datum(/datum/antagonist/ashwalker, team)
+ team.players_spawned += (new_spawn.key)
+ eggshell.egg = null
+ QDEL_NULL(eggshell)
-/obj/effect/mob_spawn/human/ash_walker/Initialize(mapload)
+/obj/effect/mob_spawn/human/ash_walker/Initialize(mapload, datum/team/ashwalkers/ashteam)
. = ..()
var/area/A = get_area(src)
+ team = ashteam
+ eggshell = new /obj/structure/ash_walker_eggshell(get_turf(loc))
+ eggshell.egg = src
+ src.forceMove(eggshell)
if(A)
notify_ghosts("An ash walker egg is ready to hatch in \the [A.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_key = POLL_IGNORE_ASHWALKER, ignore_dnr_observers = TRUE)
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 8dbc2b863a..498951bfd2 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -98,7 +98,7 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/on_body_transfer(mob/living/old_body, mob/living/new_body)
SHOULD_CALL_PARENT(TRUE)
remove_innate_effects(old_body)
- if(!soft_antag && old_body.stat != DEAD && !LAZYLEN(old_body.mind?.antag_datums))
+ if(!soft_antag && old_body && old_body.stat != DEAD && !length(old_body.mind?.antag_datums))
old_body.remove_from_current_living_antags()
apply_innate_effects(new_body)
if(!soft_antag && new_body.stat != DEAD)
diff --git a/code/modules/antagonists/ashwalker/ashwalker.dm b/code/modules/antagonists/ashwalker/ashwalker.dm
new file mode 100644
index 0000000000..0ba84a201f
--- /dev/null
+++ b/code/modules/antagonists/ashwalker/ashwalker.dm
@@ -0,0 +1,40 @@
+/datum/team/ashwalkers
+ name = "Ashwalkers"
+ show_roundend_report = FALSE
+ var/list/players_spawned = new
+
+/datum/antagonist/ashwalker
+ name = "\improper Ash Walker"
+ job_rank = ROLE_LAVALAND
+ show_in_antagpanel = FALSE
+ show_to_ghosts = TRUE
+ antagpanel_category = "Ash Walkers"
+ var/datum/team/ashwalkers/ashie_team
+
+/datum/antagonist/ashwalker/create_team(datum/team/team)
+ if(team)
+ ashie_team = team
+ objectives |= ashie_team.objectives
+ else
+ ashie_team = new
+
+/datum/antagonist/ashwalker/get_team()
+ return ashie_team
+
+/datum/antagonist/ashwalker/on_body_transfer(mob/living/old_body, mob/living/new_body)
+ . = ..()
+ RegisterSignal(new_body, COMSIG_MOB_EXAMINATE, .proc/on_examinate)
+
+/datum/antagonist/ashwalker/on_gain()
+ . = ..()
+ RegisterSignal(owner.current, COMSIG_MOB_EXAMINATE, .proc/on_examinate)
+
+/datum/antagonist/ashwalker/on_removal()
+ . = ..()
+ UnregisterSignal(owner.current, COMSIG_MOB_EXAMINATE)
+
+/datum/antagonist/ashwalker/proc/on_examinate(datum/source, atom/A)
+ SIGNAL_HANDLER
+
+ if(istype(A, /obj/structure/headpike))
+ SEND_SIGNAL(owner.current, COMSIG_ADD_MOOD_EVENT, "headspear", /datum/mood_event/sacrifice_good)
diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm
index cd5c2f76f3..668a8dcb92 100644
--- a/code/modules/awaymissions/corpse.dm
+++ b/code/modules/awaymissions/corpse.dm
@@ -33,6 +33,10 @@
var/ghost_usable = TRUE
var/skip_reentry_check = FALSE //Skips the ghost role blacklist time for people who ghost/suicide/cryo
+///override this to add special spawn conditions to a ghost role
+/obj/effect/mob_spawn/proc/allow_spawn(mob/user, silent = FALSE)
+ return TRUE
+
//ATTACK GHOST IGNORING PARENT RETURN VALUE
/obj/effect/mob_spawn/attack_ghost(mob/user, latejoinercalling)
if(!SSticker.HasRoundStarted() || !loc || !ghost_usable)
@@ -43,6 +47,8 @@
if(jobban_isbanned(user, banType))
to_chat(user, "You are jobanned!")
return
+ if(!allow_spawn(user, silent = FALSE))
+ return
if(QDELETED(src) || QDELETED(user))
return
if(isobserver(user))
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index dd7c946d3c..2a978bedb4 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -308,10 +308,15 @@
adjustHealth(unsuitable_atmos_damage)
/mob/living/simple_animal/gib(no_brain, no_organs, no_bodyparts, datum/explosion/was_explosion)
- if(butcher_results)
+ if(butcher_results || guaranteed_butcher_results)
+ var/list/butcher = list()
+ if(butcher_results)
+ butcher += butcher_results
+ if(guaranteed_butcher_results)
+ butcher += guaranteed_butcher_results
var/atom/Tsec = drop_location()
- for(var/path in butcher_results)
- for(var/i in 1 to butcher_results[path])
+ for(var/path in butcher)
+ for(var/i in 1 to butcher[path])
new path(Tsec)
..()
diff --git a/code/modules/ruins/objects_and_mobs/ash_walker_den.dm b/code/modules/ruins/objects_and_mobs/ash_walker_den.dm
index 2401fc63a0..d6693fcca2 100644
--- a/code/modules/ruins/objects_and_mobs/ash_walker_den.dm
+++ b/code/modules/ruins/objects_and_mobs/ash_walker_den.dm
@@ -12,14 +12,29 @@
max_integrity = 200
var/faction = list("ashwalker")
var/meat_counter = 6
+ var/datum/team/ashwalkers/ashies
+ var/datum/linked_objective
-/obj/structure/lavaland/ash_walker/Initialize()
+/obj/structure/lavaland/ash_walker/Initialize(mapload)
.=..()
+ ashies = new /datum/team/ashwalkers()
+ var/datum/objective/protect_object/objective = new
+ objective.set_target(src)
+ linked_objective = objective
+ ashies.objectives += objective
START_PROCESSING(SSprocessing, src)
+/obj/structure/lavaland/ash_walker/Destroy()
+ ashies.objectives -= linked_objective
+ ashies = null
+ QDEL_NULL(linked_objective)
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
/obj/structure/lavaland/ash_walker/deconstruct(disassembled)
new /obj/item/assembly/signaler/anomaly (get_step(loc, pick(GLOB.alldirs)))
new /obj/effect/collapse(loc)
+ return ..()
/obj/structure/lavaland/ash_walker/process()
consume()
@@ -28,20 +43,64 @@
/obj/structure/lavaland/ash_walker/proc/consume()
for(var/mob/living/H in view(src, 1)) //Only for corpse right next to/on same tile
if(H.stat)
- visible_message("Serrated tendrils eagerly pull [H] to [src], tearing the body apart as its blood seeps over the eggs.")
- playsound(get_turf(src),'sound/magic/demon_consume.ogg', 100, 1)
for(var/obj/item/W in H)
if(!H.dropItemToGround(W))
qdel(W)
+ if(issilicon(H)) //no advantage to sacrificing borgs...
+ H.gib()
+ visible_message(span_notice("Serrated tendrils eagerly pull [H] apart, but find nothing of interest."))
+ return
+
+ if(H.mind?.has_antag_datum(/datum/antagonist/ashwalker) && (H.key || H.get_ghost(FALSE, TRUE))) //special interactions for dead lava lizards with ghosts attached
+ visible_message(span_warning("Serrated tendrils carefully pull [H] to [src], absorbing the body and creating it anew."))
+ var/datum/mind/deadmind
+ if(H.key)
+ deadmind = H
+ else
+ deadmind = H.get_ghost(FALSE, TRUE)
+ to_chat(deadmind, "Your body has been returned to the nest. You are being remade anew, and will awaken shortly. Your memories will remain intact in your new body, as your soul is being salvaged")
+ SEND_SOUND(deadmind, sound('sound/magic/enter_blood.ogg',volume=100))
+ addtimer(CALLBACK(src, .proc/remake_walker, H.mind, H.real_name), 20 SECONDS)
+ new /obj/effect/gibspawner/generic(get_turf(H))
+ qdel(H)
+ return
+
if(ismegafauna(H))
meat_counter += 20
else
meat_counter++
+ visible_message(span_warning("Serrated tendrils eagerly pull [H] to [src], tearing the body apart as its blood seeps over the eggs."))
+ playsound(get_turf(src),'sound/magic/demon_consume.ogg', 100, TRUE)
+ var/deliverykey = H.fingerprintslast //key of whoever brought the body
+ var/mob/living/deliverymob = get_mob_by_key(deliverykey) //mob of said key
+ //there is a 40% chance that the Lava Lizard unlocks their respawn with each sacrifice
+ if(deliverymob && (deliverymob.mind?.has_antag_datum(/datum/antagonist/ashwalker)) && (deliverykey in ashies.players_spawned) && (prob(40)))
+ to_chat(deliverymob, span_warning("The Necropolis is pleased with your sacrifice. You feel confident your existence after death is secure."))
+ ashies.players_spawned -= deliverykey
H.gib()
obj_integrity = min(obj_integrity + max_integrity*0.05,max_integrity)//restores 5% hp of tendril
+ for(var/mob/living/L in view(src, 5))
+ if(L.mind?.has_antag_datum(/datum/antagonist/ashwalker))
+ SEND_SIGNAL(L, COMSIG_ADD_MOOD_EVENT, "headspear", /datum/mood_event/sacrifice_good)
+ else
+ SEND_SIGNAL(L, COMSIG_ADD_MOOD_EVENT, "headspear", /datum/mood_event/sacrifice_bad)
+
+/obj/structure/lavaland/ash_walker/proc/remake_walker(datum/mind/oldmind, oldname)
+ var/mob/living/carbon/human/M = new /mob/living/carbon/human(get_step(loc, pick(GLOB.alldirs)))
+ M.set_species(/datum/species/lizard/ashwalker)
+ M.real_name = oldname
+ M.underwear = "Nude"
+ M.undershirt = "Nude"
+ M.socks = "Nude"
+ M.update_body()
+ M.remove_language(/datum/language/common)
+ oldmind.transfer_to(M)
+ M.mind.grab_ghost()
+ to_chat(M, "You have been pulled back from beyond the grave, with a new body and renewed purpose. Glory to the Necropolis!")
+ playsound(get_turf(M),'sound/magic/exit_blood.ogg', 100, TRUE)
/obj/structure/lavaland/ash_walker/proc/spawn_mob()
if(meat_counter >= ASH_WALKER_SPAWN_THRESHOLD)
- new /obj/effect/mob_spawn/human/ash_walker(get_step(loc, pick(GLOB.alldirs)))
+ new /obj/effect/mob_spawn/human/ash_walker(get_step(loc, pick(GLOB.alldirs)), ashies)
visible_message("One of the eggs swells to an unnatural size and tumbles free. It's ready to hatch!")
meat_counter -= ASH_WALKER_SPAWN_THRESHOLD
diff --git a/tgstation.dme b/tgstation.dme
index 1bfe4f7561..0a9279ef09 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1560,6 +1560,7 @@
#include "code\modules\antagonists\abductor\machinery\dispenser.dm"
#include "code\modules\antagonists\abductor\machinery\experiment.dm"
#include "code\modules\antagonists\abductor\machinery\pad.dm"
+#include "code\modules\antagonists\ashwalker\ashwalker.dm"
#include "code\modules\antagonists\blob\blob.dm"
#include "code\modules\antagonists\blob\blob\blob_report.dm"
#include "code\modules\antagonists\blob\blob\overmind.dm"