diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index bcb0b0a9e85..800b6086762 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -94,8 +94,18 @@
///From obj/item/toy/crayon/spraycan
#define COMSIG_LIVING_MOB_PAINTED "living_mob_painted"
-///From mob/living/proc/wabbajack_act
-#define COMSIG_LIVING_WABBAJACKED "living_wabbajacked"
+
+///From mob/living/proc/wabbajack(): (randomize_type)
+#define COMSIG_LIVING_PRE_WABBAJACKED "living_mob_wabbajacked"
+ /// Return to stop the rest of the wabbajack from triggering.
+ #define STOP_WABBAJACK (1<<0)
+///From mob/living/proc/on_wabbajack(): (mob/living/new_mob)
+#define COMSIG_LIVING_ON_WABBAJACKED "living_wabbajacked"
+
+/// From /datum/status_effect/shapechange_mob/on_apply(): (mob/living/shape)
+#define COMSIG_LIVING_SHAPESHIFTED "living_shapeshifted"
+/// From /datum/status_effect/shapechange_mob/after_unchange(): (mob/living/caster)
+#define COMSIG_LIVING_UNSHAPESHIFTED "living_unshapeshifted"
///From /obj/effect/rune/convert/do_sacrifice() : (list/invokers)
#define COMSIG_LIVING_CULT_SACRIFICED "living_cult_sacrificed"
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index fff65736f55..66efb3cb2f3 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -484,6 +484,16 @@
//Wabbacjack staff projectiles
#define WABBAJACK (1<<6)
+// Randomization keys for calling wabbajack with.
+// Note the contents of these keys are important, as they're displayed to the player
+// Ex: (You turn into a "monkey", You turn into a "xenomorph")
+#define WABBAJACK_MONKEY "monkey"
+#define WABBAJACK_ROBOT "robot"
+#define WABBAJACK_SLIME "slime"
+#define WABBAJACK_XENO "xenomorph"
+#define WABBAJACK_HUMAN "humanoid"
+#define WABBAJACK_ANIMAL "animal"
+
// Reasons a defibrilation might fail
#define DEFIB_POSSIBLE (1<<0)
#define DEFIB_FAIL_SUICIDE (1<<1)
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index dce92503ba6..13220043713 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -37,5 +37,4 @@
#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
-#define STASIS_ASCENSION_EFFECT "heretic_ascension"
-
+#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
diff --git a/code/datums/elements/content_barfer.dm b/code/datums/elements/content_barfer.dm
index 2652467e8a2..0d105d7750b 100644
--- a/code/datums/elements/content_barfer.dm
+++ b/code/datums/elements/content_barfer.dm
@@ -13,10 +13,10 @@
if(!isliving(target))
return ELEMENT_INCOMPATIBLE
- RegisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_WABBAJACKED), .proc/barf_contents)
+ RegisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED), .proc/barf_contents)
/datum/element/content_barfer/Detach(datum/target)
- UnregisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_WABBAJACKED))
+ UnregisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED))
return ..()
/datum/element/content_barfer/proc/barf_contents(mob/living/target)
diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
index 98d955d52d6..e98fdc606e6 100644
--- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm
@@ -303,7 +303,7 @@
. = ..()
priority_announce("[generate_heretic_text()] Ever coiling vortex. Reality unfolded. ARMS OUTREACHED, THE LORD OF THE NIGHT, [user.real_name] has ascended! Fear the ever twisting hand! [generate_heretic_text()]", "[generate_heretic_text()]", ANNOUNCER_SPANOMALIES)
- var/datum/action/cooldown/spell/shed_human_form/worm_spell = new(user.mind)
+ var/datum/action/cooldown/spell/shapeshift/shed_human_form/worm_spell = new(user.mind)
worm_spell.Grant(user)
user.client?.give_award(/datum/award/achievement/misc/flesh_ascension, user)
diff --git a/code/modules/antagonists/heretic/magic/flesh_ascension.dm b/code/modules/antagonists/heretic/magic/flesh_ascension.dm
index ebe1f89d273..73eea83a8d3 100644
--- a/code/modules/antagonists/heretic/magic/flesh_ascension.dm
+++ b/code/modules/antagonists/heretic/magic/flesh_ascension.dm
@@ -1,4 +1,4 @@
-/datum/action/cooldown/spell/shed_human_form
+/datum/action/cooldown/spell/shapeshift/shed_human_form
name = "Shed form"
desc = "Shed your fragile form, become one with the arms, become one with the emperor. \
Causes heavy amounts of brain damage and sanity loss to nearby mortals."
@@ -7,49 +7,41 @@
button_icon_state = "worm_ascend"
school = SCHOOL_FORBIDDEN
- cooldown_time = 10 SECONDS
invocation = "REALITY UNCOIL!"
invocation_type = INVOCATION_SHOUT
spell_requirements = NONE
+ possible_shapes = list(/mob/living/simple_animal/hostile/heretic_summon/armsy/prime)
+
/// The length of our new wormy when we shed.
var/segment_length = 10
/// The radius around us that we cause brain damage / sanity damage to.
var/scare_radius = 9
-/datum/action/cooldown/spell/shed_human_form/is_valid_target(atom/cast_on)
- return isliving(cast_on)
+/datum/action/cooldown/spell/shapeshift/shed_human_form/do_shapeshift(mob/living/caster)
+ // When we transform into the worm, everyone nearby gets freaked out
+ for(var/mob/living/carbon/human/nearby_human in view(scare_radius, caster))
+ if(IS_HERETIC_OR_MONSTER(nearby_human))
+ continue
-/datum/action/cooldown/spell/shed_human_form/cast(mob/living/cast_on)
- . = ..()
- if(istype(cast_on, /mob/living/simple_animal/hostile/heretic_summon/armsy/prime))
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/old_armsy = cast_on
- var/mob/living/our_heretic = locate() in old_armsy
+ // 25% chance to cause a trauma
+ if(prob(25))
+ var/datum/brain_trauma/trauma = pick(subtypesof(BRAIN_TRAUMA_MILD) + subtypesof(BRAIN_TRAUMA_SEVERE))
+ nearby_human.gain_trauma(trauma, TRAUMA_RESILIENCE_LOBOTOMY)
+ // And a negative moodlet
+ nearby_human.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus)
- if(our_heretic.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_ASCENSION_EFFECT))
- our_heretic.forceMove(old_armsy.loc)
+ return ..()
- old_armsy.mind.transfer_to(our_heretic, TRUE)
- segment_length = old_armsy.get_length()
- qdel(old_armsy)
+/datum/action/cooldown/spell/shapeshift/shed_human_form/do_unshapeshift(mob/living/simple_animal/hostile/heretic_summon/armsy/caster)
+ if(istype(caster))
+ segment_length = caster.get_length()
- else
- var/mob/living/simple_animal/hostile/heretic_summon/armsy/prime/new_armsy = new(cast_on.loc, TRUE, segment_length)
+ return ..()
- cast_on.mind.transfer_to(new_armsy, TRUE)
- cast_on.forceMove(new_armsy)
- cast_on.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_ASCENSION_EFFECT)
-
- // They see the very reality uncoil before their eyes.
- for(var/mob/living/carbon/human/nearby_human in view(scare_radius, new_armsy))
- if(IS_HERETIC_OR_MONSTER(nearby_human))
- continue
- nearby_human.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus)
-
- if(prob(25))
- var/datum/brain_trauma/trauma = pick(subtypesof(BRAIN_TRAUMA_MILD) + subtypesof(BRAIN_TRAUMA_SEVERE))
- nearby_human.gain_trauma(trauma, TRAUMA_RESILIENCE_LOBOTOMY)
+/datum/action/cooldown/spell/shapeshift/shed_human_form/create_shapeshift_mob(atom/loc)
+ return new shapeshift_type(loc, TRUE, segment_length)
/datum/action/cooldown/spell/worm_contract
name = "Force Contract"
diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm
index e3f29cd3352..0e177b079d5 100644
--- a/code/modules/antagonists/xeno/xeno.dm
+++ b/code/modules/antagonists/xeno/xeno.dm
@@ -42,3 +42,13 @@
mind.add_antag_datum(/datum/antagonist/xeno)
mind.set_assigned_role(SSjob.GetJobType(/datum/job/xenomorph))
mind.special_role = ROLE_ALIEN
+
+/mob/living/carbon/alien/on_wabbajacked(mob/living/new_mob)
+ . = ..()
+ if(!mind)
+ return
+ if(isalien(new_mob))
+ return
+ mind.remove_antag_datum(/datum/antagonist/xeno)
+ mind.set_assigned_role(SSjob.GetJobType(/datum/job/unassigned))
+ mind.special_role = null
diff --git a/code/modules/events/wizard/magicarp.dm b/code/modules/events/wizard/magicarp.dm
index fb53fdbc3e0..bb1fd39136c 100644
--- a/code/modules/events/wizard/magicarp.dm
+++ b/code/modules/events/wizard/magicarp.dm
@@ -40,13 +40,23 @@
gold_core_spawnable = NO_SPAWN
random_color = FALSE
- var/allowed_projectile_types = list(/obj/projectile/magic/change, /obj/projectile/magic/animate, /obj/projectile/magic/resurrection,
- /obj/projectile/magic/death, /obj/projectile/magic/teleport, /obj/projectile/magic/door, /obj/projectile/magic/fireball,
- /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage)
+ /// List of all projectiles we can fire.
+ /// Non-static, because subtypes can have their own lists.
+ var/list/allowed_projectile_types = list(
+ /obj/projectile/magic/animate,
+ /obj/projectile/magic/arcane_barrage,
+ /obj/projectile/magic/change,
+ /obj/projectile/magic/death,
+ /obj/projectile/magic/door,
+ /obj/projectile/magic/fireball,
+ /obj/projectile/magic/resurrection,
+ /obj/projectile/magic/spellblade,
+ /obj/projectile/magic/teleport,
+ )
/mob/living/simple_animal/hostile/carp/ranged/Initialize(mapload)
projectiletype = pick(allowed_projectile_types)
- . = ..()
+ return ..()
/mob/living/simple_animal/hostile/carp/ranged/chaos
name = "chaos magicarp"
@@ -58,14 +68,27 @@
/mob/living/simple_animal/hostile/carp/ranged/chaos/Shoot()
projectiletype = pick(allowed_projectile_types)
- ..()
+ return ..()
/mob/living/simple_animal/hostile/carp/ranged/xenobiology // these are for the xenobio gold slime pool
gold_core_spawnable = HOSTILE_SPAWN
- allowed_projectile_types = list(/obj/projectile/magic/animate, /obj/projectile/magic/teleport,
- /obj/projectile/magic/door, /obj/projectile/magic/fireball, /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage) //thanks Lett1
+
+ allowed_projectile_types = list(
+ /obj/projectile/magic/animate,
+ /obj/projectile/magic/teleport,
+ /obj/projectile/magic/door,
+ /obj/projectile/magic/fireball,
+ /obj/projectile/magic/spellblade,
+ /obj/projectile/magic/arcane_barrage,
+ ) //thanks Lett1
/mob/living/simple_animal/hostile/carp/ranged/chaos/xenobiology
gold_core_spawnable = HOSTILE_SPAWN
- allowed_projectile_types = list(/obj/projectile/magic/animate, /obj/projectile/magic/teleport,
- /obj/projectile/magic/door, /obj/projectile/magic/fireball, /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage)
+ allowed_projectile_types = list(
+ /obj/projectile/magic/animate,
+ /obj/projectile/magic/teleport,
+ /obj/projectile/magic/door,
+ /obj/projectile/magic/fireball,
+ /obj/projectile/magic/spellblade,
+ /obj/projectile/magic/arcane_barrage,
+ )
diff --git a/code/modules/mapfluff/ruins/icemoonruin_code/hotsprings.dm b/code/modules/mapfluff/ruins/icemoonruin_code/hotsprings.dm
index cea4dcc0fd7..9e188d45856 100644
--- a/code/modules/mapfluff/ruins/icemoonruin_code/hotsprings.dm
+++ b/code/modules/mapfluff/ruins/icemoonruin_code/hotsprings.dm
@@ -19,28 +19,23 @@
. = ..()
if(!isliving(arrived))
return
- var/mob/living/L = arrived
- if(!L.client || L.incorporeal_move || !L.mind)
+ var/mob/living/to_transform = arrived
+ var/datum/mind/transforming_mind = to_transform.mind
+ if(!to_transform.client || to_transform.incorporeal_move || !transforming_mind)
return
- if(HAS_TRAIT(L.mind, TRAIT_HOT_SPRING_CURSED)) // no double dipping
+ if(HAS_TRAIT(transforming_mind, TRAIT_HOT_SPRING_CURSED)) // no double dipping
return
- ADD_TRAIT(L.mind, TRAIT_HOT_SPRING_CURSED, TRAIT_GENERIC)
- var/random_choice = pick("Mob", "Appearance")
- switch(random_choice)
- if("Mob")
- L = L.wabbajack("animal")
- if("Appearance")
- var/mob/living/carbon/human/H = L.wabbajack("humanoid")
- randomize_human(H)
- var/list/all_species = list()
- for(var/stype in subtypesof(/datum/species))
- var/datum/species/S = stype
- if(initial(S.changesource_flags) & RACE_SWAP)
- all_species += stype
- var/random_race = pick(all_species)
- H.set_species(random_race)
- L = H
- var/turf/T = find_safe_turf(extended_safety_checks = TRUE, dense_atoms = FALSE)
- L.forceMove(T)
- to_chat(L, span_notice("You blink and find yourself in [get_area_name(T)]."))
+ ADD_TRAIT(transforming_mind, TRAIT_HOT_SPRING_CURSED, TRAIT_GENERIC)
+ var/mob/living/transformed_mob = to_transform.wabbajack(pick(WABBAJACK_HUMAN, WABBAJACK_ANIMAL), change_flags = RACE_SWAP)
+ if(!transformed_mob)
+ // Wabbajack failed, maybe the mob had godmode or something.
+ if(!QDELETED(to_transform))
+ to_chat(to_transform, span_notice("The water seems to have no effect on you."))
+ // because it failed, let's allow them to try again in a lil' bit
+ addtimer(TRAIT_CALLBACK_REMOVE(transforming_mind, TRAIT_HOT_SPRING_CURSED, TRAIT_GENERIC), 10 SECONDS)
+ return
+
+ var/turf/return_turf = find_safe_turf(extended_safety_checks = TRUE, dense_atoms = FALSE)
+ transformed_mob.forceMove(return_turf)
+ to_chat(transformed_mob, span_notice("You blink and find yourself in [get_area_name(return_turf)]."))
diff --git a/code/modules/mining/lavaland/tendril_loot.dm b/code/modules/mining/lavaland/tendril_loot.dm
index e1f2bf90eaa..6fcab569bee 100644
--- a/code/modules/mining/lavaland/tendril_loot.dm
+++ b/code/modules/mining/lavaland/tendril_loot.dm
@@ -168,7 +168,7 @@
/obj/item/clothing/neck/necklace/memento_mori/proc/check_health(mob/living/source)
SIGNAL_HANDLER
- var/list/guardians = source.hasparasites()
+ var/list/guardians = source.get_all_linked_holoparasites()
if(!length(guardians))
return
if(source.health <= HEALTH_THRESHOLD_DEAD)
diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm
index f639b8b83be..b5974b81cf3 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -56,9 +56,6 @@
vampire.blood_volume -= 0.125 * delta_time
if(vampire.blood_volume <= BLOOD_VOLUME_SURVIVE)
to_chat(vampire, span_danger("You ran out of blood!"))
- var/obj/shapeshift_holder/holder = locate() in vampire
- if(holder)
- holder.shape.dust() //vampires do not have batform anymore, but this would still lead to very weird stuff with other shapeshift holders
vampire.dust()
var/area/A = get_area(vampire)
if(istype(A, /area/station/service/chapel))
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 1a0080b27e7..0cc307c58bc 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1193,18 +1193,22 @@
. = ..()
// Used in polymorph code to shapeshift mobs into other creatures
-/mob/living/proc/wabbajack(randomize)
- // If the mob has a shapeshifted form, we want to pull out the reference of the caster's original body from it.
- // We then want to restore this original body through the shapeshift holder itself.
- var/obj/shapeshift_holder/shapeshift = locate() in src
- if(shapeshift)
- shapeshift.restore()
- if(shapeshift.stored != src) // To reduce the risk of an infinite loop.
- return shapeshift.stored.wabbajack(randomize)
-
+/**
+ * Polymorphs our mob into another mob.
+ * If successful, our current mob is qdeleted!
+ *
+ * what_to_randomize - what are we randomizing the mob into? See the defines for valid options.
+ * change_flags - only used for humanoid randomization (currently), what pool of changeflags should we draw from?
+ *
+ * Returns a mob (what our mob turned into) or null (if we failed).
+ */
+/mob/living/proc/wabbajack(what_to_randomize, change_flags = WABBAJACK)
if(stat == DEAD || notransform || (GODMODE & status_flags))
return
+ if(SEND_SIGNAL(src, COMSIG_LIVING_PRE_WABBAJACKED, what_to_randomize) & STOP_WABBAJACK)
+ return
+
notransform = TRUE
ADD_TRAIT(src, TRAIT_IMMOBILIZED, MAGIC_TRAIT)
ADD_TRAIT(src, TRAIT_HANDS_BLOCKED, MAGIC_TRAIT)
@@ -1231,51 +1235,64 @@
var/mob/living/new_mob
- if(!randomize)
- randomize = pick("monkey","robot","slime","xeno","humanoid","animal")
- switch(randomize)
- if("monkey")
+ var/static/list/possible_results = list(
+ WABBAJACK_MONKEY,
+ WABBAJACK_ROBOT,
+ WABBAJACK_SLIME,
+ WABBAJACK_XENO,
+ WABBAJACK_HUMAN,
+ WABBAJACK_ANIMAL,
+ )
+
+ // If we weren't passed one, pick a default one
+ what_to_randomize ||= pick(possible_results)
+
+ switch(what_to_randomize)
+ if(WABBAJACK_MONKEY)
new_mob = new /mob/living/carbon/human/species/monkey(loc)
- if("robot")
- var/robot = pick(
- 200 ; /mob/living/silicon/robot,
- /mob/living/silicon/robot/model/syndicate,
- /mob/living/silicon/robot/model/syndicate/medical,
- /mob/living/silicon/robot/model/syndicate/saboteur,
- 200 ; /mob/living/simple_animal/drone/polymorphed,
+ if(WABBAJACK_ROBOT)
+ var/static/list/robot_options = list(
+ /mob/living/silicon/robot = 200,
+ /mob/living/simple_animal/drone/polymorphed = 200,
+ /mob/living/silicon/robot/model/syndicate = 1,
+ /mob/living/silicon/robot/model/syndicate/medical = 1,
+ /mob/living/silicon/robot/model/syndicate/saboteur = 1,
)
- new_mob = new robot(loc)
+
+ var/picked_robot = pick(robot_options)
+ new_mob = new picked_robot(loc)
if(issilicon(new_mob))
+ var/mob/living/silicon/robot/created_robot = new_mob
new_mob.gender = gender
new_mob.invisibility = 0
new_mob.job = JOB_CYBORG
- var/mob/living/silicon/robot/Robot = new_mob
- Robot.lawupdate = FALSE
- Robot.connected_ai = null
- Robot.mmi.transfer_identity(src) //Does not transfer key/client.
- Robot.clear_inherent_laws(announce = FALSE)
- Robot.clear_zeroth_law(announce = FALSE)
+ created_robot.lawupdate = FALSE
+ created_robot.connected_ai = null
+ created_robot.mmi.transfer_identity(src) //Does not transfer key/client.
+ created_robot.clear_inherent_laws(announce = FALSE)
+ created_robot.clear_zeroth_law(announce = FALSE)
- if("slime")
+ if(WABBAJACK_SLIME)
new_mob = new /mob/living/simple_animal/slime/random(loc)
- if("xeno")
- var/xeno_type
+ if(WABBAJACK_XENO)
+ var/picked_xeno_type
+
if(ckey)
- xeno_type = pick(
+ picked_xeno_type = pick(
/mob/living/carbon/alien/humanoid/hunter,
/mob/living/carbon/alien/humanoid/sentinel,
)
else
- xeno_type = pick(
+ picked_xeno_type = pick(
/mob/living/carbon/alien/humanoid/hunter,
/mob/living/simple_animal/hostile/alien/sentinel,
)
- new_mob = new xeno_type(loc)
+ new_mob = new picked_xeno_type(loc)
- if("animal")
- var/path = pick(
+ if(WABBAJACK_ANIMAL)
+ var/picked_animal = pick(
/mob/living/simple_animal/hostile/carp,
/mob/living/simple_animal/hostile/bear,
/mob/living/simple_animal/hostile/mushroom,
@@ -1311,17 +1328,17 @@
/mob/living/simple_animal/pet/dog/breaddog,
/mob/living/simple_animal/chick,
)
- new_mob = new path(loc)
+ new_mob = new picked_animal(loc)
- if("humanoid")
- var/mob/living/carbon/human/new_human = new (loc)
+ if(WABBAJACK_HUMAN)
+ var/mob/living/carbon/human/new_human = new(loc)
+ // 50% chance that we'll also randomice race
if(prob(50))
var/list/chooseable_races = list()
- for(var/speciestype in subtypesof(/datum/species))
- var/datum/species/S = speciestype
- if(initial(S.changesource_flags) & WABBAJACK)
- chooseable_races += speciestype
+ for(var/datum/species/species_type as anything in subtypesof(/datum/species))
+ if(initial(species_type.changesource_flags) & change_flags)
+ chooseable_races += species_type
if(length(chooseable_races))
new_human.set_species(pick(chooseable_races))
@@ -1332,45 +1349,49 @@
new_human.dna.update_dna_identity()
new_mob = new_human
+ else
+ stack_trace("wabbajack() was called without an invalid randomization choice. ([what_to_randomize])")
+
if(!new_mob)
return
+ to_chat(src, span_hypnophrase(span_big("Your form morphs into that of a [what_to_randomize]!")))
+
+ // And of course, make sure they get policy for being transformed
+ var/poly_msg = get_policy(POLICY_POLYMORPH)
+ if(poly_msg)
+ to_chat(src, poly_msg)
+
// Some forms can still wear some items
for(var/obj/item/item as anything in item_contents)
new_mob.equip_to_appropriate_slot(item)
- log_message("became [new_mob.name]([new_mob.type])", LOG_ATTACK, color="orange")
+ // I don't actually know why we do this
new_mob.set_combat_mode(TRUE)
- wabbajack_act(new_mob)
- to_chat(new_mob, span_warning("Your form morphs into that of a [randomize]."))
- var/poly_msg = get_policy(POLICY_POLYMORPH)
- if(poly_msg)
- to_chat(new_mob, poly_msg)
+ // on_wabbajack is where we handle setting up the name,
+ // transfering the mind and observerse, and other miscellaneous
+ // actions that should be done before we delete the original mob.
+ on_wabbajacked(new_mob)
- transfer_observers_to(new_mob)
-
- . = new_mob
qdel(src)
+ return new_mob
// Called when we are hit by a bolt of polymorph and changed
// Generally the mob we are currently in is about to be deleted
-/mob/living/proc/wabbajack_act(mob/living/new_mob)
- SEND_SIGNAL(src, COMSIG_LIVING_WABBAJACKED, new_mob)
- log_message("was wabbajack polymorphed into: [new_mob.name]([new_mob.type]).", LOG_GAME)
+/mob/living/proc/on_wabbajacked(mob/living/new_mob)
+ log_message("became [new_mob.name] ([new_mob.type])", LOG_ATTACK, color = "orange")
+ SEND_SIGNAL(src, COMSIG_LIVING_ON_WABBAJACKED, new_mob)
new_mob.name = real_name
new_mob.real_name = real_name
+ // Transfer mind to the new mob (also handles actions and observers and stuff)
if(mind)
mind.transfer_to(new_mob)
- else
- new_mob.key = key
- for(var/para in hasparasites())
- var/mob/living/simple_animal/hostile/guardian/G = para
- G.summoner = new_mob
- G.Recall()
- to_chat(G, span_holoparasite("Your summoner has changed form!"))
+ // Well, no mmind, guess we should try to move a key over
+ else if(key)
+ new_mob.key = key
/mob/living/proc/unfry_mob() //Callback proc to tone down spam from multiple sizzling frying oil dipping.
REMOVE_TRAIT(src, TRAIT_OIL_FRIED, "cooking_oil_react")
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index 49cf084f5ce..a4888d77043 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm
@@ -68,7 +68,43 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
GLOB.parasites += src
updatetheme(theme)
AddElement(/datum/element/simple_flying)
- . = ..()
+ return ..()
+
+/// Setter for our summoner mob.
+/mob/living/simple_animal/hostile/guardian/proc/set_summoner(mob/to_who)
+ if(summoner)
+ UnregisterSignal(summoner, list(COMSIG_LIVING_ON_WABBAJACKED, COMSIG_LIVING_SHAPESHIFTED, COMSIG_LIVING_UNSHAPESHIFTED))
+
+ summoner = to_who
+ Recall(TRUE)
+ RegisterSignal(to_who, COMSIG_LIVING_ON_WABBAJACKED, .proc/on_owner_wabbajacked)
+ RegisterSignal(to_who, COMSIG_LIVING_SHAPESHIFTED, .proc/on_owner_shapeshifted)
+ RegisterSignal(to_who, COMSIG_LIVING_UNSHAPESHIFTED, .proc/on_owner_unshapeshifted)
+
+/// Signal proc for [COMSIG_LIVING_ON_WABBAJACKED], when our summoner is wabbajacked we should be alerted.
+/mob/living/simple_animal/hostile/guardian/proc/on_owner_wabbajacked(mob/living/source, mob/living/new_mob)
+ SIGNAL_HANDLER
+
+ set_summoner(new_mob)
+ to_chat(src, span_holoparasite("Your summoner has changed form!"))
+
+/// Signal proc for [COMSIG_LIVING_SHAPESHIFTED], when our summoner is shapeshifted we should change to the new mob
+/mob/living/simple_animal/hostile/guardian/proc/on_owner_shapeshifted(mob/living/source, mob/living/new_shape)
+ SIGNAL_HANDLER
+
+ set_summoner(new_shape)
+ to_chat(src, span_holoparasite("Your summoner has shapeshifted into that of a [new_shape]!"))
+
+/// Signal proc for [COMSIG_LIVING_UNSHAPESHIFTED], when our summoner unshapeshifts go back to that mob
+/mob/living/simple_animal/hostile/guardian/proc/on_owner_unshapeshifted(mob/living/source, mob/living/old_summoner)
+ SIGNAL_HANDLER
+
+ set_summoner(old_summoner)
+ to_chat(src, span_holoparasite("Your summoner has shapeshifted back into their normal form!"))
+
+// Ha, no
+/mob/living/simple_animal/hostile/guardian/wabbajack(what_to_randomize, change_flags = WABBAJACK)
+ visible_message(span_warning("[src] resists the polymorph!"))
/mob/living/simple_animal/hostile/guardian/med_hud_set_health()
if(summoner)
@@ -401,7 +437,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
var/my_message = "[src]: [preliminary_message]" //add source, color source with the guardian's color
to_chat(summoner, "[my_message]")
- var/list/guardians = summoner.hasparasites()
+ var/list/guardians = summoner.get_all_linked_holoparasites()
for(var/para in guardians)
to_chat(para, "[my_message]")
for(var/M in GLOB.dead_mob_list)
@@ -422,7 +458,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
var/my_message = "[src]: [preliminary_message]" //add source, color source with default grey...
to_chat(src, "[my_message]")
- var/list/guardians = hasparasites()
+ var/list/guardians = get_all_linked_holoparasites()
for(var/para in guardians)
var/mob/living/simple_animal/hostile/guardian/G = para
to_chat(G, "[src]: [preliminary_message]" )
@@ -438,7 +474,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
set name = "Recall Guardian"
set category = "Guardian"
set desc = "Forcibly recall your guardian."
- var/list/guardians = hasparasites()
+ var/list/guardians = get_all_linked_holoparasites()
for(var/para in guardians)
var/mob/living/simple_animal/hostile/guardian/G = para
G.Recall()
@@ -448,7 +484,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
set category = "Guardian"
set desc = "Re-rolls which ghost will control your Guardian. One use per Guardian."
- var/list/guardians = hasparasites()
+ var/list/guardians = get_all_linked_holoparasites()
for(var/para in guardians)
var/mob/living/simple_animal/hostile/guardian/P = para
if(P.reset)
@@ -492,16 +528,19 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
////////parasite tracking/finding procs
-/mob/living/proc/hasparasites() //returns a list of guardians the mob is a summoner for
- . = list()
- for(var/P in GLOB.parasites)
- var/mob/living/simple_animal/hostile/guardian/G = P
- if(G.summoner == src)
- . += G
-
-/mob/living/simple_animal/hostile/guardian/proc/hasmatchingsummoner(mob/living/simple_animal/hostile/guardian/G) //returns 1 if the summoner matches the target's summoner
- return (istype(G) && G.summoner == summoner)
+/// Returns a list of all holoparasites that has this mob as a summoner.
+/mob/living/proc/get_all_linked_holoparasites()
+ RETURN_TYPE(/list)
+ var/list/all_parasites = list()
+ for(var/mob/living/simple_animal/hostile/guardian/stand as anything in GLOB.parasites)
+ if(stand.summoner != src)
+ continue
+ all_parasites += stand
+ return all_parasites
+/// Returns true if this holoparasite has the same summoner as the passed holoparasite.
+/mob/living/simple_animal/hostile/guardian/proc/hasmatchingsummoner(mob/living/simple_animal/hostile/guardian/other_guardian)
+ return istype(other_guardian) && other_guardian.summoner == summoner
////////Creation
@@ -528,7 +567,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
if(isguardian(user) && !allowguardian)
to_chat(user, span_holoparasite("[mob_name] chains are not allowed."))
return
- var/list/guardians = user.hasparasites()
+ var/list/guardians = user.get_all_linked_holoparasites()
if(length(guardians) && !allowmultiple)
to_chat(user, span_holoparasite("You already have a [mob_name]!"))
return
@@ -596,14 +635,14 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
if("Gravitokinetic")
pickedtype = /mob/living/simple_animal/hostile/guardian/gravitokinetic
- var/list/guardians = user.hasparasites()
+ var/list/guardians = user.get_all_linked_holoparasites()
if(length(guardians) && !allowmultiple)
to_chat(user, span_holoparasite("You already have a [mob_name]!") )
used = FALSE
return
var/mob/living/simple_animal/hostile/guardian/G = new pickedtype(user, theme)
G.name = mob_name
- G.summoner = user
+ G.set_summoner(user)
G.key = candidate.key
G.mind.enslave_mind_to_creator(user)
G.copy_languages(user, LANGUAGE_MASTER) // make sure holoparasites speak same language as master
diff --git a/code/modules/mob/living/simple_animal/guardian/types/ranged.dm b/code/modules/mob/living/simple_animal/guardian/types/ranged.dm
index 1a481faa2b7..18e1d98ca50 100644
--- a/code/modules/mob/living/simple_animal/guardian/types/ranged.dm
+++ b/code/modules/mob/living/simple_animal/guardian/types/ranged.dm
@@ -122,7 +122,7 @@
SIGNAL_HANDLER
if(isliving(AM) && spawner && spawner.summoner && AM != spawner && !spawner.hasmatchingsummoner(AM))
to_chat(spawner.summoner, "[span_danger("[AM] has crossed surveillance snare, [name].")]")
- var/list/guardians = spawner.summoner.hasparasites()
+ var/list/guardians = spawner.summoner.get_all_linked_holoparasites()
for(var/para in guardians)
to_chat(para, "[span_danger("[AM] has crossed surveillance snare, [name].")]")
diff --git a/code/modules/mob/living/simple_animal/hostile/mimic.dm b/code/modules/mob/living/simple_animal/hostile/mimic.dm
index b4a1b5ffc5b..38b6b880182 100644
--- a/code/modules/mob/living/simple_animal/hostile/mimic.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mimic.dm
@@ -133,7 +133,7 @@ GLOBAL_LIST_INIT(mimic_blacklist, list(/obj/structure/table, /obj/structure/cabl
. = ..()
return . - creator
-/mob/living/simple_animal/hostile/mimic/copy/wabbajack()
+/mob/living/simple_animal/hostile/mimic/copy/wabbajack(what_to_randomize, change_flags = WABBAJACK)
visible_message(span_warning("[src] resists polymorphing into a new creature!"))
/mob/living/simple_animal/hostile/mimic/copy/proc/ChangeOwner(mob/owner)
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
index cefc59bd283..1537eed928a 100644
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
@@ -170,8 +170,7 @@
fire_stream()
/mob/living/simple_animal/hostile/space_dragon/death(gibbed)
- empty_contents()
- ..()
+ . = ..()
add_dragon_overlay()
UnregisterSignal(small_sprite, COMSIG_ACTION_TRIGGER)
@@ -180,10 +179,6 @@
add_dragon_overlay()
RegisterSignal(small_sprite, COMSIG_ACTION_TRIGGER, .proc/add_dragon_overlay)
-/mob/living/simple_animal/hostile/space_dragon/wabbajack_act(mob/living/new_mob)
- empty_contents()
- . = ..()
-
/**
* Allows space dragon to choose its own name.
*
@@ -334,18 +329,6 @@
return TRUE
return FALSE
-/**
- * Disperses the contents of the mob on the surrounding tiles.
- *
- * Randomly places the contents of the mob onto surrounding tiles.
- * Has a 10% chance to place on the same tile as the mob.
- */
-/mob/living/simple_animal/hostile/space_dragon/proc/empty_contents()
- for(var/atom/movable/AM in src)
- AM.forceMove(loc)
- if(prob(90))
- step(AM, pick(GLOB.alldirs))
-
/**
* Resets Space Dragon's status after using wing gust.
*
diff --git a/code/modules/pai/shell.dm b/code/modules/pai/shell.dm
index 2aada5132a4..3bc8e3ac03a 100644
--- a/code/modules/pai/shell.dm
+++ b/code/modules/pai/shell.dm
@@ -17,7 +17,7 @@
if(loc != card)
visible_message(span_notice("[src] [resting? "lays down for a moment..." : "perks up from the ground."]"))
-/mob/living/silicon/pai/wabbajack()
+/mob/living/silicon/pai/wabbajack(what_to_randomize, change_flags = WABBAJACK)
if(length(possible_chassis) < 2)
return FALSE
var/holochassis = pick(possible_chassis - chassis)
diff --git a/code/modules/projectiles/ammunition/special/magic.dm b/code/modules/projectiles/ammunition/special/magic.dm
index d690bddbf2c..423f1ab6148 100644
--- a/code/modules/projectiles/ammunition/special/magic.dm
+++ b/code/modules/projectiles/ammunition/special/magic.dm
@@ -9,6 +9,19 @@
/obj/item/ammo_casing/magic/change
projectile_type = /obj/projectile/magic/change
+/obj/item/ammo_casing/magic/change/ready_proj(atom/target, mob/living/user, quiet, zone_override = "", atom/fired_from)
+ if (!loaded_projectile)
+ return
+
+ // If we were fired by a Staff of Change, we can try to inherent their preset vars for our wabbajack
+ var/obj/item/gun/magic/staff/change/change_staff = fired_from
+ var/obj/projectile/magic/change/change_projectile = loaded_projectile
+ if(istype(change_staff) && istype(change_projectile))
+ change_projectile.set_wabbajack_effect = change_staff.preset_wabbajack_type
+ change_projectile.set_wabbajack_changeflags = change_staff.preset_wabbajack_changeflag
+
+ return ..()
+
/obj/item/ammo_casing/magic/animate
projectile_type = /obj/projectile/magic/animate
diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm
index 6046d770116..0d2801ce238 100644
--- a/code/modules/projectiles/guns/magic/staff.dm
+++ b/code/modules/projectiles/guns/magic/staff.dm
@@ -6,6 +6,8 @@
lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi'
item_flags = NEEDS_PERMIT | NO_MAT_REDEMPTION
+ /// Can non-magic folk use our staff?
+ /// If FALSE, only wizards or survivalists can use the staff to its full potential - If TRUE, anyone can
var/allow_intruder_use = FALSE
/obj/item/gun/magic/staff/proc/is_wizard_or_friend(mob/user)
@@ -34,6 +36,10 @@
icon_state = "staffofchange"
inhand_icon_state = "staffofchange"
school = SCHOOL_TRANSMUTATION
+ /// If set, all wabbajacks this staff produces will be of this type, instead of random
+ var/preset_wabbajack_type
+ /// If set, all wabbajacks this staff produces will be of this changeflag, instead of only WABBAJACK
+ var/preset_wabbajack_changeflag
/obj/item/gun/magic/staff/change/unrestricted
allow_intruder_use = TRUE
@@ -46,8 +52,11 @@
/obj/item/gun/magic/staff/change/on_intruder_use(mob/living/user, atom/target)
user.dropItemToGround(src, TRUE)
- var/randomize = pick("monkey","humanoid","animal")
- var/mob/new_body = user.wabbajack(randomize)
+ var/wabbajack_into = preset_wabbajack_type || pick(WABBAJACK_MONKEY, WABBAJACK_HUMAN, WABBAJACK_ANIMAL)
+ var/mob/living/new_body = user.wabbajack(wabbajack_into, preset_wabbajack_changeflag)
+ if(!new_body)
+ return
+
balloon_alert(new_body, "wabbajack, wabbajack!")
/obj/item/gun/magic/staff/animate
@@ -67,6 +76,7 @@
icon_state = "staffofhealing"
inhand_icon_state = "staffofhealing"
school = SCHOOL_RESTORATION
+ /// Our internal healbeam, used if an intruder (non-magic person) tries to use our staff
var/obj/item/gun/medbeam/healing_beam
/obj/item/gun/magic/staff/healing/pickup(mob/user)
@@ -81,7 +91,7 @@
healing_beam.mounted = TRUE
/obj/item/gun/magic/staff/healing/Destroy()
- qdel(healing_beam)
+ QDEL_NULL(healing_beam)
return ..()
/obj/item/gun/magic/staff/healing/unrestricted
@@ -111,18 +121,36 @@
recharge_rate = 2
no_den_usage = 1
school = SCHOOL_FORBIDDEN //this staff is evil. okay? it just is. look at this projectile type list. this is wrong.
- var/allowed_projectile_types = list(/obj/projectile/magic/change, /obj/projectile/magic/animate, /obj/projectile/magic/resurrection,
- /obj/projectile/magic/death, /obj/projectile/magic/teleport, /obj/projectile/magic/door, /obj/projectile/magic/fireball,
- /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage, /obj/projectile/magic/locker, /obj/projectile/magic/flying,
- /obj/projectile/magic/bounty, /obj/projectile/magic/antimagic, /obj/projectile/magic/fetch, /obj/projectile/magic/sapping,
- /obj/projectile/magic/necropotence, /obj/projectile/magic, /obj/projectile/temp/chill, /obj/projectile/magic/wipe)
+
+ /// Static list of all projectiles we can fire from our staff.
+ /// Doesn't contain all subtypes of magic projectiles, unlike what it looks like
+ var/static/list/allowed_projectile_types = list(
+ /obj/projectile/magic/animate,
+ /obj/projectile/magic/antimagic,
+ /obj/projectile/magic/arcane_barrage,
+ /obj/projectile/magic/bounty,
+ /obj/projectile/magic/change,
+ /obj/projectile/magic/death,
+ /obj/projectile/magic/door,
+ /obj/projectile/magic/fetch,
+ /obj/projectile/magic/fireball,
+ /obj/projectile/magic/flying,
+ /obj/projectile/magic/locker,
+ /obj/projectile/magic/necropotence,
+ /obj/projectile/magic/resurrection,
+ /obj/projectile/magic/sapping,
+ /obj/projectile/magic/spellblade,
+ /obj/projectile/magic/teleport,
+ /obj/projectile/magic/wipe,
+ /obj/projectile/temp/chill,
+ )
/obj/item/gun/magic/staff/chaos/unrestricted
allow_intruder_use = TRUE
/obj/item/gun/magic/staff/chaos/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0)
chambered.projectile_type = pick(allowed_projectile_types)
- . = ..()
+ return ..()
/obj/item/gun/magic/staff/chaos/on_intruder_use(mob/living/user)
if(!user.can_cast_magic()) // Don't let people with antimagic use the staff of chaos.
@@ -179,10 +207,10 @@
/obj/item/gun/magic/staff/spellblade/Initialize(mapload)
. = ..()
AddComponent(/datum/component/butchering, \
- speed = 1.5 SECONDS, \
- effectiveness = 125, \
- bonus_modifier = 0, \
- butcher_sound = hitsound, \
+ speed = 1.5 SECONDS, \
+ effectiveness = 125, \
+ bonus_modifier = 0, \
+ butcher_sound = hitsound, \
)
/obj/item/gun/magic/staff/spellblade/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm
index 757c89b994b..9d781f89590 100644
--- a/code/modules/projectiles/guns/magic/wand.dm
+++ b/code/modules/projectiles/guns/magic/wand.dm
@@ -147,7 +147,7 @@
max_charges = 10 //10, 5, 5, 4
/obj/item/gun/magic/wand/polymorph/zap_self(mob/living/user)
- ..() //because the user mob ceases to exists by the time wabbajack fully resolves
+ . = ..() //because the user mob ceases to exists by the time wabbajack fully resolves
user.wabbajack()
charges--
diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm
index 6fa2a2243c4..0de2b8234b1 100644
--- a/code/modules/projectiles/projectile/magic.dm
+++ b/code/modules/projectiles/projectile/magic.dm
@@ -162,13 +162,17 @@
damage = 0
damage_type = BURN
nodamage = TRUE
+ /// If set, this projectile will only do a certain wabbajack effect
+ var/set_wabbajack_effect
+ /// If set, this projectile will only pass certain changeflags to wabbajack
+ var/set_wabbajack_changeflags
/obj/projectile/magic/change/on_hit(atom/target)
. = ..()
if(isliving(target))
var/mob/living/victim = target
- victim.wabbajack()
+ victim.wabbajack(set_wabbajack_effect, set_wabbajack_changeflags)
if(istype(target, /obj/machinery/hydroponics))
var/obj/machinery/hydroponics/plant_tray = target
diff --git a/code/modules/research/xenobiology/crossbreeding/_mobs.dm b/code/modules/research/xenobiology/crossbreeding/_mobs.dm
index 1f893371d8b..cbaabdf35d5 100644
--- a/code/modules/research/xenobiology/crossbreeding/_mobs.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_mobs.dm
@@ -21,7 +21,7 @@ Slimecrossing Mobs
/// If TRUE, we self-delete (remove ourselves) the next time we turn back into a human
var/remove_on_restore = FALSE
-/datum/action/cooldown/spell/shapeshift/slime_form/restore_form(mob/living/shape)
+/datum/action/cooldown/spell/shapeshift/slime_form/do_unshapeshift(mob/living/caster)
. = ..()
if(!.)
return
diff --git a/code/modules/spells/spell_types/pointed/mind_transfer.dm b/code/modules/spells/spell_types/pointed/mind_transfer.dm
index bf31d402a4c..59d44a1933a 100644
--- a/code/modules/spells/spell_types/pointed/mind_transfer.dm
+++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm
@@ -19,6 +19,10 @@
/// If TRUE, we cannot mindswap into mobs with minds if they do not currently have a key / player.
var/target_requires_key = TRUE
+ /// If TRUE, we cannot mindswap into people without a mind.
+ /// You may be wondering "What's the point of mindswap if the target has no mind"?
+ /// Primarily for debugging - targets hit with this set to FALSE will init a mind, then do the swap.
+ var/target_requires_mind = TRUE
/// For how long is the caster stunned for after the spell
var/unconscious_amount_caster = 40 SECONDS
/// For how long is the victim stunned for after the spell
@@ -64,7 +68,7 @@
if(living_target.stat == DEAD)
to_chat(owner, span_warning("You don't particularly want to be dead!"))
return FALSE
- if(!living_target.mind)
+ if(!living_target.mind && target_requires_mind)
to_chat(owner, span_warning("[living_target.p_theyve(TRUE)] doesn't appear to have a mind to swap into!"))
return FALSE
if(!living_target.key && target_requires_key)
@@ -86,6 +90,10 @@
if(stand.summoner)
to_swap = stand.summoner
+ // Gives the target a mind if we don't require one and they don't have one
+ if(!to_swap.mind && !target_requires_mind)
+ to_swap.mind_initialize()
+
var/datum/mind/mind_to_swap = to_swap.mind
if(to_swap.can_block_magic(antimagic_flags) \
|| mind_to_swap.has_antag_datum(/datum/antagonist/wizard) \
diff --git a/code/modules/spells/spell_types/shapeshift/_shape_status.dm b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
new file mode 100644
index 00000000000..b95abb47bce
--- /dev/null
+++ b/code/modules/spells/spell_types/shapeshift/_shape_status.dm
@@ -0,0 +1,236 @@
+// A status effect for having a mob temporarily (usually) change form into that of another mob.
+// When the status effect is removed, the mob is reverted back to their human form in most cases.
+// If you want a more permanent polymorph, see [/proc/wabbajack].
+/datum/status_effect/shapechange_mob
+ id = "shapechange_mob"
+ alert_type = /atom/movable/screen/alert/status_effect/shapeshifted
+ // When the mob's deleted on_remove() will handle our references
+ on_remove_on_mob_delete = TRUE
+
+ /// The caster's mob. Who has transformed into us
+ /// This reference is handled in [/proc/restore_caster], which is always called if we delete
+ var/mob/living/caster_mob
+ /// Whether we're currently undoing the change
+ var/already_restored = FALSE
+
+/datum/status_effect/shapechange_mob/on_creation(mob/living/new_owner, mob/living/caster)
+ // If any type or subtype of shapeshift mob is on the new_owner already throw an error and self-delete
+ if(locate(type) in new_owner.status_effects)
+ stack_trace("Mob shapechange status effect applied to a mob which already was shapechanged, which will definitely cause issues.")
+ qdel(src)
+ return
+
+ // No caster mob, no one to put in the mob. Self-delete
+ if(!istype(caster))
+ stack_trace("Mob shapechange status effect applied without a proper caster.")
+ qdel(src)
+ return
+
+ src.caster_mob = caster
+ return ..()
+
+/datum/status_effect/shapechange_mob/on_apply()
+ caster_mob.mind?.transfer_to(owner)
+ caster_mob.forceMove(owner)
+ caster_mob.notransform = TRUE
+ caster_mob.apply_status_effect(/datum/status_effect/grouped/stasis, STASIS_SHAPECHANGE_EFFECT)
+
+ RegisterSignal(owner, COMSIG_LIVING_PRE_WABBAJACKED, .proc/on_wabbajacked)
+ RegisterSignal(owner, COMSIG_LIVING_DEATH, .proc/on_shape_death)
+ RegisterSignal(caster_mob, COMSIG_LIVING_DEATH, .proc/on_caster_death)
+ RegisterSignal(caster_mob, COMSIG_PARENT_QDELETING, .proc/on_caster_deleted)
+
+ SEND_SIGNAL(caster_mob, COMSIG_LIVING_SHAPESHIFTED, owner)
+ return TRUE
+
+/datum/status_effect/shapechange_mob/on_remove()
+ // If the owner was straight up deleted we shouldn't restore anyone
+ // (the caster's stored in the owner's contents so if it's qdel'd so is the caster)
+ if(!QDELETED(owner))
+ restore_caster()
+
+ // Either restore_caster() or other signals should have handled this reference by now,
+ // but juuust in case make sure nothing sticks around.
+ caster_mob = null
+
+/// Signal proc for [COMSIG_LIVING_PRE_WABBAJACKED] to prevent us from being Wabbajacked and messed up.
+/datum/status_effect/shapechange_mob/proc/on_wabbajacked(mob/living/source, randomized)
+ SIGNAL_HANDLER
+
+ var/mob/living/revealed_mob = caster_mob
+ source.visible_message(span_warning("[revealed_mob] gets pulled back to their normal form!"))
+ restore_caster()
+ revealed_mob.Paralyze(10 SECONDS, ignore_canstun = TRUE)
+ return STOP_WABBAJACK
+
+/// Restores the caster back to their human form.
+/// if kill_caster_after is TRUE, the caster will have death() called on them after restoring.
+/datum/status_effect/shapechange_mob/proc/restore_caster(kill_caster_after = FALSE)
+ if(already_restored || !caster_mob)
+ return
+
+ if(QDELETED(owner))
+ CRASH("Mob shapechange effect called restore_caster while the owner was qdeleted, this shouldn't happen.")
+
+ already_restored = TRUE
+ UnregisterSignal(owner, list(COMSIG_LIVING_PRE_WABBAJACKED, COMSIG_LIVING_DEATH))
+ UnregisterSignal(caster_mob, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
+
+ caster_mob.forceMove(owner.loc)
+ caster_mob.notransform = FALSE
+ caster_mob.remove_status_effect(/datum/status_effect/grouped/stasis, STASIS_SHAPECHANGE_EFFECT)
+ owner.mind?.transfer_to(caster_mob)
+
+ if(kill_caster_after)
+ caster_mob.death()
+
+ after_unchange()
+ caster_mob = null
+
+ // Destroy the owner after all's said and done, this will also destroy our status effect (src)
+ // retore_caster() should never reach this point while either the owner or the effect is being qdeleted
+ qdel(owner)
+
+/// Effects done after the casting mob has reverted to their human form.
+/datum/status_effect/shapechange_mob/proc/after_unchange()
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(owner, COMSIG_LIVING_UNSHAPESHIFTED, caster_mob)
+
+/// Signal proc for [COMSIG_LIVING_DEATH] from our owner.
+/// If our owner mob is killed, we should revert back to normal.
+/datum/status_effect/shapechange_mob/proc/on_shape_death(datum/source, gibbed)
+ SIGNAL_HANDLER
+
+ // gibbed = deleted = nothing to restore
+ if(gibbed)
+ return
+
+ restore_caster()
+
+/// Signal proc for [COMSIG_LIVING_DEATH] from our caster.
+/// If our internal caster is killed, kill our owner, too (which causes the above signal).
+/// This should very rarely end up being called but you never know
+/datum/status_effect/shapechange_mob/proc/on_caster_death(datum/source, gibbed)
+ SIGNAL_HANDLER
+
+ // Our caster inside was gibbed, mirror the gib to our mob
+ if(gibbed)
+ owner.gib()
+
+ // Otherwise our caster died, just make our mob die
+ else
+ owner.death()
+
+/// Signal proc for [COMSIG_PARENT_QDELETING] from our caster, delete us / our owner if we get deleted
+/datum/status_effect/shapechange_mob/proc/on_caster_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ caster_mob = null
+ if(QDELETED(owner))
+ return
+
+ qdel(owner)
+
+// A subtype for a shapechange sourced from a shapeshift spell.
+/datum/status_effect/shapechange_mob/from_spell
+ id = "shapechange_from_spell"
+ /// The shapechange spell that's caused our change
+ var/datum/weakref/source_weakref
+
+/datum/status_effect/shapechange_mob/from_spell/on_creation(mob/living/new_owner, mob/living/caster, datum/action/cooldown/spell/shapeshift/source_spell)
+ if(!istype(source_spell))
+ stack_trace("Mob shapechange \"from spell\" status effect applied without a source spell.")
+ qdel(src)
+ return
+
+ source_weakref = WEAKREF(source_spell)
+ return ..()
+
+/datum/status_effect/shapechange_mob/from_spell/on_apply()
+ var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref.resolve()
+ if(source_spell.owner == caster_mob)
+ // Assuming the spell is owned by the caster, give it over to the shapeshifted mob
+ // so they can actually transform back to their original form
+ source_spell.Grant(owner)
+
+ if(source_spell.convert_damage)
+ var/damage_to_apply = owner.maxHealth * ((caster_mob.maxHealth - caster_mob.health) / caster_mob.maxHealth)
+
+ owner.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND)
+ owner.blood_volume = caster_mob.blood_volume
+
+ for(var/datum/action/bodybound_action as anything in caster_mob.actions)
+ if(bodybound_action.target != caster_mob)
+ continue
+ bodybound_action.Grant(owner)
+
+ return ..()
+
+/datum/status_effect/shapechange_mob/from_spell/restore_caster(kill_caster_after)
+ var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref.resolve()
+ // The owner = owner check here is specifically for edge cases in which the owner of the spell
+ // is no longer in control of the shapeshifted mob, such as mindswapping out of a shapeshift
+ if(!QDELETED(source_spell) && source_spell.owner == owner)
+ source_spell.Grant(caster_mob)
+
+ for(var/datum/action/bodybound_action as anything in owner.actions)
+ if(bodybound_action.target != caster_mob)
+ continue
+ bodybound_action.Grant(caster_mob)
+
+ return ..()
+
+/datum/status_effect/shapechange_mob/from_spell/after_unchange()
+ . = ..()
+ var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref?.resolve()
+ if(QDELETED(source_spell) || !source_spell.convert_damage)
+ return
+
+ if(caster_mob.stat != DEAD)
+ caster_mob.revive(full_heal = TRUE, admin_revive = FALSE)
+
+ var/damage_to_apply = caster_mob.maxHealth * ((owner.maxHealth - owner.health) / owner.maxHealth)
+ caster_mob.apply_damage(damage_to_apply, source_spell.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND)
+
+ caster_mob.blood_volume = owner.blood_volume
+
+/datum/status_effect/shapechange_mob/from_spell/on_shape_death(datum/source, gibbed)
+ var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref.resolve()
+ // If our spell dictates our wizard dies when our shape dies, we won't restore by default
+ if(!QDELETED(source_spell) && source_spell.die_with_shapeshifted_form)
+ // (But if our spell says we should revert on death anyways, we'll also do that)
+ if(source_spell.revert_on_death)
+ restore_caster(kill_caster_after = TRUE)
+ // Otherwise, we just do nothing - we dead
+ return
+
+ return ..() // Restore like normal
+
+/datum/status_effect/shapechange_mob/from_spell/on_caster_death(datum/source)
+ var/datum/action/cooldown/spell/shapeshift/source_spell = source_weakref.resolve()
+ // If our spell does not have revert_on_death, don't do anything when our caster dies
+ if(!QDELETED(source_spell) && !source_spell.revert_on_death)
+ return
+
+ return ..() // Kill our owner and revert, like normal
+
+/atom/movable/screen/alert/status_effect/shapeshifted
+ name = "Shapeshifted"
+ desc = "Your form is not your own... you're shapeshifted into another creature! \
+ A wizard could turn you back - or maybe you're stuck like this for good?"
+ icon_state = "shapeshifted"
+
+/atom/movable/screen/alert/status_effect/shapeshifted/Click(location, control, params)
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/living_user = usr
+ if(!istype(living_user))
+ return
+
+ // Clicking the action will try to cast whatever spell shifted us in the first place
+ for(var/datum/action/cooldown/spell/shapeshift/shift_spell in living_user.actions)
+ if(istype(living_user, shift_spell.shapeshift_type))
+ shift_spell.Trigger()
+ return TRUE
diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
index 4ddad53c935..befb05aa8f6 100644
--- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
@@ -1,9 +1,20 @@
+/// Helper for checking of someone's shapeshifted currently.
+#define is_shifted(mob) mob.has_status_effect(/datum/status_effect/shapechange_mob/from_spell)
+
+/**
+ * Shapeshift spells.
+ *
+ * Allows the caster to transform to and from a different mob type.
+ */
/datum/action/cooldown/spell/shapeshift
button_icon_state = "shapeshift"
school = SCHOOL_TRANSMUTATION
cooldown_time = 10 SECONDS
- /// Whehter we revert to our human form on death.
+ /// Our spell's requrements before we shapeshifted. Stored on shapeshift so we can restore them after unshifting.
+ var/pre_shift_requirements
+
+ /// Whether we revert to our human form on death.
var/revert_on_death = TRUE
/// Whether we die when our shapeshifted form is killed
var/die_with_shapeshifted_form = TRUE
@@ -12,29 +23,35 @@
/// If convert damage is true, the damage type we deal when converting damage back and forth
var/convert_damage_type = BRUTE
- /// Our chosen type
+ /// Our chosen type.
var/mob/living/shapeshift_type
- /// All possible types we can become
+ /// All possible types we can become.
+ /// This should be implemented even if there is only one choice.
var/list/atom/possible_shapes
/datum/action/cooldown/spell/shapeshift/is_valid_target(atom/cast_on)
return isliving(cast_on)
-/datum/action/cooldown/spell/shapeshift/proc/is_shifted(mob/living/cast_on)
- return locate(/obj/shapeshift_holder) in cast_on
-
-/datum/action/cooldown/spell/shapeshift/before_cast(atom/cast_on)
+/datum/action/cooldown/spell/shapeshift/before_cast(mob/living/cast_on)
. = ..()
if(. & SPELL_CANCEL_CAST)
return
if(shapeshift_type)
+ // If another shapeshift spell was casted while we're already shifted, they could technically go to do_unshapeshift().
+ // However, we don't really want people casting shapeshift A to un-shapeshift from shapeshift B,
+ // as it could cause bugs or unintended behavior. So we'll just stop them here.
+ if(is_shifted(cast_on) && !is_type_in_list(cast_on, possible_shapes))
+ to_chat(cast_on, span_warning("This spell won't un-shapeshift you from this form!"))
+ return . | SPELL_CANCEL_CAST
+
return
if(length(possible_shapes) == 1)
shapeshift_type = possible_shapes[1]
return
+ // Not bothering with caching these as they're only ever shown once
var/list/shape_names_to_types = list()
var/list/shape_names_to_image = list()
if(!length(shape_names_to_types) || !length(shape_names_to_image))
@@ -67,24 +84,25 @@
cast_on.buckled?.unbuckle_mob(cast_on, force = TRUE)
var/currently_ventcrawling = (cast_on.movement_type & VENTCRAWLING)
+ var/mob/living/resulting_mob
// Do the shift back or forth
if(is_shifted(cast_on))
- restore_form(cast_on)
+ resulting_mob = do_unshapeshift(cast_on)
else
- do_shapeshift(cast_on)
+ resulting_mob = do_shapeshift(cast_on)
// The shift is done, let's make sure they're in a valid state now
// If we're not ventcrawling, we don't need to mind
- if(!currently_ventcrawling)
+ if(!currently_ventcrawling || !resulting_mob)
return
// We are ventcrawling - can our new form support ventcrawling?
- if(HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_NUDE))
+ if(HAS_TRAIT(resulting_mob, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(resulting_mob, TRAIT_VENTCRAWLER_NUDE))
return
// Uh oh. You've shapeshifted into something that can't fit into a vent, while ventcrawling.
- eject_from_vents(cast_on)
+ eject_from_vents(resulting_mob)
/// Whenever someone shapeshifts within a vent,
/// and enters a state in which they are no longer a ventcrawler,
@@ -107,9 +125,11 @@
playsound(possible_vent, 'sound/effects/reee.ogg', 75, TRUE)
priority_announce("We detected a pipe blockage around [get_area(get_turf(cast_on))], please dispatch someone to investigate.", "Central Command")
- cast_on.death()
- qdel(cast_on)
+ // Gib our caster, and make sure to leave nothing behind
+ // (If we leave something behind, it'll drop on the turf of the pipe, which is kinda wrong.)
+ cast_on.gib(TRUE, TRUE, TRUE)
+/// Callback for the radial that allows the user to choose their species.
/datum/action/cooldown/spell/shapeshift/proc/check_menu(mob/living/caster)
if(QDELETED(src))
return FALSE
@@ -118,153 +138,43 @@
return !caster.incapacitated()
+/// Actually does the shapeshift, for the caster.
/datum/action/cooldown/spell/shapeshift/proc/do_shapeshift(mob/living/caster)
- if(is_shifted(caster))
- to_chat(caster, span_warning("You're already shapeshifted!"))
- CRASH("[type] called do_shapeshift while shapeshifted.")
-
- var/mob/living/new_shape = new shapeshift_type(caster.loc)
- var/obj/shapeshift_holder/new_shape_holder = new(new_shape, src, caster)
-
- spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB)
-
- return new_shape_holder
-
-/datum/action/cooldown/spell/shapeshift/proc/restore_form(mob/living/caster)
- var/obj/shapeshift_holder/current_shift = is_shifted(caster)
- if(QDELETED(current_shift))
+ var/mob/living/new_shape = create_shapeshift_mob(caster.loc)
+ var/datum/status_effect/shapechange_mob/shapechange = new_shape.apply_status_effect(/datum/status_effect/shapechange_mob/from_spell, caster, src)
+ if(!shapechange)
+ // We failed to shift, maybe because we were already shapeshifted?
+ // Whatver the case, this shouldn't happen, so throw a stack trace.
+ to_chat(caster, span_warning("You can't shapeshift in this form!"))
+ stack_trace("[type] do_shapeshift was called when the mob was already shapeshifted (from a spell).")
return
- var/mob/living/restored_player = current_shift.stored
+ // Make sure it's castable even in their new form.
+ pre_shift_requirements = spell_requirements
+ spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB)
- current_shift.restore()
- spell_requirements = initial(spell_requirements) // Miiight mess with admin stuff.
+ return new_shape
- return restored_player
+/// Actually does the un-shapeshift, from the caster. (Caster is a shapeshifted mob.)
+/datum/action/cooldown/spell/shapeshift/proc/do_unshapeshift(mob/living/caster)
+ var/datum/status_effect/shapechange_mob/shapechange = caster.has_status_effect(/datum/status_effect/shapechange_mob/from_spell)
+ if(!shapechange)
+ // We made it to do_unshapeshift without having a shapeshift status effect, this shouldn't happen.
+ to_chat(caster, span_warning("You can't un-shapeshift from this form!"))
+ stack_trace("[type] do_unshapeshift was called when the mob wasn't even shapeshifted (from a spell).")
+ return
-// Maybe one day, this can be a component or something
-// Until then, this is what holds data between wizard and shapeshift form whenever shapeshift is cast.
-/obj/shapeshift_holder
- name = "Shapeshift holder"
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ON_FIRE | UNACIDABLE | ACID_PROOF
- var/mob/living/stored
- var/mob/living/shape
- var/restoring = FALSE
- var/datum/action/cooldown/spell/shapeshift/source
+ // Restore the requirements.
+ spell_requirements = pre_shift_requirements
+ pre_shift_requirements = null
-/obj/shapeshift_holder/Initialize(mapload, datum/action/cooldown/spell/shapeshift/_source, mob/living/caster)
- . = ..()
- source = _source
- shape = loc
- if(!istype(shape))
- stack_trace("shapeshift holder created outside mob/living")
- return INITIALIZE_HINT_QDEL
- stored = caster
+ var/mob/living/unshapeshifted_mob = shapechange.caster_mob
+ caster.remove_status_effect(/datum/status_effect/shapechange_mob/from_spell)
+ return unshapeshifted_mob
- // Transfer the Shapeshift spell over, if we actually own it
- if(source.owner == caster)
- source.Grant(shape)
+/// Helper proc that instantiates the mob we shapeshift into.
+/// Returns an instance of a living mob. Can be overridden.
+/datum/action/cooldown/spell/shapeshift/proc/create_shapeshift_mob(atom/loc)
+ return new shapeshift_type(loc)
- // Also transfer over any actions bound to them specifically - this leaves behind item actions and similar
- // (Mindbound actions are automatically tranferred over, so we don't need to worry about it)
- for(var/datum/action/bodybound_action as anything in caster.actions)
- if(bodybound_action.target != caster)
- continue
- bodybound_action.Grant(shape)
-
- if(stored.mind)
- stored.mind.transfer_to(shape)
- stored.forceMove(src)
- stored.notransform = TRUE
- if(source.convert_damage)
- var/damage_percent = (stored.maxHealth - stored.health) / stored.maxHealth;
- var/damapply = damage_percent * shape.maxHealth;
-
- shape.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND);
- shape.blood_volume = stored.blood_volume;
-
- RegisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/shape_death)
- RegisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/caster_death)
-
-/obj/shapeshift_holder/Destroy()
- // restore_form manages signal unregistering. If restoring is TRUE, we've already unregistered the signals and we're here
- // because restore() qdel'd src.
- if(!restoring)
- restore()
- stored = null
- shape = null
- return ..()
-
-/obj/shapeshift_holder/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- if(!restoring && !QDELETED(src))
- restore()
-
-/obj/shapeshift_holder/handle_atom_del(atom/A)
- if(A == stored && !restoring)
- restore()
-
-/obj/shapeshift_holder/Exited(atom/movable/gone, direction)
- if(stored == gone && !restoring)
- restore()
-
-/obj/shapeshift_holder/proc/caster_death()
- SIGNAL_HANDLER
-
- //Something kills the stored caster through direct damage.
- if(source.revert_on_death)
- restore(death = TRUE)
- else
- shape.death()
-
-/obj/shapeshift_holder/proc/shape_death()
- SIGNAL_HANDLER
-
- //Shape dies.
- if(source.die_with_shapeshifted_form)
- if(source.revert_on_death)
- restore(death = TRUE)
- else
- restore()
-
-/obj/shapeshift_holder/proc/restore(death=FALSE)
- // Destroy() calls this proc if it hasn't been called. Unregistering here prevents multiple qdel loops
- // when caster and shape both die at the same time.
- UnregisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
- UnregisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
- restoring = TRUE
-
- // Give Shapeshift back to the OG, if we actually own it
- // (Shapeshift into mindswap may change the action's owner)
- if(!QDELETED(source) && source.owner == shape)
- source.Grant(stored)
-
- // Also transfer their bodybound actions back
- for(var/datum/action/bodybound_action as anything in shape.actions)
- if(bodybound_action.target != stored)
- continue
- bodybound_action.Grant(stored)
-
- stored.forceMove(shape.loc)
- stored.notransform = FALSE
- if(shape.mind)
- shape.mind.transfer_to(stored)
- if(death)
- stored.death()
- else if(source.convert_damage)
- stored.revive(full_heal = TRUE, admin_revive = FALSE)
-
- var/damage_percent = (shape.maxHealth - shape.health)/shape.maxHealth;
- var/damapply = stored.maxHealth * damage_percent
-
- stored.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus=CANT_WOUND)
- if(source.convert_damage)
- stored.blood_volume = shape.blood_volume;
-
- // This guard is important because restore() can also be called on COMSIG_PARENT_QDELETING for shape, as well as on death.
- // This can happen in, for example, [/proc/wabbajack] where the mob hit is qdel'd.
- if(!QDELETED(shape))
- QDEL_NULL(shape)
-
- qdel(src)
- return stored
+#undef is_shifted
diff --git a/code/modules/spells/spell_types/shapeshift/dragon.dm b/code/modules/spells/spell_types/shapeshift/dragon.dm
index 358ff8a44fd..061a342d1d3 100644
--- a/code/modules/spells/spell_types/shapeshift/dragon.dm
+++ b/code/modules/spells/spell_types/shapeshift/dragon.dm
@@ -3,6 +3,7 @@
name = "Dragon Form"
desc = "Take on the shape a lesser ash drake."
invocation = "RAAAAAAAAWR!"
+ invocation_type = INVOCATION_SHOUT
spell_requirements = NONE
possible_shapes = list(/mob/living/simple_animal/hostile/megafauna/dragon/lesser)
diff --git a/code/modules/spells/spell_types/shapeshift/polar_bear.dm b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
index 73f0bae9496..af3efc878d3 100644
--- a/code/modules/spells/spell_types/shapeshift/polar_bear.dm
+++ b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
@@ -2,6 +2,7 @@
name = "Polar Bear Form"
desc = "Take on the shape of a polar bear."
invocation = "RAAAAAAAAWR!"
+ invocation_type = INVOCATION_SHOUT
spell_requirements = NONE
possible_shapes = list(/mob/living/simple_animal/hostile/asteroid/polarbear/lesser)
diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm
index 8e24fbb2ecd..276048ba16c 100644
--- a/code/modules/unit_tests/create_and_destroy.dm
+++ b/code/modules/unit_tests/create_and_destroy.dm
@@ -54,8 +54,6 @@
ignore += typesof(/obj/effect/pod_landingzone)
//We have a baseturf limit of 10, adding more than 10 baseturf helpers will kill CI, so here's a future edge case to fix.
ignore += typesof(/obj/effect/baseturf_helper)
- //There's no shapeshift to hold
- ignore += typesof(/obj/shapeshift_holder)
//No tauma to pass in
ignore += typesof(/mob/camera/imaginary_friend)
//No pod to gondola
diff --git a/code/modules/unit_tests/spell_shapeshift.dm b/code/modules/unit_tests/spell_shapeshift.dm
index 0aebbd7e492..750b942fd2c 100644
--- a/code/modules/unit_tests/spell_shapeshift.dm
+++ b/code/modules/unit_tests/spell_shapeshift.dm
@@ -1,6 +1,5 @@
/**
- * Validates that all shapeshift type spells
- * have a valid possible_shapes setup.
+ * Validates that all shapeshift type spells have a valid possible_shapes setup.
*/
/datum/unit_test/shapeshift_spell_validity
@@ -18,3 +17,89 @@
Fail("Shapeshift spell: [shift] had an invalid / non-living shift type ([shift_type]) in their possible shapes list.")
qdel(shift)
+
+/**
+ * Validates that shapeshift spells put the mob in another mob, as they should.
+ */
+/datum/unit_test/shapeshift_spell
+
+/datum/unit_test/shapeshift_spell/Run()
+
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human)
+ dummy.mind_initialize()
+
+ for(var/spell_type in subtypesof(/datum/action/cooldown/spell/shapeshift))
+ // Test all shapeshifts as if they were on the mob's body
+ var/datum/action/cooldown/spell/shapeshift/bodybound_shift = new spell_type(dummy)
+ bodybound_shift.Grant(dummy)
+ if(LAZYLEN(bodybound_shift.possible_shapes) > 1)
+ for(var/forced_shape in bodybound_shift.possible_shapes)
+ test_spell(dummy, bodybound_shift, forced_shape)
+
+ else if(LAZYLEN(bodybound_shift.possible_shapes) == 1)
+ test_spell(dummy, bodybound_shift)
+
+ qdel(bodybound_shift)
+
+ // And test all shapeshifts as if they were on the mob's mind
+ var/datum/action/cooldown/spell/shapeshift/mindbound_shift = new spell_type(dummy.mind)
+ mindbound_shift.Grant(dummy)
+ if(LAZYLEN(mindbound_shift.possible_shapes) > 1)
+ for(var/forced_shape in mindbound_shift.possible_shapes)
+ test_spell(dummy, mindbound_shift, forced_shape)
+
+ else if(LAZYLEN(bodybound_shift.possible_shapes) == 1)
+ test_spell(dummy, mindbound_shift)
+
+ qdel(mindbound_shift)
+
+/datum/unit_test/shapeshift_spell/proc/test_spell(mob/living/carbon/human/dummy, datum/action/cooldown/spell/shapeshift/shift, forced_shape)
+ if(forced_shape)
+ shift.shapeshift_type = forced_shape
+
+ shift.next_use_time = 0
+ shift.Trigger()
+ if(!istype(dummy.loc, shift.shapeshift_type))
+ return TEST_FAIL("Shapeshift spell: [shift.name] failed to transform the dummy into the shape [initial(shift.shapeshift_type.name)].")
+
+ var/mob/living/shape = dummy.loc
+ if(!(shift in shape.actions))
+ return TEST_FAIL("Shapeshift spell: [shift.name] failed to grant the spell to the dummy's shape.")
+
+ shift.next_use_time = 0
+ shift.Trigger()
+ if(istype(dummy.loc, shift.shapeshift_type))
+ return TEST_FAIL("Shapeshift spell: [shift.name] failed to transform the dummy back into a human.")
+
+
+/**
+ * Validates that shapeshifts function properly with holoparasites.
+ */
+/datum/unit_test/shapeshift_holoparasites
+
+/datum/unit_test/shapeshift_holoparasites/Run()
+
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human)
+
+ var/datum/action/cooldown/spell/shapeshift/wizard/shift = new(dummy)
+ shift.shapeshift_type = shift.possible_shapes[1]
+ shift.Grant(dummy)
+
+ var/mob/living/simple_animal/hostile/guardian/test_stand = allocate(/mob/living/simple_animal/hostile/guardian)
+ test_stand.set_summoner(dummy)
+
+ // The stand's summoner is dummy.
+ TEST_ASSERT_EQUAL(test_stand.summoner, dummy, "Holoparasite failed to set the summoner to the correct mob.")
+
+ // Dummy casts shapeshift. The stand's summoner should become the shape the dummy is within.
+ shift.Trigger()
+ TEST_ASSERT(istype(dummy.loc, shift.shapeshift_type), "Shapeshift spell failed to transform the dummy into the shape [initial(shift.shapeshift_type.name)].")
+ TEST_ASSERT_EQUAL(test_stand.summoner, dummy.loc, "Shapeshift spell failed to transfer the holoparasite to the dummy's shape.")
+
+ // Dummy casts shapeshfit back, the stand's summoner should become the dummy again.
+ shift.next_use_time = 0
+ shift.Trigger()
+ TEST_ASSERT(!istype(dummy.loc, shift.shapeshift_type), "Shapeshift spell failed to transform the dummy back into human form.")
+ TEST_ASSERT_EQUAL(test_stand.summoner, dummy, "Shapeshift spell failed to transfer the holoparasite back to the dummy's human form.")
+
+ qdel(shift)
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index 539007b2652..cea41599afa 100755
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 6d083392fb0..961a4e89ea9 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -4343,6 +4343,7 @@
#include "code\modules\spells\spell_types\self\stop_time.dm"
#include "code\modules\spells\spell_types\self\summonitem.dm"
#include "code\modules\spells\spell_types\self\voice_of_god.dm"
+#include "code\modules\spells\spell_types\shapeshift\_shape_status.dm"
#include "code\modules\spells\spell_types\shapeshift\_shapeshift.dm"
#include "code\modules\spells\spell_types\shapeshift\dragon.dm"
#include "code\modules\spells\spell_types\shapeshift\polar_bear.dm"