mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 21:18:37 +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:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -37,5 +37,4 @@
|
||||
|
||||
#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
|
||||
|
||||
#define STASIS_ASCENSION_EFFECT "heretic_ascension"
|
||||
|
||||
#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]."))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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--
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user