mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 04:30:44 +01:00
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user