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