diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 4e4436db6a..75173786c3 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -14,6 +14,7 @@
#define ROLE_DELF "Dark Elf"
#define ROLE_PREBEL "Peasant Rebel"
#define ROLE_ZIZOIDCULTIST "Zizoid Cultist"
+#define ROLE_LICH "Lich"
#define ROLE_SYNDICATE "Syndicate"
#define ROLE_TRAITOR "Traitor"
@@ -45,6 +46,8 @@
#define ROLE_LAVALAND "Lavaland"
#define ROLE_INTERNAL_AFFAIRS "Internal Affairs Agent"
+#define ROLE_NECRO_SKELETON "Necromancer Skeleton"
+
//Missing assignment means it's not a gamemode specific role, IT'S NOT A BUG OR ERROR.
//The gamemode specific ones are just so the gamemodes can query whether a player is old enough
//(in game days played) to play that role
@@ -69,7 +72,8 @@ GLOBAL_LIST_INIT(special_roles_rogue, list(
ROLE_BANDIT = /datum/antagonist/bandit,
ROLE_ZIZOIDCULTIST = /datum/antagonist/zizocultist,
// ROLE_WEREWOLF = /datum/antagonist/werewolf,
- ROLE_NBEAST = /datum/antagonist/vampirelord
+ ROLE_NBEAST = /datum/antagonist/vampirelord,
+ ROLE_LICH = /datum/antagonist/lich
))
//Job defines for what happens when you fail to qualify for any job during job selection
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index fa78094a07..24ec283232 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -26,6 +26,7 @@
#define TRAIT_RETARD_ANATOMY "Inhumen Anatomy" //can't wear hats and shoes
#define TRAIT_NASTY_EATER "Inhumen Digestion" //can eat rotten food, organs, poison berries, and drink murky water
#define TRAIT_NOFALLDAMAGE1 "Minor fall damage immunity"
+#define TRAIT_DEATHSIGHT "Veiled Whispers" // Is notified when a player character dies, but not told exactly where or how.
#define TRAIT_ROT_EATER "Blessing of Pestra" //can eat rotten food
#define TRAIT_ORGAN_EATER "Blessing of Graggar"
#define TRAIT_SOUL_EXAMINE "Blessing of Necra" //can check bodies to see if they have departed
@@ -63,6 +64,7 @@
#define TRAIT_IWASREVIVED "iwasrevived" //prevents PQ gain from reviving the same person twice
#define TRAIT_IWASUNZOMBIFIED "iwasunzombified" //prevents PQ gain from curing a zombie twice
#define TRAIT_ZIZOID_HUNTED "zizoidhunted" // Used to signal character has been marked by death by the Zizoid cult
+#define TRAIT_CABAL "Of the Cabal" //Zizo cultists recognize each other too
// JOB RELATED TRAITS
@@ -114,7 +116,9 @@ GLOBAL_LIST_INIT(roguetraits, list(
TRAIT_INTRAINING = "I'm going to be a knight someday! I can use training dummies more effectively than others.",
TRAIT_MALUMFIRE = "My hands are blessed by Malum to forge items of superb quality.",
TRAIT_MOB_FIRE_IMMUNE = span_info("I am not easily burned by flames."),
- TRAIT_KAIZOKU = "Whether by birth or by learning, I've inherited the Islander ways instead of Imperial culture.",
+ TRAIT_DEATHSIGHT = span_info("I can feel when someone nearby draws the Undermaiden's attention."),
+ TRAIT_CABAL = span_info("In secret, I have studied the ways of Zizo's ascension, and know of others of the Cabal."),
+ TRAIT_KAIZOKU = "Whether by birth or by learning, I've inherited the Islander ways instead of Imperial culture."
))
// trait accessor defines
diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm
index a1039db614..c32b1432a7 100644
--- a/code/_globalvars/lists/poll_ignore.dm
+++ b/code/_globalvars/lists/poll_ignore.dm
@@ -20,6 +20,7 @@
#define POLL_IGNORE_SPLITPERSONALITY "split_personality"
#define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support"
#define POLL_IGNORE_ACADEMY_WIZARD "academy_wizard"
+#define POLL_IGNORE_NECROMANCER_SKELETON "necromancer_skeleton"
GLOBAL_LIST_INIT(poll_ignore_desc, list(
@@ -42,7 +43,8 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list(
POLL_IGNORE_IMAGINARYFRIEND = "Imaginary Friend",
POLL_IGNORE_SPLITPERSONALITY = "Split Personality",
POLL_IGNORE_CONTRACTOR_SUPPORT = "Contractor Support Unit",
- POLL_IGNORE_ACADEMY_WIZARD = "Academy Wizard Defender"
+ POLL_IGNORE_ACADEMY_WIZARD = "Academy Wizard Defender",
+ POLL_IGNORE_NECROMANCER_SKELETON = "Necromancer Skeleton"
))
GLOBAL_LIST_INIT(poll_ignore, init_poll_ignore())
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 9dcdba90a7..3551b7c5c9 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -52,6 +52,7 @@
var/setup_error //What stopepd setting up the mode.
var/list/datum/mind/villains = list() //Murders Runtimes via shoving this into parent
+ var/list/datum/mind/liches = list()
var/list/datum/mind/vampires = list()
var/list/datum/mind/deathknights = list() // Ditto as villains mind list.
var/list/datum/mind/werewolves = list()
@@ -59,6 +60,7 @@
var/list/datum/mind/cultists = list()
var/list/datum/mind/pre_villains = list()
+ var/list/datum/mind/pre_liches = list()
var/list/datum/mind/pre_werewolves = list()
var/list/datum/mind/pre_vampires = list()
var/list/datum/mind/pre_bandits = list()
diff --git a/code/game/gamemodes/roguetown/roguetown.dm b/code/game/gamemodes/roguetown/roguetown.dm
index 6fadd92495..245b2cd740 100644
--- a/code/game/gamemodes/roguetown/roguetown.dm
+++ b/code/game/gamemodes/roguetown/roguetown.dm
@@ -1,5 +1,5 @@
// This mode will become the main basis for the typical roguetown round. Based off of chaos mode.
-GLOBAL_LIST_INIT(roguegamemodes, list("Rebellion", "Vampire Lord", "Extended", "Bandits", "CANCEL")) // This is mainly used for forcemgamemodes
+GLOBAL_LIST_INIT(roguegamemodes, list("Rebellion", "Vampire Lord", "Extended", "Bandits", "Lich", "CANCEL")) // This is mainly used for forcemgamemodes
/datum/game_mode/chaosmode
name = "roguemode"
@@ -169,6 +169,26 @@ GLOBAL_LIST_INIT(roguegamemodes, list("Rebellion", "Vampire Lord", "Extended", "
else
return TRUE
+/datum/game_mode/chaosmode/proc/pick_lich()
+ restricted_jobs = list("King", "Queen", "Merchant", "Priest")
+ antag_candidates = get_players_for_role(ROLE_LICH)
+ var/datum/mind/lichman = pick_n_take(antag_candidates)
+ if(lichman)
+ var/blockme = FALSE
+ if(!(lichman in allantags))
+ blockme = TRUE
+ if(blockme)
+ return
+ allantags -= lichman
+ pre_liches += lichman
+ lichman.special_role = ROLE_LICH
+ lichman.restricted_roles = restricted_jobs.Copy()
+ testing("[key_name(lichman)] has been selected as the [lichman.special_role]")
+ log_game("[key_name(lichman)] has been selected as the [lichman.special_role]")
+ for(var/antag in pre_liches)
+ GLOB.pre_setup_antags |= antag
+ restricted_jobs = list()
+
/datum/game_mode/chaosmode/proc/pick_bandits()
//BANDITS
banditgoal = rand(200,400)
@@ -415,6 +435,13 @@ GLOBAL_LIST_INIT(roguegamemodes, list("Rebellion", "Vampire Lord", "Extended", "
GLOB.pre_setup_antags -= cultist
cultists += cultist
+///////////////// LICH
+ for(var/datum/mind/lichman in pre_liches)
+ var/datum/antagonist/new_antag = new /datum/antagonist/lich()
+ addtimer(CALLBACK(lichman, TYPE_PROC_REF(/datum/mind, add_antag_datum), new_antag), rand(10,100))
+ GLOB.pre_setup_antags -= lichman
+ liches += lichman
+
///////////////// WWOLF
for(var/datum/mind/werewolf in pre_werewolves)
var/datum/antagonist/new_antag = new /datum/antagonist/werewolf()
diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm
index 02598db88c..85ec051369 100644
--- a/code/modules/admin/sql_ban_system.dm
+++ b/code/modules/admin/sql_ban_system.dm
@@ -254,9 +254,10 @@
var/list/long_job_lists = list("Peasants" = GLOB.peasant_positions,
"Towners" = GLOB.towner_positions,
"Apprentices" = GLOB.apprentices_positions,
+ "Ghost and Other Roles" = list(ROLE_NECRO_SKELETON),
"Antagonist Positions" = list(ROLE_VILLAIN, ROLE_WEREWOLF,
ROLE_VAMPIRE, ROLE_NBEAST, ROLE_BANDIT,
- ROLE_DELF, ROLE_PREBEL, ROLE_ZIZOIDCULTIST))
+ ROLE_DELF, ROLE_PREBEL, ROLE_ZIZOIDCULTIST, ROLE_LICH))
for(var/department in long_job_lists)
output += "
"
break_counter = 0
diff --git a/code/modules/antagonists/roguetown/villain/lich.dm b/code/modules/antagonists/roguetown/villain/lich.dm
new file mode 100644
index 0000000000..a329242feb
--- /dev/null
+++ b/code/modules/antagonists/roguetown/villain/lich.dm
@@ -0,0 +1,192 @@
+/datum/antagonist/lich
+ name = "Lich"
+ roundend_category = "Lich"
+ antagpanel_category = "Lich"
+ job_rank = ROLE_LICH
+ confess_lines = list(
+ "I WILL LIVE ETERNAL!",
+ "I AM BEHIND SEVEN PHYLACTERIES!",
+ "YOU CANNOT KILL ME!",
+ )
+ var/list/phylacteries = list()
+ var/out_of_lives = FALSE
+
+/mob/living/carbon/human
+ /// List of minions that this mob has control over. Used for things like the Lich's "Command Undead" spell.
+ var/list/mob/minions = list()
+
+/datum/antagonist/lich/on_gain()
+ var/datum/game_mode/C = SSticker.mode
+ C.liches |= owner
+ . = ..()
+ owner.special_role = name
+ skele_look()
+ equip_lich()
+ greet()
+ return ..()
+
+/datum/antagonist/lich/greet()
+ to_chat(owner.current, span_userdanger("The secret of immortality is mine, but this is not enough. A thousand lichdoms have risen and fallen over the eras. Mine will be the one to last."))
+ owner.announce_objectives()
+ ..()
+
+/datum/antagonist/lich/proc/skele_look()
+ var/mob/living/carbon/human/L = owner.current
+ L.hairstyle = "Bald"
+ L.facial_hairstyle = "Shaved"
+ L.update_body()
+ L.update_hair()
+ L.update_body_parts(redraw = TRUE)
+
+/datum/antagonist/lich/proc/equip_lich()
+ owner.unknow_all_people()
+ for(var/datum/mind/MF in get_minds())
+ owner.become_unknown_to(MF)
+ var/mob/living/carbon/human/L = owner.current
+ ADD_TRAIT(L, TRAIT_NOROGSTAM, "[type]")
+ ADD_TRAIT(L, TRAIT_NOHUNGER, "[type]")
+ ADD_TRAIT(L, TRAIT_NOBREATH, "[type]")
+ ADD_TRAIT(L, TRAIT_NOPAIN, "[type]")
+ ADD_TRAIT(L, TRAIT_TOXIMMUNE, "[type]")
+ ADD_TRAIT(L, TRAIT_STEELHEARTED, "[type]")
+ ADD_TRAIT(L, TRAIT_NOSLEEP, "[type]")
+ ADD_TRAIT(L, TRAIT_VAMPMANSION, "[type]")
+ ADD_TRAIT(L, TRAIT_NOMOOD, "[type]")
+ ADD_TRAIT(L, TRAIT_NOLIMBDISABLE, "[type]")
+ ADD_TRAIT(L, TRAIT_SHOCKIMMUNE, "[type]")
+ ADD_TRAIT(L, TRAIT_LIMBATTACHMENT, "[type]")
+ ADD_TRAIT(L, TRAIT_SEEPRICES, "[type]")
+ ADD_TRAIT(L, TRAIT_CRITICAL_RESISTANCE, "[type]")
+ ADD_TRAIT(L, TRAIT_HEAVYARMOR, "[type]")
+ ADD_TRAIT(L, TRAIT_CABAL, "[type]")
+ ADD_TRAIT(L, TRAIT_DEATHSIGHT, "[type]")
+ L.cmode_music = 'sound/music/combat_cult.ogg'
+ L.faction = list("undead")
+ if(L.charflaw)
+ QDEL_NULL(L.charflaw)
+ L.mob_biotypes |= MOB_UNDEAD
+ var/obj/item/organ/eyes/eyes = L.getorganslot(ORGAN_SLOT_EYES)
+ if(eyes)
+ eyes.Remove(L,1)
+ QDEL_NULL(eyes)
+ eyes = new /obj/item/organ/eyes/night_vision/zombie
+ eyes.Insert(L)
+ for(var/obj/item/bodypart/B in L.bodyparts)
+ B.skeletonize(FALSE)
+ L.equipOutfit(/datum/outfit/job/roguetown/lich)
+ L.set_patron(/datum/patron/inhumen/zizo)
+
+/datum/outfit/job/roguetown/lich/pre_equip(mob/living/carbon/human/H)
+ ..()
+ head = /obj/item/clothing/head/roguetown/helmet/skullcap/cult
+ pants = /obj/item/clothing/under/roguetown/chainlegs
+ shoes = /obj/item/clothing/shoes/roguetown/shortboots
+ neck = /obj/item/clothing/neck/roguetown/chaincoif
+ armor = /obj/item/clothing/suit/roguetown/shirt/robe/necromancer
+ shirt = /obj/item/clothing/suit/roguetown/shirt/tunic/ucolored
+ wrists = /obj/item/clothing/wrists/roguetown/bracers
+ gloves = /obj/item/clothing/gloves/roguetown/chain
+ belt = /obj/item/storage/belt/rogue/leather/black
+ backl = /obj/item/storage/backpack/rogue/satchel
+ beltr = /obj/item/reagent_containers/glass/bottle/rogue/manapot
+ beltl = /obj/item/rogueweapon/knife/dagger/steel
+ r_hand = /obj/item/rogueweapon/polearm/woodstaff
+
+ H.mind.adjust_skillrank(/datum/skill/misc/reading, 6, TRUE)
+ H.mind?.adjust_skillrank(/datum/skill/craft/alchemy, 5, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/magic/arcane, 5, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/riding, 4, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/polearms, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/unarmed, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/swimming, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/climbing, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/athletics, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/knives, 5, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/craft/crafting, 1, TRUE)
+ //H.mind.adjust_skillrank(/datum/skill/misc/medicine, 4, TRUE)
+
+ H.change_stat("strength", -1)
+ H.change_stat("intelligence", 5)
+ H.change_stat("constitution", 5)
+ H.change_stat("endurance", -1)
+ H.change_stat("speed", -1)
+
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/self/command_undead)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/strengthen_undead)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/raise_undead)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/projectile/fireball)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/projectile/bloodlightning)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/eyebite)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/projectile/sickness)
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/invoked/projectile/fetch)
+ H.ambushable = FALSE
+
+ addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon/human, choose_name_popup), "LICH"), 5 SECONDS)
+
+/datum/outfit/job/roguetown/lich/post_equip(mob/living/carbon/human/H)
+ ..()
+ var/datum/antagonist/lich/lichman = H.mind.has_antag_datum(/datum/antagonist/lich)
+ for(var/i in 1 to 3)
+ var/obj/item/phylactery/new_phylactery = new(H.loc)
+ lichman.phylacteries += new_phylactery
+ new_phylactery.possessor = lichman
+ H.equip_to_slot_or_del(new_phylactery,SLOT_IN_BACKPACK, TRUE)
+
+/datum/antagonist/lich/proc/consume_phylactery(timer = 10 SECONDS)
+ for(var/obj/item/phylactery/phyl in phylacteries)
+ phyl.be_consumed(timer)
+ phylacteries -= phyl
+ return TRUE
+
+/datum/antagonist/lich/proc/rise_anew()
+ var/mob/living/carbon/human/bigbad = owner.current
+ bigbad.revive(TRUE, TRUE)
+
+ for(var/obj/item/bodypart/B in bigbad.bodyparts)
+ B.skeletonize(FALSE)
+
+ bigbad.faction = list("undead")
+ if(bigbad.charflaw)
+ QDEL_NULL(bigbad.charflaw)
+ bigbad.mob_biotypes |= MOB_UNDEAD
+ var/obj/item/organ/eyes/eyes = bigbad.getorganslot(ORGAN_SLOT_EYES)
+ if(eyes)
+ eyes.Remove(bigbad,1)
+ QDEL_NULL(eyes)
+ eyes = new /obj/item/organ/eyes/night_vision/zombie
+ eyes.Insert(bigbad)
+
+
+/obj/item/phylactery
+ name = "phylactery"
+ desc = "Looks like it is filled with some intense power."
+ icon = 'icons/obj/wizard.dmi'
+ icon_state = "soulstone"
+ item_state = "electronic"
+ lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi'
+ layer = HIGH_OBJ_LAYER
+ w_class = WEIGHT_CLASS_TINY
+ light_color = "#003300"
+ var/datum/antagonist/lich/possessor
+
+ var/resurrections = 0
+ var/datum/mind/mind
+ var/respawn_time = 1800
+
+ var/static/active_phylacteries = 0
+
+/obj/item/phylactery/Initialize(mapload, datum/mind/newmind)
+ . = ..()
+ filters += filter(type="drop_shadow", x=0, y=0, size=1, offset=2, color=rgb(rand(1,255),rand(1,255),rand(1,255)))
+
+/obj/item/phylactery/proc/be_consumed(timer)
+ var/offset = prob(50) ? -2 : 2
+ animate(src, pixel_x = pixel_x + offset, time = 0.2, loop = -1) //start shaking
+ visible_message(span_warning("[src] begins to glow and shake violently!"))
+ spawn(timer)
+ possessor.owner.current.forceMove(get_turf(src))
+ possessor.rise_anew()
+ qdel(src)
diff --git a/code/modules/antagonists/wizard/equipment/spellbook.dm b/code/modules/antagonists/wizard/equipment/spellbook.dm
index 4a5b5a33f1..310ed0458b 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook.dm
@@ -197,11 +197,6 @@
category = "Assistance"
cost = 1
-/datum/spellbook_entry/lichdom
- name = "Bind Soul"
- spell_type = /obj/effect/proc_holder/spell/targeted/lichdom
- category = "Defensive"
-
/datum/spellbook_entry/teslablast
name = "Tesla Blast"
spell_type = /obj/effect/proc_holder/spell/targeted/tesla
diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm
index 879769a956..14b6657965 100644
--- a/code/modules/mob/living/carbon/human/death.dm
+++ b/code/modules/mob/living/carbon/human/death.dm
@@ -45,6 +45,18 @@
dust(just_ash=TRUE,drop_items=TRUE)
return
+ var/datum/antagonist/lich/L = mind.has_antag_datum(/datum/antagonist/lich)
+ if (L && !L.out_of_lives)
+ if(L.consume_phylactery())
+ visible_message(span_warning("[src]'s body begins to shake violently, as eldritch forces begin to whisk them away!"))
+ to_chat(src, span_userdanger("Death is not the end for me. I begin to rise again."))
+ playsound(src, 'sound/magic/antimagic.ogg', 100, FALSE)
+ else
+ to_chat(src, span_userdanger("No, NO! This cannot be!"))
+ L.out_of_lives = TRUE
+ gib()
+ return
+
if(!gibbed)
if(!is_in_roguetown(src))
zombie_check()
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index a2674e7675..9670a4f229 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -129,4 +129,34 @@
set_typing_indicator(FALSE)
+ if (client)
+ if (!gibbed)
+ var/locale = prepare_deathsight_message()
+ for (var/mob/living/player in GLOB.player_list)
+ if (player.stat == DEAD || isbrain(player))
+ continue
+ if (HAS_TRAIT(player, TRAIT_DEATHSIGHT))
+ if (HAS_TRAIT(player, TRAIT_CABAL))
+ to_chat(player, span_warning("I feel the faint passage of disjointed life essence as it flees [locale]."))
+ else
+ to_chat(player, span_warning("Veiled whispers herald the Undermaiden's gaze in my mind's eye as it turn towards [locale] for but a brief, singular moment."))
+
return TRUE
+
+/mob/living/proc/prepare_deathsight_message()
+ var/area_of_death = lowertext(get_area_name(src))
+ var/locale = "a locale wreathed in enigmatic fog"
+ switch (area_of_death) // we're deliberately obtuse with this.
+ if ("mountains", "mt decapitation")
+ locale = "a twisted tangle of soaring peaks"
+ if ("wilderness", "rockhill basin")
+ locale = "somewhere in the wilds"
+ if ("bog", "dense bog")
+ locale = "a wretched, fetid bog"
+ if ("coast", "coastforest")
+ locale = "somewhere betwixt Abyssor's realm and Dendor's bounty"
+ if ("indoors", "shop", "physician", "outdoors", "roofs", "manor", "wizard's tower", "garrison", "dungeon cell", "baths", "tavern")
+ locale = "the city of Rockhill and all its bustling souls"
+ if ("church")
+ locale = "a hallowed place, sworn to the Ten" // special bit for the church since it's sacred ground
+ return locale
diff --git a/code/modules/spells/roguetown/necromancer.dm b/code/modules/spells/roguetown/necromancer.dm
new file mode 100644
index 0000000000..33ff4b6cc3
--- /dev/null
+++ b/code/modules/spells/roguetown/necromancer.dm
@@ -0,0 +1,286 @@
+/obj/effect/proc_holder/spell/invoked/strengthen_undead
+ name = "Strengthen Undead"
+ overlay_state = "raiseskele"
+ releasedrain = 30
+ chargetime = 5
+ range = 7
+ warnie = "sydwarning"
+ movement_interrupt = FALSE
+ chargedloop = null
+ sound = 'sound/magic/whiteflame.ogg'
+ associated_skill = /datum/skill/magic/arcane
+ antimagic_allowed = TRUE
+ charge_max = 15 SECONDS
+ miracle = FALSE
+
+/obj/effect/proc_holder/spell/invoked/strengthen_undead/cast(list/targets, mob/living/user)
+ . = ..()
+ if(isliving(targets[1]))
+ var/mob/living/target = targets[1]
+ if(target.mob_biotypes & MOB_UNDEAD) //positive energy harms the undead
+ var/obj/item/bodypart/affecting = target.get_bodypart(check_zone(user.zone_selected))
+ if(affecting)
+ if(affecting.heal_damage(50, 50))
+ target.update_damage_overlays()
+ if(affecting.heal_wounds(50))
+ target.update_damage_overlays()
+ target.visible_message(span_danger("[target] reforms under the vile energy!"), span_notice("I'm remade by dark magic!"))
+ return TRUE
+ target.visible_message(span_info("Necrotic energy floods over [target]!"), span_userdanger("I feel colder as the dark energy floods into me!"))
+ if(iscarbon(target))
+ target.Paralyze(50)
+ else
+ target.adjustBruteLoss(20)
+ return TRUE
+ return FALSE
+
+/obj/effect/proc_holder/spell/invoked/eyebite
+ name = "Eyebite"
+ overlay_state = "profane"
+ releasedrain = 30
+ chargetime = 15
+ range = 7
+ warnie = "sydwarning"
+ movement_interrupt = FALSE
+ chargedloop = null
+ sound = 'sound/items/beartrap.ogg'
+ associated_skill = /datum/skill/magic/arcane
+ antimagic_allowed = TRUE
+ charge_max = 15 SECONDS
+ miracle = FALSE
+
+/obj/effect/proc_holder/spell/invoked/eyebite/cast(list/targets, mob/living/user)
+ . = ..()
+ if(isliving(targets[1]))
+ var/mob/living/carbon/target = targets[1]
+ target.visible_message(span_info("A loud crunching sound has come from [target]!"), span_userdanger("I feel arcane teeth biting into my eyes!"))
+ target.adjustBruteLoss(30)
+ target.blind_eyes(2)
+ target.blur_eyes(10)
+ return TRUE
+ return FALSE
+
+/obj/effect/proc_holder/spell/invoked/raise_undead
+ name = "Raise Undead"
+ desc = ""
+ clothes_req = FALSE
+ range = 7
+ overlay_state = "raiseskele"
+ sound = list('sound/magic/magnet.ogg')
+ releasedrain = 40
+ chargetime = 60
+ warnie = "spellwarning"
+ no_early_release = TRUE
+ charging_slowdown = 1
+ chargedloop = /datum/looping_sound/invokegen
+ associated_skill = /datum/skill/magic/arcane
+ charge_max = 30 SECONDS
+
+
+/**
+ * Raises a minion from a corpse. Prioritizing ownership to original player > ghosts > npc.
+ *
+ * Vars:
+ * * targets: list of mobs that are targetted.
+ * * user: spell caster.
+ */
+/obj/effect/proc_holder/spell/invoked/raise_undead/cast(list/targets, mob/living/carbon/human/user)
+ . = ..()
+
+ user.say("Hgf'ant'kthar!")
+
+ var/obj = targets[1]
+
+ if(!obj || !istype(obj, /mob/living/carbon/human))
+ to_chat(user, span_warning("I need to cast this spell on a corpse."))
+ return FALSE
+
+ // bandaid until goblin skeleton immortality is fixed
+ if(istype(obj, /mob/living/carbon/human/species/goblin))
+ to_chat(user, span_warning("I cannot raise goblins."))
+ return FALSE
+
+ var/mob/living/carbon/human/target = obj
+
+ if(target.stat != DEAD)
+ to_chat(user, span_warning("I cannot raise the living."))
+ return FALSE
+
+ var/obj/item/bodypart/target_head = target.get_bodypart(BODY_ZONE_HEAD)
+ if(!target_head)
+ to_chat(user, span_warning("This corpse is headless."))
+ return FALSE
+
+ var/offer_refused = FALSE
+
+ target.visible_message(span_warning("[target.real_name]'s body is engulfed by dark energy..."))
+
+ if(target.ckey) //player still inside body
+
+ var/offer = alert(target, "Do you wish to be reanimated as a minion?", "RAISED BY NECROMANCER", "Yes", "No")
+ var/offer_time = world.time
+
+ if(offer == "No" || world.time > offer_time + 5 SECONDS)
+ to_chat(target, span_danger("Another soul will take over."))
+ offer_refused = TRUE
+
+ else if(offer == "Yes")
+ to_chat(target, span_danger("You rise as a minion."))
+ target.turn_to_minion(user, target.ckey)
+ target.visible_message(span_warning("[target.real_name]'s eyes light up with an evil glow."))
+ return TRUE
+
+ if(!target.ckey || offer_refused) //player is not inside body or has refused, poll for candidates
+
+ var/list/candidates = pollCandidatesForMob("Do you want to play as a Necromancer's minion?", null, null, null, 100, target, POLL_IGNORE_NECROMANCER_SKELETON)
+
+ // theres at least one candidate
+ if(LAZYLEN(candidates))
+ var/mob/C = pick(candidates)
+ target.turn_to_minion(user, C.ckey)
+ target.visible_message(span_warning("[target.real_name]'s eyes light up with an eerie glow."))
+
+ //no candidates, raise as npc
+ else
+ target.turn_to_minion(user)
+ target.visible_message(span_warning("[target.real_name]'s eyes light up with a weak glow."))
+
+ return TRUE
+
+ return FALSE
+
+/**
+ * Turns a mob into a skeletonized minion. Used for raising undead minions.
+ * If a ckey is provided, the minion will be controlled by the player, NPC otherwise.
+ *
+ * Vars:
+ * * master: master of the minion.
+ * * ckey (optional): ckey of the player that will control the minion.
+ */
+/mob/living/carbon/human/proc/turn_to_minion(mob/living/carbon/human/master, ckey)
+
+ if(!master)
+ return FALSE
+
+ src.revive(TRUE, TRUE)
+
+ if(ckey) //player
+ src.ckey = ckey
+ else //npc
+ aggressive = 1
+ mode = AI_HUNT
+ wander = TRUE
+
+ if(!mind)
+ mind_initialize()
+
+ mind.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE)
+ mind.adjust_skillrank(/datum/skill/combat/crossbows, 3, TRUE)
+ mind.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE)
+ mind.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE)
+ mind.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE)
+ mind.current.job = null
+
+ dna.species.species_traits |= NOBLOOD
+ dna.species.soundpack_m = new /datum/voicepack/skeleton()
+ dna.species.soundpack_f = new /datum/voicepack/skeleton()
+
+
+ cmode_music = 'sound/music/combat_cult.ogg'
+
+ patron = master.patron
+ mob_biotypes = MOB_UNDEAD
+ faction = list("undead")
+ ambushable = FALSE
+ underwear = "Nude"
+
+ for(var/obj/item/bodypart/BP in bodyparts)
+ BP.skeletonize()
+
+ var/obj/item/organ/eyes/eyes = getorganslot(ORGAN_SLOT_EYES)
+ if(eyes)
+ eyes.Remove(src,1)
+ QDEL_NULL(eyes)
+
+ eyes = new /obj/item/organ/eyes/night_vision/zombie
+ eyes.Insert(src)
+
+ if(charflaw)
+ QDEL_NULL(charflaw)
+
+ ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NOLIMBDISABLE, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_EASYDISMEMBER, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_LIMBATTACHMENT, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NOBREATH, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NOPAIN, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_TOXIMMUNE, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_NOSLEEP, TRAIT_GENERIC)
+ ADD_TRAIT(src, TRAIT_SHOCKIMMUNE, TRAIT_GENERIC)
+
+ update_body()
+
+ to_chat(src, span_userdanger("My master is [master.real_name]."))
+
+ master.minions += src
+
+ return TRUE
+
+/obj/effect/proc_holder/spell/invoked/projectile/sickness
+ name = "Ray of Sickness"
+ desc = ""
+ clothes_req = FALSE
+ range = 15
+ projectile_type = /obj/projectile/magic/sickness
+ overlay_state = "profane"
+ sound = list('sound/misc/portal_enter.ogg')
+ active = FALSE
+ releasedrain = 30
+ chargetime = 10
+ warnie = "spellwarning"
+ no_early_release = TRUE
+ charging_slowdown = 1
+ chargedloop = /datum/looping_sound/invokegen
+ associated_skill = /datum/skill/magic/arcane
+ charge_max = 15 SECONDS
+
+/obj/effect/proc_holder/spell/self/command_undead
+ name = "Command Undead"
+ desc = "!"
+ overlay_state = "animate"
+ sound = list('sound/magic/magnet.ogg')
+ invocation = "Zuth'gorash vel'thar dral'oth!"
+ invocation_type = "shout"
+ antimagic_allowed = TRUE
+ charge_max = 15 SECONDS
+
+/obj/effect/proc_holder/spell/self/command_undead/cast(mob/user = usr)
+ ..()
+
+ var/message = input("Speak to your minions!", "LICH") as text|null
+
+ if(!message)
+ return
+
+ var/mob/living/carbon/human/lich_player = user
+
+ to_chat(lich_player, span_boldannounce("Lich [lich_player.real_name] commands: [message]"))
+
+ for(var/mob/player in lich_player.minions)
+ if(player.mind)
+ to_chat(player, span_boldannounce("Lich [lich_player.real_name] commands: [message]"))
+
+
+/obj/projectile/magic/sickness
+ name = "Bolt of Sickness"
+ icon_state = "xray"
+ damage = 10
+ damage_type = BURN
+ flag = "magic"
+
+/obj/projectile/magic/sickness/on_hit(atom/target, blocked = FALSE)
+ . = ..()
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.reagents.add_reagent(/datum/reagent/toxin, 3)
diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm
deleted file mode 100644
index 7695370cc8..0000000000
--- a/code/modules/spells/spell_types/lichdom.dm
+++ /dev/null
@@ -1,160 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/lichdom
- name = "Bind Soul"
- desc = "A dark necromantic pact that can forever bind my soul to an \
- item of my choosing. So long as both my body and the item remain \
- intact and on the same plane you can revive from death, though the time \
- between reincarnations grows steadily with use, along with the weakness \
- that the new skeleton body will experience upon 'birth'. Note that \
- becoming a lich destroys all internal organs except the brain."
- school = "necromancy"
- charge_max = 10
- clothes_req = FALSE
- centcom_cancast = FALSE
- invocation = "NECREM IMORTIUM!"
- invocation_type = "shout"
- range = -1
- level_max = 0 //cannot be improved
- cooldown_min = 10
- include_user = TRUE
-
- action_icon = 'icons/mob/actions/actions_spells.dmi'
- action_icon_state = "skeleton"
-
-/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets,mob/user = usr)
- for(var/mob/M in targets)
- var/list/hand_items = list()
- if(iscarbon(M))
- hand_items = list(M.get_active_held_item(),M.get_inactive_held_item())
- if(!hand_items.len)
- to_chat(M, "I must hold an item you wish to make my phylactery!")
- return
- if(!M.mind.hasSoul)
- to_chat(user, "I do not possess a soul!")
- return
-
- var/obj/item/marked_item
-
- for(var/obj/item/item in hand_items)
- // I ensouled the nuke disk once. But it's probably a really
- // mean tactic, so probably should discourage it.
- if((item.item_flags & ABSTRACT) || HAS_TRAIT(item, TRAIT_NODROP) || SEND_SIGNAL(item, COMSIG_ITEM_IMBUE_SOUL, user))
- continue
- marked_item = item
- to_chat(M, "I begin to focus my very being into [item]...")
- break
-
- if(!marked_item)
- to_chat(M, "None of the items you hold are suitable for emplacement of my fragile soul.")
- return
-
- playsound(user, 'sound/blank.ogg', 100)
-
- if(!do_after(M, 50, needhand=FALSE, target=marked_item))
- to_chat(M, "My soul snaps back to my body as you stop ensouling [marked_item]!")
- return
-
- marked_item.name = "ensouled [marked_item.name]"
- marked_item.desc += "\nA terrible aura surrounds this item, its very existence is offensive to life itself..."
- marked_item.add_atom_colour("#003300", ADMIN_COLOUR_PRIORITY)
-
- new /obj/item/phylactery(marked_item, M.mind)
-
- to_chat(M, "With a hideous feeling of emptiness you watch in horrified fascination as skin sloughs off bone! Blood boils, nerves disintegrate, eyes boil in their sockets! As my organs crumble to dust in my fleshless chest you come to terms with my choice. You're a lich!")
- M.mind.hasSoul = FALSE
- M.set_species(/datum/species/skeleton)
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- H.dropItemToGround(H.wear_pants)
- H.dropItemToGround(H.wear_armor)
- H.dropItemToGround(H.head)
- H.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(H), SLOT_ARMOR)
- H.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(H), SLOT_HEAD)
- H.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(H), SLOT_PANTS)
-
- // you only get one phylactery.
- M.mind.RemoveSpell(src)
-
-
-/obj/item/phylactery
- name = "phylactery"
- desc = ""
- icon = 'icons/obj/projectiles.dmi'
- icon_state = "bluespace"
- color = "#003300"
- light_color = "#003300"
- var/lon_range = 3
- var/resurrections = 0
- var/datum/mind/mind
- var/respawn_time = 1800
-
- var/static/active_phylacteries = 0
-
-/obj/item/phylactery/Initialize(mapload, datum/mind/newmind)
- . = ..()
- mind = newmind
- name = "phylactery of [mind.name]"
-
- active_phylacteries++
- GLOB.poi_list |= src
- START_PROCESSING(SSobj, src)
- set_light(lon_range)
- if(initial(SSticker.mode.round_ends_with_antag_death))
- SSticker.mode.round_ends_with_antag_death = FALSE
-
-/obj/item/phylactery/Destroy(force=FALSE)
- STOP_PROCESSING(SSobj, src)
- active_phylacteries--
- GLOB.poi_list -= src
- if(!active_phylacteries)
- SSticker.mode.round_ends_with_antag_death = initial(SSticker.mode.round_ends_with_antag_death)
- . = ..()
-
-/obj/item/phylactery/process()
- if(QDELETED(mind))
- qdel(src)
- return
-
- if(!mind.current || (mind.current && mind.current.stat == DEAD))
- addtimer(CALLBACK(src, PROC_REF(rise)), respawn_time, TIMER_UNIQUE)
-
-/obj/item/phylactery/proc/rise()
- if(mind.current && mind.current.stat != DEAD)
- return "[mind] already has a living body: [mind.current]"
-
- var/turf/item_turf = get_turf(src)
- if(!item_turf)
- return "[src] is not at a turf? NULLSPACE!?"
-
- var/mob/old_body = mind.current
- var/mob/living/carbon/human/lich = new(item_turf)
-
- lich.equip_to_slot_or_del(new /obj/item/clothing/shoes/sandal/magic(lich), SLOT_SHOES)
- lich.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(lich), SLOT_PANTS)
- lich.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(lich), SLOT_ARMOR)
- lich.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(lich), SLOT_HEAD)
-
- lich.real_name = mind.name
- mind.transfer_to(lich)
- mind.grab_ghost(force=TRUE)
- lich.hardset_dna(null,null,lich.real_name,null, new /datum/species/skeleton)
- to_chat(lich, "My bones clatter and shudder as you are pulled back into this world!")
- var/turf/body_turf = get_turf(old_body)
- lich.Paralyze(200 + 200*resurrections)
- resurrections++
- if(old_body && old_body.loc)
- if(iscarbon(old_body))
- var/mob/living/carbon/C = old_body
- for(var/obj/item/W in C)
- C.dropItemToGround(W)
- for(var/X in C.internal_organs)
- var/obj/item/organ/I = X
- I.Remove(C)
- I.forceMove(body_turf)
- var/wheres_wizdo = dir2text(get_dir(body_turf, item_turf))
- if(wheres_wizdo)
- old_body.visible_message("Suddenly [old_body.name]'s corpse falls to pieces! You see a strange energy rise from the remains, and speed off towards the [wheres_wizdo]!")
- body_turf.Beam(item_turf,icon_state="lichbeam",time=10+10*resurrections,maxdistance=INFINITY)
- old_body.dust()
-
-
- return "Respawn of [mind] successful."
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 3f8c9eb6be..355709ca45 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -204,7 +204,7 @@
for(var/obj/item/I in src)
I.forceMove(T)
-/obj/item/bodypart/proc/skeletonize()
+/obj/item/bodypart/proc/skeletonize(lethal = TRUE)
if(bandage)
remove_bandage()
for(var/obj/item/I in embedded_objects)
@@ -213,14 +213,14 @@
qdel(I)
skeletonized = TRUE
-/obj/item/bodypart/chest/skeletonize()
+/obj/item/bodypart/chest/skeletonize(lethal = TRUE)
. = ..()
- if(owner && !(NOBLOOD in owner.dna?.species?.species_traits))
+ if(lethal && owner && !(NOBLOOD in owner.dna?.species?.species_traits))
owner.death()
-/obj/item/bodypart/head/skeletonize()
+/obj/item/bodypart/head/skeletonize(lethal = TRUE)
. = ..()
- if(owner && !(NOBLOOD in owner.dna?.species?.species_traits))
+ if(lethal && owner && !(NOBLOOD in owner.dna?.species?.species_traits))
owner.death()
/obj/item/bodypart/proc/consider_processing()
diff --git a/icons/mob/actions/roguespells.dmi b/icons/mob/actions/roguespells.dmi
index 92cc34d188..ebce5489ae 100644
Binary files a/icons/mob/actions/roguespells.dmi and b/icons/mob/actions/roguespells.dmi differ
diff --git a/stonekeep.dme b/stonekeep.dme
index 6d0f4b8bd5..86bdaaf30a 100644
--- a/stonekeep.dme
+++ b/stonekeep.dme
@@ -1337,6 +1337,7 @@
#include "code\modules\antagonists\roguetown\villain\assassin.dm"
#include "code\modules\antagonists\roguetown\villain\bandit.dm"
#include "code\modules\antagonists\roguetown\villain\choosename.dm"
+#include "code\modules\antagonists\roguetown\villain\lich.dm"
#include "code\modules\antagonists\roguetown\villain\maniac.dm"
#include "code\modules\antagonists\roguetown\villain\peasantrebel.dm"
#include "code\modules\antagonists\roguetown\villain\vampire.dm"
@@ -2517,6 +2518,7 @@
#include "code\modules\spells\roguetown\confessor.dm"
#include "code\modules\spells\roguetown\jester.dm"
#include "code\modules\spells\roguetown\monk.dm"
+#include "code\modules\spells\roguetown\necromancer.dm"
#include "code\modules\spells\roguetown\priest.dm"
#include "code\modules\spells\roguetown\spider.dm"
#include "code\modules\spells\roguetown\wizard.dm"
@@ -2546,7 +2548,6 @@
#include "code\modules\spells\spell_types\infinite_guns.dm"
#include "code\modules\spells\spell_types\inflict_handler.dm"
#include "code\modules\spells\spell_types\knock.dm"
-#include "code\modules\spells\spell_types\lichdom.dm"
#include "code\modules\spells\spell_types\lightning.dm"
#include "code\modules\spells\spell_types\mime.dm"
#include "code\modules\spells\spell_types\mind_transfer.dm"