Kills /obj/shapeshift_holder, replaces it with /datum/status_effect/shapechange_mob, also does a lot of Wabbajack refactoring (#69091)

About The Pull Request

    Deletes /obj/shapeshift_holder, replaces it with /datum/status_effect/shapechange_mob
    Refactors Heretic worm form into a shapeshift spell
    Refactors Wabbajack, and associated code

Fixes #69117
Fixes #65653
Fixes #59127
Fixes #52786
Why It's Good For The Game

/obj/shapeshift_holder was one of the worst remaining abuses of /obj direct subtypes, so I replaced it with a cool fancy datum.

This also decouples the shapeshifting behavior entirely from the shapeshifting spell. So we have support for shapeshifted mobs not sourced from a spell. Which is neat, we could technically swap Wabbajack to use this in the future.
Changelog

cl Melbert
fix: Wabbajacking a shapeshifted mob no longer runtimes horribly. When a shapeshifted mob is wabbajacked, they'll now be removed from their shapeshift and stunned.
fix: Transforming via a shapeshift should no longer rob you of your hearing / runechat awareness.
fix: Shapeshifting plays nicer with holoparasites.
fix: Being polymorphed from a xeno to a non-xeno correctly makes you a non-xeno
refactor: Refactored shapeshifting, the shapeshift holder is now a status effect instead of an object.
refactor: Heretic worm form is a shapeshift spell now, this might have some minor behavioral changes but should overall be the same.
refactor: Refactored Wabbajack (+ cursed pool). Overall a bit more clean / consistent behavior.
/cl
This commit is contained in:
MrMelbert
2022-09-02 09:44:41 +12:00
committed by GitHub
parent c7cf23ae7e
commit de04b3be80
31 changed files with 710 additions and 346 deletions
@@ -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"
+10
View File
@@ -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)
+1 -2
View File
@@ -37,5 +37,4 @@
#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
#define STASIS_ASCENSION_EFFECT "heretic_ascension"
#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
+2 -2
View File
@@ -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)
@@ -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)
@@ -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"
+10
View File
@@ -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
+32 -9
View File
@@ -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,
)
@@ -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)]."))
+1 -1
View File
@@ -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)
@@ -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))
+82 -61
View File
@@ -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")
@@ -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 = "<font color=\"[guardiancolor]\"><b><i>[src]:</i></b></font> [preliminary_message]" //add source, color source with the guardian's color
to_chat(summoner, "<span class='say'>[my_message]</span>")
var/list/guardians = summoner.hasparasites()
var/list/guardians = summoner.get_all_linked_holoparasites()
for(var/para in guardians)
to_chat(para, "<span class='say'>[my_message]</span>")
for(var/M in GLOB.dead_mob_list)
@@ -422,7 +458,7 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
var/my_message = "<span class='holoparasite bold'><i>[src]:</i> [preliminary_message]</span>" //add source, color source with default grey...
to_chat(src, "<span class='say'>[my_message]</span>")
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, "<span class='say'><font color=\"[G.guardiancolor]\"><b><i>[src]:</i></b></font> [preliminary_message]</span>" )
@@ -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
@@ -122,7 +122,7 @@
SIGNAL_HANDLER
if(isliving(AM) && spawner && spawner.summoner && AM != spawner && !spawner.hasmatchingsummoner(AM))
to_chat(spawner.summoner, "[span_danger("<B>[AM] has crossed surveillance snare, [name].")]</B>")
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("<B>[AM] has crossed surveillance snare, [name].")]</B>")
@@ -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)
@@ -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.
*
+1 -1
View File
@@ -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)
@@ -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
+41 -13
View File
@@ -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)
+1 -1
View File
@@ -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--
+5 -1
View File
@@ -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
@@ -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
@@ -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) \
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
+87 -2
View File
@@ -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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 118 KiB

+1
View File
@@ -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"