From 3cb0b239b7c03c4f4e16c913e3e8ce89248a04fc Mon Sep 17 00:00:00 2001 From: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:55:47 -0400 Subject: [PATCH] Merges ghosts into spirits & a rework (#92879) ## About The Pull Request Having 2 different species with similar names, looks, attributes etc. seemed like it would be confusing so I thought we should probably trim it down to 1, however since I'm doing that I thought of other changes to go along with it: - The passthrough ability was replaced, now while you're passing through things you no longer drop stuff, but you put a tombstone where you start phasing, and are pulled back to it while done. You still can't interact with anything in this form. Having the tombstone dug out will be raiding your grave, so you'll be pulled out as a dead skeleton. - Removes holy water interactions - Ghosts now have organic limbs & are a Humanoid, so they can have surgery and other forms of healing used on them. https://github.com/user-attachments/assets/5db0c204-0070-466b-8b6c-1702ff0fd94e ## Why It's Good For The Game Ghosts being available to Wizards is okayish but is a rather big buff for one you get free, yes you can still be caught up to if you're in hallways and such but it's way easier to get away with than like, your default teleport scroll even. Being available to Miners though was a big mistake because it opened them to be quite an annoyance with it. The only counter was the fact no healing worked on them, which I don't think is how it should be balanced. I thought I could trust players with such tools but apparently I had too high hopes. Anyway this makes it more bearable for it to be given out at the low cost it currently is at, it's still good as flavor, to spook people or ghost around, and I think while it doesn't make 100% sense why you lose access to your hands, inventory, and UI when you supposedly "drop" your tombstone, I think it's still a good advantage for "scouting", sorta like a lesser spirit realm rune. Closes https://github.com/tgstation/tgstation/issues/91849 ## Changelog :cl: del: Merged ghosts and spirits into one species. balance: Spirit's passthrough ability is now tied down 7 tiles from a tombstone, but is no longer affected by holy water nor does it make them drop their equipped gear. balance: Ghosts can now be healed. /:cl: --- code/__DEFINES/is_helpers.dm | 2 +- code/__DEFINES/mobs.dm | 1 - .../lavalandruin_code/elephantgraveyard.dm | 8 + code/modules/mob/living/carbon/human/human.dm | 5 +- .../carbon/human/species_types/ghost.dm | 221 ++++++------------ code/modules/photography/photos/photo.dm | 2 +- .../species_parts/ghost_bodyparts.dm | 13 +- ...enshot_humanoids__datum_species_ghost.png} | Bin ..._humanoids__datum_species_spirit_ghost.png | Bin 2011 -> 0 bytes icons/mob/human/species/ghost.dmi | Bin 2324 -> 2127 bytes 10 files changed, 88 insertions(+), 164 deletions(-) rename code/modules/unit_tests/screenshots/{screenshot_humanoids__datum_species_spirit.png => screenshot_humanoids__datum_species_ghost.png} (100%) delete mode 100644 code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_spirit_ghost.png diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 6c2460cd1f9..bc590cf10d5 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -115,7 +115,7 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list( //Human sub-species #define isabductor(A) (is_species(A, /datum/species/abductor)) -#define isspirit(A) (is_species(A, /datum/species/spirit)) +#define isghostspecies(A) (is_species(A, /datum/species/ghost)) #define isgolem(A) (is_species(A, /datum/species/golem)) #define islizard(A) (is_species(A, /datum/species/lizard)) #define isashwalker(A) (is_species(A, /datum/species/lizard/ashwalker)) diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 22c9819c7ae..7cdd886694d 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -177,7 +177,6 @@ #define SPECIES_HUMAN "human" #define SPECIES_JELLYPERSON "jelly" #define SPECIES_SLIMEPERSON "slime" -#define SPECIES_SPIRIT "spirit" #define SPECIES_LUMINESCENT "luminescent" #define SPECIES_STARGAZER "stargazer" #define SPECIES_LIZARD "lizard" diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm index a77131e73ce..418a4b8e509 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm @@ -333,6 +333,14 @@ new /obj/effect/decal/cleanable/blood/gibs/old(src) new /obj/item/book/granter/crafting_recipe/boneyard_notes(src) +/obj/structure/closet/crate/grave/skeleton + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + affect_mood = TRUE + +/obj/structure/closet/crate/grave/skeleton/PopulateContents() + . = ..() + new /mob/living/carbon/human/species/skeleton(src) + //***Fluff items for lore/intrigue /obj/item/paper/crumpled/muddy/fluff/elephant_graveyard name = "posted warning" diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 9792d645ebb..79da995c053 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1115,11 +1115,8 @@ /mob/living/carbon/human/species/lizard/silverscale race = /datum/species/lizard/silverscale -/mob/living/carbon/human/species/spirit - race = /datum/species/spirit - /mob/living/carbon/human/species/ghost - race = /datum/species/spirit/ghost + race = /datum/species/ghost /mob/living/carbon/human/species/ethereal race = /datum/species/ethereal diff --git a/code/modules/mob/living/carbon/human/species_types/ghost.dm b/code/modules/mob/living/carbon/human/species_types/ghost.dm index 7e54ce26196..e1653c9f3b4 100644 --- a/code/modules/mob/living/carbon/human/species_types/ghost.dm +++ b/code/modules/mob/living/carbon/human/species_types/ghost.dm @@ -1,22 +1,23 @@ ///Spirit mob that lacks legs but still roams the station as part of the unliving. -/datum/species/spirit - name = "Spirit" - id = SPECIES_SPIRIT +/datum/species/ghost + name = "Ghost" + id = SPECIES_GHOST inherent_traits = list( TRAIT_GENELESS, + TRAIT_MOVE_FLYING, TRAIT_NEVER_WOUNDED, TRAIT_NOBLOOD, TRAIT_NODISMEMBER, TRAIT_NO_DNA_COPY, + TRAIT_NO_FLOATING_ANIM, TRAIT_NO_PLASMA_TRANSFORM, TRAIT_NO_UNDERWEAR, + TRAIT_RESISTLOWPRESSURE, TRAIT_UNHUSKABLE, - TRAIT_NO_FLOATING_ANIM, - TRAIT_MOVE_FLYING, ) - inherent_biotypes = MOB_SPIRIT | MOB_UNDEAD + inherent_biotypes = MOB_HUMANOID | MOB_SPIRIT | MOB_UNDEAD no_equip_flags = ITEM_SLOT_FEET - changesource_flags = MIRROR_BADMIN | WABBAJACK | SLIME_EXTRACT + changesource_flags = MIRROR_BADMIN | WABBAJACK | SLIME_EXTRACT | MIRROR_PRIDE | MIRROR_MAGIC sexes = FALSE meat = /obj/item/ectoplasm @@ -32,34 +33,45 @@ bodypart_overrides = list( BODY_ZONE_HEAD = /obj/item/bodypart/head/ghost, - BODY_ZONE_CHEST = /obj/item/bodypart/chest/ghost/spirit, + BODY_ZONE_CHEST = /obj/item/bodypart/chest/ghost, BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/ghost, BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/ghost, ) - ///Boolean on whether this species type is available at roundstart during halloween, used to deny subtypes. - var/halloween_exclusive = TRUE + ///Innate passthrough ability given to ghosts that allows them to phase but drops their stuff. + var/datum/action/innate/toggle_passthrough/passthrough_ability -/datum/species/spirit/check_roundstart_eligible() - if(check_holidays(HALLOWEEN) && halloween_exclusive) +/datum/species/ghost/check_roundstart_eligible() + if(check_holidays(HALLOWEEN)) return TRUE return ..() -/datum/species/spirit/get_physical_attributes() - return "Spirits are the spiritual remains of long-passed entities. They lack legs, can fly, but still eat, breathe, hear and see." +/datum/species/ghost/on_species_gain(mob/living/carbon/human/new_ghost, datum/species/old_species, pref_load, regenerate_icons) + . = ..() + passthrough_ability = new(src) + passthrough_ability.Grant(new_ghost) -/datum/species/spirit/get_species_description() +/datum/species/ghost/on_species_loss(mob/living/carbon/human/former_ghost, datum/species/new_species, pref_load) + QDEL_NULL(passthrough_ability) + return ..() + +/datum/species/ghost/get_physical_attributes() + return "Ghosts are the spiritual remains of long-passed entities. They lack legs, can fly, and phase through walls, \ + but still eat, breathe, hear and see." + +/datum/species/ghost/get_species_description() return "Spirits are spirits of long-dead creatures whom, for one reason or another, still roam around." -/datum/species/spirit/get_species_lore() +/datum/species/ghost/get_species_lore() return list( - "Spirits are the non-physical remains that linger onto their mortal coil. \ - They still need their protein and organs to keep themselves \"alive\", \ - which leads to many of them still believing they are still part of the living, \ - whether or not they are is a very open-ended debate between philosophers.", + "Ghosts are one of the spookiest creatures known in the galaxy. \ + While they still need their protein to sustain themselves, they are able to control their own bodies, \ + going through walls and getting rid of all their posessions at will. \ + Most knowledge known about them is kept secret by Nanotrasen's top Chaplains, who are keen \ + to keep it private.", ) -/datum/species/spirit/create_pref_unique_perks() +/datum/species/ghost/create_pref_unique_perks() var/list/to_add = list() to_add += list(list( @@ -69,6 +81,14 @@ SPECIES_PERK_DESC = "Ghosts lack legs and float, preventing you from falling into holes in the ground.", )) + to_add += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON = "ghost", + SPECIES_PERK_NAME = "Incorporeal", + SPECIES_PERK_DESC = "Ghost carry their tombstones with them and are directly tied to it. \ + dropping the tombstone will allow you to phase through solid matter, but leaves you vulnerable.", + )) + to_add += list(list( SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, SPECIES_PERK_ICON = "shoe-prints", @@ -78,102 +98,35 @@ return to_add -/** - * Ghost subtype - * This is the type of ghost that can actually phase through walls, - * exclusive to magic mirrors & admins, as roundstart-ability to phase anywhere - * is not something that is generally fun to play against. - */ -/datum/species/spirit/ghost - name = "Ghost" - id = SPECIES_GHOST - inherent_traits = list( - TRAIT_GENELESS, - TRAIT_NEVER_WOUNDED, - TRAIT_NOBLOOD, - TRAIT_NODISMEMBER, - TRAIT_NO_DNA_COPY, - TRAIT_NO_PLASMA_TRANSFORM, - TRAIT_NO_UNDERWEAR, - TRAIT_UNHUSKABLE, - TRAIT_NO_FLOATING_ANIM, - TRAIT_MOVE_FLYING, - //ghost-unique - TRAIT_SEE_BLESSED_TILES, - ) - //they have a different chest. - bodypart_overrides = list( - BODY_ZONE_HEAD = /obj/item/bodypart/head/ghost, - BODY_ZONE_CHEST = /obj/item/bodypart/chest/ghost, - BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/ghost, - BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/ghost, - ) - changesource_flags = MIRROR_BADMIN | MIRROR_PRIDE | MIRROR_MAGIC - halloween_exclusive = FALSE - - ///Innate passthrough ability given to ghosts that allows them to phase but drops their stuff. - var/datum/action/innate/toggle_passthrough/passthrough_ability - -/datum/species/spirit/ghost/get_physical_attributes() - return "Ghosts are the spiritual remains of long-passed entities. They lack legs, can fly, can choose at will to become incorporeal, \ - but still eat, breathe, hear and see." - -/datum/species/spirit/ghost/get_species_lore() - return list( - "Ghosts are one of the spookiest creatures known in the galaxy. \ - While they still need their protein to sustain themselves, they are able to control their own bodies, \ - going through walls and getting rid of all their posessions at will. \ - Most knowledge known about them is kept secret by Nanotrasen's top Chaplains, who are keen \ - to keep it private.", - ) - -/datum/species/spirit/ghost/on_species_gain(mob/living/carbon/human/new_ghost, datum/species/old_species, pref_load, regenerate_icons) - . = ..() - passthrough_ability = new(src) - passthrough_ability.Grant(new_ghost) - for(var/datum/atom_hud/alternate_appearance/basic/blessed_aware/blessed_hud in GLOB.active_alternate_appearances) - blessed_hud.check_hud(new_ghost) - -/datum/species/spirit/ghost/on_species_loss(mob/living/carbon/human/former_ghost, datum/species/new_species, pref_load) - . = ..() - QDEL_NULL(passthrough_ability) - //this has to be called after parent so inherent traits is cleared before we update our HUDs - for(var/datum/atom_hud/alternate_appearance/basic/blessed_aware/blessed_hud in GLOB.active_alternate_appearances) - blessed_hud.check_hud(former_ghost) - -/datum/species/spirit/ghost/create_pref_unique_perks() - var/list/to_add = ..() - - to_add += list(list( - SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "ghost", - SPECIES_PERK_NAME = "Incorporeal", - SPECIES_PERK_DESC = "Ghost are able to control their body to the extent where you can willingly make yourself able \ - to phase through anything, including your own equipment.", - )) - - return to_add - /** * Passthrough ability * * Ghost innate ability that allows them to enter ghost mode, - * which gives them the phasing ability, but makes them drop everything - * and completely unable to wear anything. + * which gives them the phasing ability, but makes them unable to use anything, + * and they will be tied to a tombstone that, if dug up, will kill them and turn them + * into a skeleton. */ /datum/action/innate/toggle_passthrough name = "Toggle passthrough" - desc = "Toggles your ability to phase through everything, including your gear and any incompatible organs/limbs." + desc = "Toggles phasing through everything, including your hands. You are tied to your tombstone while this is active. \ + At least you know how to keep your clothes on." button_icon = 'icons/hud/actions.dmi' button_icon_state = "ghost" + ///Grave that appears when we're passing through, which we are also tied to. + var/obj/structure/closet/crate/grave/skeleton/grave + /datum/action/innate/toggle_passthrough/Grant(mob/grant_to) . = ..() - RegisterSignal(grant_to, COMSIG_CARBON_POST_ATTACH_LIMB, PROC_REF(on_new_limb)) + grave = new() + //contents are initialized when the grave is robbed as that's when the crate is opened for the first time. + RegisterSignal(grave, COMSIG_CLOSET_CONTENTS_INITIALIZED, PROC_REF(on_grave_robbed)) + RegisterSignal(grave, COMSIG_CLOSET_POST_OPEN, PROC_REF(post_grave_robbed)) /datum/action/innate/toggle_passthrough/Remove(mob/remove_from) - swap_mode(force_off = TRUE) - UnregisterSignal(remove_from, COMSIG_CARBON_POST_ATTACH_LIMB) + if(!QDELING(remove_from)) + swap_mode(force_off = TRUE) + QDEL_NULL(grave) return ..() /datum/action/innate/toggle_passthrough/Activate() @@ -182,30 +135,6 @@ return swap_mode() -/datum/action/innate/toggle_passthrough/IsAvailable(feedback) - if(!isliving(owner)) - return FALSE - var/mob/living/living_owner = owner - if(living_owner.has_reagent(/datum/reagent/water/holywater)) - return FALSE - //technically you can trap a ghost by blessing them as theyre phasing, - //but they can still be dragged out. - if(locate(/obj/effect/blessing) in get_turf(owner)) - return FALSE - var/obj/item/bodypart/chest/their_chest = living_owner.get_bodypart(BODY_ZONE_CHEST) - if(!their_chest || !(their_chest.bodytype & BODYTYPE_GHOST)) - return FALSE - return ..() - -///Called when the owner of this action gets a new limb, if it isn't a ghost-limb -///the action will turn itself off, and you'll lose said limb if you try using this action again. -/datum/action/innate/toggle_passthrough/proc/on_new_limb(mob/source, obj/item/bodypart/new_part, special) - SIGNAL_HANDLER - if(!iscarbon(owner)) - return - if(!(new_part.bodytype & BODYTYPE_GHOST)) - swap_mode(force_off = TRUE) - ///Swaps the mode, allowing us to phase through stuff but drops everything. Optional 'force_off' arg to prevent being able to turn it on. /datum/action/innate/toggle_passthrough/proc/swap_mode(force_off) //we can only turn off, early return if we're trying to turn it on instead. @@ -213,17 +142,9 @@ return var/mob/living/carbon/carbon_owner = owner - var/datum/species/carbon_species = carbon_owner.dna.species - - //drop any limbs & organs that can't phase, now that you're phasing. - for(var/obj/item/bodypart/bodypart as anything in carbon_owner.bodyparts) - if(!(bodypart.bodytype & BODYTYPE_GHOST)) - bodypart.drop_limb(special = FALSE, dismembered = FALSE, move_to_floor = TRUE) - for(var/obj/item/organ/organ as anything in carbon_owner.organs) - if(!(organ.organ_flags & ORGAN_GHOST)) - organ.Remove(owner, special = FALSE) - if(HAS_TRAIT_FROM(carbon_owner, TRAIT_NO_FLOATING_ANIM, SPECIES_TRAIT)) + grave.forceMove(get_turf(carbon_owner)) + carbon_owner.AddComponent(/datum/component/leash, grave, distance = 7) REMOVE_TRAIT(carbon_owner, TRAIT_NO_FLOATING_ANIM, SPECIES_TRAIT) carbon_owner.add_traits(list( TRAIT_MOVE_PHASING, @@ -232,10 +153,11 @@ TRAIT_HANDS_BLOCKED, //MOBILITY_USE | MOBILITY_PICKUP | MOBILITY_STORAGE TRAIT_PULL_BLOCKED, //MOBILITY_PULL TRAIT_UI_BLOCKED, //MOBILITY_UI - ), SPECIES_TRAIT) - carbon_species.update_no_equip_flags(carbon_owner, ALL) - RegisterSignal(carbon_owner, COMSIG_MOB_CLIENT_PRE_LIVING_MOVE, PROC_REF(attempt_move)) + ), SPECIES_TRAIT) else + qdel(carbon_owner.GetComponent(/datum/component/leash)) + carbon_owner.forceMove(get_turf(grave)) + grave.moveToNullspace() ADD_TRAIT(carbon_owner, TRAIT_NO_FLOATING_ANIM, SPECIES_TRAIT) carbon_owner.remove_traits(list( TRAIT_MOVE_PHASING, @@ -244,13 +166,16 @@ TRAIT_HANDS_BLOCKED, TRAIT_PULL_BLOCKED, TRAIT_UI_BLOCKED, - ), SPECIES_TRAIT) - carbon_species.update_no_equip_flags(carbon_owner, initial(carbon_species.no_equip_flags)) - UnregisterSignal(carbon_owner, COMSIG_MOB_CLIENT_PRE_LIVING_MOVE) + ), SPECIES_TRAIT) -///Called when attempting to move to a new tile while the action is active, returns to cancel moving. -/datum/action/innate/toggle_passthrough/proc/attempt_move(mob/source, new_loc, direct) +///Called when the contents are made, which means the grave has been 'opened', therefore robbed. +/datum/action/innate/toggle_passthrough/proc/on_grave_robbed(obj/structure/closet/crate/grave/skeleton/source) SIGNAL_HANDLER - if(locate(/obj/effect/blessing) in new_loc) - to_chat(source, span_warning("Holy energies block your path!")) - return COMSIG_MOB_CLIENT_BLOCK_PRE_LIVING_MOVE + var/mob/living/carbon/human/species/skeleton/skeletons_in_the_closet = locate() in source.contents + owner.mind.transfer_to(skeletons_in_the_closet, force_key_move = TRUE) + skeletons_in_the_closet.death(gibbed = FALSE) + +///Called AFTER the contents have been spit out, which means the owner is now in the skeleton. Let's clean up. +/datum/action/innate/toggle_passthrough/proc/post_grave_robbed(obj/structure/closet/crate/grave/skeleton/source) + SIGNAL_HANDLER + qdel(owner) diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm index 450746c71a2..9c76e499a60 100644 --- a/code/modules/photography/photos/photo.dm +++ b/code/modules/photography/photos/photo.dm @@ -53,7 +53,7 @@ if(!seen) P.mobs_seen -= seen_ref continue - if(!isobserver(seen) && !isspirit(seen)) + if(!isobserver(seen) && !isghostspecies(seen)) continue set_custom_materials(list(/datum/material/hauntium =SHEET_MATERIAL_AMOUNT)) grind_results = list(/datum/reagent/hauntium = 20) diff --git a/code/modules/surgery/bodyparts/species_parts/ghost_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/ghost_bodyparts.dm index adeaa04067c..765dd8d465b 100644 --- a/code/modules/surgery/bodyparts/species_parts/ghost_bodyparts.dm +++ b/code/modules/surgery/bodyparts/species_parts/ghost_bodyparts.dm @@ -3,7 +3,7 @@ icon_static = 'icons/mob/human/species/ghost.dmi' icon_state = "ghost_head" biological_state = BIO_FLESH - bodytype = BODYTYPE_GHOST + bodytype = BODYTYPE_ORGANIC|BODYTYPE_GHOST limb_id = SPECIES_GHOST is_dimorphic = FALSE should_draw_greyscale = FALSE @@ -18,24 +18,19 @@ icon_state = "ghost_chest" biological_state = BIO_FLESH acceptable_bodyshape = BODYTYPE_GHOST - bodytype = BODYTYPE_GHOST + bodytype = BODYTYPE_ORGANIC|BODYTYPE_GHOST limb_id = SPECIES_GHOST is_dimorphic = FALSE should_draw_greyscale = FALSE dmg_overlay_type = null wing_types = null -//slightly different sprite meant to differentiate spirit from ghost. -/obj/item/bodypart/chest/ghost/spirit - icon_state = "spirit_chest" - limb_id = SPECIES_SPIRIT - /obj/item/bodypart/arm/left/ghost icon = 'icons/mob/human/species/ghost.dmi' icon_static = 'icons/mob/human/species/ghost.dmi' icon_state = "ghost_l_arm" biological_state = BIO_FLESH|BIO_JOINTED - bodytype = BODYTYPE_GHOST + bodytype = BODYTYPE_ORGANIC|BODYTYPE_GHOST limb_id = SPECIES_GHOST should_draw_greyscale = FALSE dmg_overlay_type = null @@ -45,7 +40,7 @@ icon_static = 'icons/mob/human/species/ghost.dmi' icon_state = "ghost_r_arm" biological_state = BIO_FLESH|BIO_JOINTED - bodytype = BODYTYPE_GHOST + bodytype = BODYTYPE_ORGANIC|BODYTYPE_GHOST limb_id = SPECIES_GHOST should_draw_greyscale = FALSE dmg_overlay_type = null diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_spirit.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_ghost.png similarity index 100% rename from code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_spirit.png rename to code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_ghost.png diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_spirit_ghost.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_spirit_ghost.png deleted file mode 100644 index c43c67f12a8b01a144cee7919749a370690ce131..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2011 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 literal 2324 zcmbVOXH=6}8vej2O$1biA`(Cl7(_}C2w;K~ff+zhSDJ{(8YH1dY9@pt;;=$&ln61% zxPa0XVM72}B19=cDWQcr0wRVo@DUP7vgqubv*(OIcK64D z4OY|49p!zcznpN5`cNn9rm3KodDqKLsQN$}b5l<`qw^u{2I=6wj5D1FZ@MN1^{w}R zI`Ttz`&jL!2DXGOteJryqsH`mT`XR}4ESRvU8NJw#}J=9002o-7l%{l;|n?CXTrx$ zD|bvsWL9OW>P4qjoNZkMdSo+~(l*sN=Cy5>e)DRk&%>)O@A6R!JL9=azwr&KQ6t3D z)G_+a^zAoXE|89Pnz1OEn+*u*;59_ZdVp?AdjPls;Jg*PZeUSB%Mcl5Qm&Yx0>KDw z((z2CDV6Xh~#Lq zZ%8Fsz~ufmW%71wvTRvS-D!{#y?=Xk%H%}cO8!{SXTRxjmIzg^O0S#|$jR7xZht7m z^^ZP0$e5-sa`Oe2O?sEqwzlr;4K3I5T_!}sAe{>sl8r;=y8@6Ax;9!PE|EB0SH6%@ z0gY8Gv+6)wOW{5q#8%y6gK?ZUc*Ua;0!5V;i86QFF)NDCh<@-%g&u#xP&TXFn5sjQ zt}~^Jzl&a2&G)eiT;kJW)i~Q!qqL)8^vrb$jiR-|;@CDeXkFMwZPl*}e7grKF6;ju z+pI}lY#4;6+iNZh6g8Ert${Eo{NuZAuAbjC8>~%C+W=1GsaRJH5_4z>*58Qs4D&Ve z)JU%3GZ-+j1%1Xa7z=IL>w5)Ekez4K7fU0VBobK;QK(Oe3rF>qvJicbEKoScMAho# zeK=Y@zOQ1$Mk%mYsVYKlr7p3_`~`ny#=aNBY~~J0R8u|`FH140a$w*+uCT2e;q)v^ zadB@lb_oNs~Y*4WQ?eb-LHF17IZQp|EjB}rDc2tHpgQBC}%v452McQWsVEkpP; zO1F^qPq9WWa5Z=WDQ1`%+)Syl92&v2GiWSjE!ml|GZQ?t53h18LO~#q!4s{KO zg!@n9;x}9o+AUIZd`dK5($;1zu9Ypu#_v5E|6@V2MfeRJk8E)~8kb^*Jy2+ONUNG4 zZp^HKOS8OiG0>aZK+phe>hq7L$kdkvMKe%u1h;nNe3-KpGL^$3;E$W41qYIANA2%i zPl9yy^Dv-m&d8iojQ~WVlOQ*pD49Hk?@l7ccbdH~MI-Ag+i$e}8-UCqB|YxuI(0E9 z#!jN6^CSYl#4kStMHbDCYndNU;e3$Sdo9giK)qjZx;9I{|Jbz(0mgF&d6+bNTflfg zKYRDU(K7O5u{U2&`sPx>YS~el8i`iMP zv!&5DfN?r@UR}L#byeZEeUZF@8Z^LvQ`5uK=0(rkGHbH*4ow)f(7`Yg2?K(wb&_jO zF|NHD<7{znAbiT;Epx6@2953MTdm-Rf1Lr3eL~u^K z{n0u+r)RFK;_-MW0_GN);VnSTaqq(0G_uNVzf6X2t7q{-Ja%VWPxlS)`Vz$d&f32} zw_y^XOjnnAz8!HR%v|(v!~h0c7BJB3sLS#@QQ1NJh=Bpb_o9cDNIOGPIekN~Q~K~WiVSPVF=_3xSRr`xxnYmFpWYVm`?oo^1{;^^*BWp_FCUq@6)D*ylh