There will be (colorful) blood: datumizes bloodtypes, greyscales blood sprites, and fixes a lot of inconsistencies with gibs and forensic data (#90593)

## About The Pull Request


This PR:

- Converts all of the blood types into their own datums, which can be
set up to have their own colors, descriptions, and other fun unique
properties. For example, the clown blood that is constantly randomizing
itself.

- Converts all the blood decals into greyscale, which in turn eliminates
the need for separate xeno sprites. They both use the same ones now.

- Audit of blood splatters/gibs/bodyparts/organs to make sure that they
are getting the correct forensic data applied to them.

- For the admins: Adds a clown blood smite.

My primary goal with was to make the appearance of the new sprites look
almost indistinguishable to the original ones.

I consider this a "first pass", as in there are still some further
refactors I would like to do on the backend side, but am satisfied with
it enough to push it forward as a first step towards a better blood
system! I didn't want to do too much at once because of A) fatigue and
B) easier to test things to make sure I'm not breaking something
important this way.

This has been test-merged on Nova for over a week now and has been going
great, so I finally got around to upstreaming the bones to TG. Although
I did test it a bit you may want to TM it just in case I missed some
things when copying it over.
This commit is contained in:
Bloop
2025-04-29 18:33:34 -06:00
committed by Shadow-Quill
parent f6da91e289
commit 4ba1520df8
100 changed files with 1007 additions and 364 deletions
@@ -150,6 +150,9 @@
return null
/obj/effect/decal/cleanable/blood/get_blood_color()
return color || ..()
/obj/effect/decal/cleanable/proc/handle_merge_decal(obj/effect/decal/cleanable/merger)
if(!merger)
return
@@ -4,16 +4,21 @@
name = "xeno blood"
desc = "It's green and acidic. It looks like... <i>blood?</i>"
icon = 'icons/effects/blood.dmi'
icon_state = "xfloor1"
random_icon_states = list("xfloor1", "xfloor2", "xfloor3", "xfloor4", "xfloor5", "xfloor6", "xfloor7")
icon_state = "floor1"
random_icon_states = list("floor1", "floor2", "floor3", "floor4", "floor5", "floor6", "floor7")
bloodiness = BLOOD_AMOUNT_PER_DECAL
blood_state = BLOOD_STATE_XENO
beauty = -250
clean_type = CLEAN_TYPE_BLOOD
/obj/effect/decal/cleanable/xenoblood/add_blood_DNA(list/blood_DNA, no_visuals = FALSE)
. = ..()
if(!no_visuals && length(blood_DNA))
color = get_blood_dna_color(blood_DNA)
/obj/effect/decal/cleanable/xenoblood/Initialize(mapload)
. = ..()
add_blood_DNA(list("UNKNOWN DNA" = "X*"))
add_blood_DNA(list("UNKNOWN DNA" = get_blood_type(BLOOD_TYPE_XENO)))
/obj/effect/decal/cleanable/xenoblood/xsplatter
random_icon_states = list("xgibbl1", "xgibbl2", "xgibbl3", "xgibbl4", "xgibbl5")
@@ -33,6 +38,13 @@
/obj/effect/decal/cleanable/xenoblood/xgibs/Initialize(mapload)
. = ..()
RegisterSignal(src, COMSIG_MOVABLE_PIPE_EJECTING, PROC_REF(on_pipe_eject))
update_appearance(UPDATE_OVERLAYS)
/obj/effect/decal/cleanable/xenoblood/xgibs/update_overlays()
. = ..()
var/mutable_appearance/gib_overlay = mutable_appearance(icon, "[icon_state]-overlay", appearance_flags = KEEP_APART|RESET_COLOR)
if(gib_overlay)
. += gib_overlay
/obj/effect/decal/cleanable/xenoblood/xgibs/proc/streak(list/directions, mapload=FALSE)
SEND_SIGNAL(src, COMSIG_GIBS_STREAK, directions)
@@ -45,7 +57,8 @@
for (var/i in 1 to range)
var/turf/my_turf = get_turf(src)
if(!isgroundlessturf(my_turf) || GET_TURF_BELOW(my_turf))
new /obj/effect/decal/cleanable/xenoblood/xsplatter(my_turf)
var/obj/effect/decal/cleanable/xenoblood/xsplatter/new_splatter = new /obj/effect/decal/cleanable/xenoblood/xsplatter(my_turf)
new_splatter.add_blood_DNA(GET_ATOM_BLOOD_DNA(src))
if (!step_to(src, get_step(src, direction), 0))
break
return
@@ -106,9 +119,9 @@
random_icon_states = list("xgiblarvahead", "xgiblarvatorso")
/obj/effect/decal/cleanable/blood/xtracks
icon_state = "xtracks"
icon_state = "tracks"
random_icon_states = null
/obj/effect/decal/cleanable/blood/xtracks/Initialize(mapload)
. = ..()
add_blood_DNA(list("Unknown DNA" = "X*"))
add_blood_DNA(list("Unknown DNA" = get_blood_type(BLOOD_TYPE_XENO)))
@@ -8,11 +8,14 @@
bloodiness = BLOOD_AMOUNT_PER_DECAL
beauty = -100
clean_type = CLEAN_TYPE_BLOOD
color = BLOOD_COLOR_RED
var/should_dry = TRUE
var/dryname = "dried blood" //when the blood lasts long enough, it becomes dry and gets a new name
var/drydesc = "Looks like it's been here a while. Eew." //as above
var/drytime = 0
var/footprint_sprite = null
/// If we've dried already
var/has_dried
/obj/effect/decal/cleanable/blood/Initialize(mapload)
. = ..()
@@ -21,7 +24,7 @@
if(bloodiness)
start_drying()
else
dry()
dry(freshly_made = TRUE)
/obj/effect/decal/cleanable/blood/Destroy()
STOP_PROCESSING(SSobj, src)
@@ -31,6 +34,11 @@
if(world.time > drytime)
dry()
/obj/effect/decal/cleanable/blood/add_blood_DNA(list/blood_DNA, no_visuals = FALSE)
. = ..()
if(!no_visuals && length(blood_DNA))
color = get_blood_dna_color(blood_DNA)
/obj/effect/decal/cleanable/blood/proc/get_timer()
drytime = world.time + 3 MINUTES
@@ -39,19 +47,36 @@
START_PROCESSING(SSobj, src)
///This is what actually "dries" the blood. Returns true if it's all out of blood to dry, and false otherwise
/obj/effect/decal/cleanable/blood/proc/dry()
/obj/effect/decal/cleanable/blood/proc/dry(freshly_made = FALSE)
if(freshly_made)
start_drying()
return FALSE
if(bloodiness > 20)
bloodiness -= BLOOD_AMOUNT_PER_DECAL
get_timer()
return FALSE
else
name = dryname
desc = drydesc
bloodiness = 0
color = COLOR_GRAY //not all blood splatters have their own sprites... It still looks pretty nice
STOP_PROCESSING(SSobj, src)
name = dryname
has_dried = TRUE
desc = drydesc
bloodiness = 0
STOP_PROCESSING(SSobj, src)
// We're not using a matrix so we're free to use BlendRGB
if(!islist(color))
add_atom_colour(BlendRGB(color, COLOR_BLACK, 0.5), FIXED_COLOUR_PRIORITY)
return TRUE
// We're using a matrix, so we need to halve all values
var/list/blood_matrix = color
for(var/i in 1 to min(length(blood_matrix), 16))
if (length(blood_matrix) == 12 && i > 9) // Don't modify constants
break
if (length(blood_matrix) >= 16 && i % 4 == 0) // Don't modify alpha either
continue
blood_matrix[i] *= 0.5
color = blood_matrix
return TRUE
/obj/effect/decal/cleanable/blood/replace_decal(obj/effect/decal/cleanable/blood/C)
C.add_blood_DNA(GET_ATOM_BLOOD_DNA(src))
if (bloodiness)
@@ -63,7 +88,7 @@
icon_state = "floor1-old"
/obj/effect/decal/cleanable/blood/old/Initialize(mapload, list/datum/disease/diseases)
add_blood_DNA(list("Non-human DNA" = random_blood_type())) // Needs to happen before ..()
add_blood_DNA(list("Non-human DNA" = random_human_blood_type())) // Needs to happen before ..()
return ..()
/obj/effect/decal/cleanable/blood/splatter
@@ -132,17 +157,24 @@
. = ..()
AddElement(/datum/element/squish_sound)
RegisterSignal(src, COMSIG_MOVABLE_PIPE_EJECTING, PROC_REF(on_pipe_eject))
update_appearance(UPDATE_OVERLAYS)
/obj/effect/decal/cleanable/blood/gibs/Destroy()
return ..()
/obj/effect/decal/cleanable/blood/gibs/update_overlays()
. = ..()
var/mutable_appearance/gib_overlay = mutable_appearance(icon, "[icon_state]-overlay", appearance_flags = KEEP_APART|RESET_COLOR)
if(gib_overlay)
if(has_dried)
gib_overlay.color = COLOR_GRAY
. += gib_overlay
/obj/effect/decal/cleanable/blood/gibs/replace_decal(obj/effect/decal/cleanable/C)
return FALSE //Never fail to place us
/obj/effect/decal/cleanable/blood/gibs/dry()
/obj/effect/decal/cleanable/blood/gibs/dry(freshly_made = FALSE)
. = ..()
if(!.)
return
update_appearance(UPDATE_OVERLAYS)
//AddComponent(/datum/component/rot, 0, 5 MINUTES, 0.7) skyrat edit
/obj/effect/decal/cleanable/blood/gibs/ex_act(severity, target)
@@ -166,23 +198,27 @@
var/range = pick(0, 200; 1, 150; 2, 50; 3, 17; 50) //the 3% chance of 50 steps is intentional and played for laughs.
if(!step_to(src, get_step(src, direction), 0))
return
if(mapload)
for (var/i in 1 to range)
var/turf/my_turf = get_turf(src)
if(!isgroundlessturf(my_turf) || GET_TURF_BELOW(my_turf))
new /obj/effect/decal/cleanable/blood/splatter(my_turf)
if (!step_to(src, get_step(src, direction), 0))
break
if(!mapload)
var/datum/move_loop/loop = GLOB.move_manager.move_to(src, get_step(src, direction), delay = delay, timeout = range * delay, priority = MOVEMENT_ABOVE_SPACE_PRIORITY)
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(spread_movement_effects))
return
var/datum/move_loop/loop = GLOB.move_manager.move_to(src, get_step(src, direction), delay = delay, timeout = range * delay, priority = MOVEMENT_ABOVE_SPACE_PRIORITY)
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(spread_movement_effects))
for (var/i in 1 to range)
var/turf/my_turf = get_turf(src)
if(!isgroundlessturf(my_turf) || GET_TURF_BELOW(my_turf))
var/obj/effect/decal/cleanable/blood/splatter/new_splatter = new /obj/effect/decal/cleanable/blood/splatter(my_turf)
new_splatter.add_blood_DNA(GET_ATOM_BLOOD_DNA(src))
if (!step_to(src, get_step(src, direction), 0))
break
/obj/effect/decal/cleanable/blood/gibs/proc/spread_movement_effects(datum/move_loop/has_target/source)
SIGNAL_HANDLER
if(NeverShouldHaveComeHere(loc))
return
new /obj/effect/decal/cleanable/blood/splatter(loc)
var/obj/effect/decal/cleanable/blood/splatter/new_splatter = new /obj/effect/decal/cleanable/blood/splatter(loc)
new_splatter.add_blood_DNA(GET_ATOM_BLOOD_DNA(src))
/obj/effect/decal/cleanable/blood/gibs/up
icon_state = "gibup1"
@@ -220,7 +256,7 @@
/obj/effect/decal/cleanable/blood/gibs/old/Initialize(mapload, list/datum/disease/diseases)
. = ..()
setDir(pick(1,2,4,8))
add_blood_DNA(list("Non-human DNA" = random_blood_type()))
add_blood_DNA(list("Non-human DNA" = random_human_blood_type()))
AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 10)
dry()
@@ -237,7 +273,6 @@
/obj/effect/decal/cleanable/blood/drip/can_bloodcrawl_in()
return TRUE
//BLOODY FOOTPRINTS
/obj/effect/decal/cleanable/blood/footprints
name = "footprints"
@@ -318,33 +353,39 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
var/image/bloodstep_overlay = GLOB.bloody_footprints_cache["entered-[footprint_sprite]-[blood_state]-[Ddir]"]
if(!bloodstep_overlay)
GLOB.bloody_footprints_cache["entered-[footprint_sprite]-[blood_state]-[Ddir]"] = bloodstep_overlay = image(icon, "[blood_state]_[footprint_sprite]_enter", dir = Ddir)
bloodstep_overlay.color = color
. += bloodstep_overlay
if(exited_dirs & Ddir)
var/image/bloodstep_overlay = GLOB.bloody_footprints_cache["exited-[footprint_sprite]-[blood_state]-[Ddir]"]
if(!bloodstep_overlay)
GLOB.bloody_footprints_cache["exited-[footprint_sprite]-[blood_state]-[Ddir]"] = bloodstep_overlay = image(icon, "[blood_state]_[footprint_sprite]_exit", dir = Ddir)
bloodstep_overlay.color = color
. += bloodstep_overlay
/obj/effect/decal/cleanable/blood/footprints/examine(mob/user)
. = ..()
if((shoe_types.len + species_types.len) > 0)
. += "You recognise \the [src] as belonging to:"
for(var/sole in shoe_types)
var/obj/item/clothing/item = sole
var/article = initial(item.gender) == PLURAL ? "Some" : "A"
. += "[icon2html(initial(item.icon), user, initial(item.icon_state))] [article] <B>[initial(item.name)]</B>."
for(var/species in species_types)
// god help me
if(species == "unknown")
. += "Some <B>feet</B>."
else if(species == SPECIES_MONKEY)
. += "[icon2html('icons/mob/human/human.dmi', user, "monkey")] Some <B>monkey paws</B>."
else if(species == SPECIES_HUMAN)
. += "[icon2html('icons/mob/human/bodyparts.dmi', user, "default_human_l_leg")] Some <B>human feet</B>."
else
. += "[icon2html('icons/mob/human/bodyparts.dmi', user, "[species]_l_leg")] Some <B>[species] feet</B>."
if(length(shoe_types) + length(species_types) == 0)
return
. += "You recognise \the [src] as belonging to:"
for(var/sole in shoe_types)
var/obj/item/clothing/item = sole
var/article = initial(item.gender) == PLURAL ? "Some" : "A"
. += "[icon2html(initial(item.icon), user, initial(item.icon_state))] [article] <B>[initial(item.name)]</B>."
for(var/species in species_types)
// god help me
if(species == "unknown")
. += "Some <B>feet</B>."
else if(species == SPECIES_MONKEY)
. += "[icon2html('icons/mob/human/human.dmi', user, "monkey")] Some <B>monkey paws</B>."
else if(species == SPECIES_HUMAN)
. += "[icon2html('icons/mob/human/bodyparts.dmi', user, "default_human_l_leg")] Some <B>human feet</B>."
else
. += "[icon2html('icons/mob/human/bodyparts.dmi', user, "[species]_l_leg")] Some <B>[species] feet</B>."
/obj/effect/decal/cleanable/blood/footprints/replace_decal(obj/effect/decal/cleanable/blood/blood_decal)
if(blood_state != blood_decal.blood_state || footprint_sprite != blood_decal.footprint_sprite) //We only replace footprints of the same type as us
@@ -381,12 +422,12 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
if(splatter_strength)
src.splatter_strength = splatter_strength
/obj/effect/decal/cleanable/blood/hitsplatter/Destroy()
/obj/effect/decal/cleanable/blood/hitsplatter/proc/expire()
if(isturf(loc) && !skip)
playsound(src, 'sound/effects/wounds/splatter.ogg', 60, TRUE, -1)
if(blood_dna_info)
loc.add_blood_DNA(blood_dna_info)
return ..()
qdel(src)
/// Set the splatter up to fly through the air until it rounds out of steam or hits something
/obj/effect/decal/cleanable/blood/hitsplatter/proc/fly_towards(turf/target_turf, range)
@@ -402,65 +443,63 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
/obj/effect/decal/cleanable/blood/hitsplatter/proc/post_move(datum/move_loop/source)
SIGNAL_HANDLER
for(var/atom/iter_atom in get_turf(src))
for(var/atom/movable/iter_atom in loc)
if(hit_endpoint)
return
if(iter_atom == src || iter_atom.invisibility || iter_atom.alpha <= 0 || (isobj(iter_atom) && !iter_atom.density))
continue
if(splatter_strength <= 0)
break
if(isitem(iter_atom))
iter_atom.add_blood_DNA(blood_dna_info)
splatter_strength--
else if(ishuman(iter_atom))
var/mob/living/carbon/human/splashed_human = iter_atom
if(splashed_human.wear_suit)
splashed_human.wear_suit.add_blood_DNA(blood_dna_info)
splashed_human.update_worn_oversuit() //updates mob overlays to show the new blood (no refresh)
if(splashed_human.w_uniform)
splashed_human.w_uniform.add_blood_DNA(blood_dna_info)
splashed_human.update_worn_undersuit() //updates mob overlays to show the new blood (no refresh)
splatter_strength--
iter_atom.add_blood_DNA(blood_dna_info)
splatter_strength--
if(splatter_strength <= 0) // we used all the puff so we delete it.
qdel(src)
expire()
/obj/effect/decal/cleanable/blood/hitsplatter/proc/loop_done(datum/source)
SIGNAL_HANDLER
if(!QDELETED(src))
qdel(src)
expire()
/obj/effect/decal/cleanable/blood/hitsplatter/Bump(atom/bumped_atom)
if(!iswallturf(bumped_atom) && !istype(bumped_atom, /obj/structure/window))
qdel(src)
expire()
return
if(istype(bumped_atom, /obj/structure/window))
var/obj/structure/window/bumped_window = bumped_atom
if(!bumped_window.fulltile)
hit_endpoint = TRUE
qdel(src)
expire()
return
hit_endpoint = TRUE
if(isturf(prev_loc))
if(!isturf(prev_loc)) // This will only happen if prev_loc is not even a turf, which is highly unlikely.
abstract_move(bumped_atom)
skip = TRUE
//Adjust pixel offset to make splatters appear on the wall
if(istype(bumped_atom, /obj/structure/window))
land_on_window(bumped_atom)
else
var/obj/effect/decal/cleanable/blood/splatter/over_window/final_splatter = new(prev_loc)
final_splatter.pixel_x = (dir == EAST ? 32 : (dir == WEST ? -32 : 0))
final_splatter.pixel_y = (dir == NORTH ? 32 : (dir == SOUTH ? -32 : 0))
else // This will only happen if prev_loc is not even a turf, which is highly unlikely.
abstract_move(bumped_atom)
qdel(src)
expire()
return
abstract_move(bumped_atom)
skip = TRUE
//Adjust pixel offset to make splatters appear on the wall
if(istype(bumped_atom, /obj/structure/window))
land_on_window(bumped_atom)
return
var/obj/effect/decal/cleanable/blood/splatter/over_window/final_splatter = new(prev_loc)
final_splatter.add_blood_DNA(blood_dna_info)
final_splatter.pixel_x = (dir == EAST ? 32 : (dir == WEST ? -32 : 0))
final_splatter.pixel_y = (dir == NORTH ? 32 : (dir == SOUTH ? -32 : 0))
/// A special case for hitsplatters hitting windows, since those can actually be moved around, store it in the window and slap it in the vis_contents
/obj/effect/decal/cleanable/blood/hitsplatter/proc/land_on_window(obj/structure/window/the_window)
if(!the_window.fulltile)
return
var/obj/effect/decal/cleanable/blood/splatter/over_window/final_splatter = new
var/obj/effect/decal/cleanable/final_splatter = new /obj/effect/decal/cleanable/blood/splatter/over_window(prev_loc)
final_splatter.add_blood_DNA(blood_dna_info)
final_splatter.forceMove(the_window)
the_window.vis_contents += final_splatter
the_window.bloodied = TRUE
qdel(src)
expire()
@@ -313,8 +313,9 @@
name = "insect guts"
desc = "One bug squashed. Four more will rise in its place."
icon = 'icons/effects/blood.dmi'
icon_state = "xfloor1"
random_icon_states = list("xfloor1", "xfloor2", "xfloor3", "xfloor4", "xfloor5", "xfloor6", "xfloor7")
icon_state = "floor1"
random_icon_states = list("floor1", "floor2", "floor3", "floor4", "floor5", "floor6", "floor7")
color = BLOOD_COLOR_XENO
/obj/effect/decal/cleanable/confetti
name = "confetti"
@@ -9,8 +9,9 @@
var/list/gibtypes = list() //typepaths of the gib decals to spawn
var/list/gibamounts = list() //amount to spawn for each gib decal type we'll spawn.
var/list/gibdirections = list() //of lists of possible directions to spread each gib decal type towards.
var/blood_dna_info // Cached blood_dna_info in case we do not have a source mob
/obj/effect/gibspawner/Initialize(mapload, mob/living/source_mob, list/datum/disease/diseases)
/obj/effect/gibspawner/Initialize(mapload, mob/living/source_mob, list/datum/disease/diseases, blood_dna_info)
. = ..()
if(gibtypes.len != gibamounts.len)
@@ -32,14 +33,16 @@
var/list/dna_to_add //find the dna to pass to the spawned gibs. do note this can be null if the mob doesn't have blood. add_blood_DNA() has built in null handling.
if(source_mob)
if(blood_dna_info)
dna_to_add = blood_dna_info
else if(source_mob)
dna_to_add = source_mob.get_blood_dna_list() //ez pz
else if(gib_mob_type)
var/mob/living/temp_mob = new gib_mob_type(src) //generate a fake mob so that we pull the right type of DNA for the gibs.
dna_to_add = temp_mob.get_blood_dna_list()
qdel(temp_mob)
else
dna_to_add = list("Non-human DNA" = random_blood_type()) //else, generate a random bloodtype for it.
dna_to_add = list("Non-human DNA" = random_human_blood_type()) //else, generate a random bloodtype for it.
for(var/i in 1 to gibtypes.len)
@@ -66,7 +69,7 @@
gibamounts = list(2, 2, 1)
sound_vol = 40
/obj/effect/gibspawner/generic/Initialize(mapload)
/obj/effect/gibspawner/generic/Initialize(mapload, blood_dna_info)
if(!gibdirections.len)
gibdirections = list(list(WEST, NORTHWEST, SOUTHWEST, NORTH),list(EAST, NORTHEAST, SOUTHEAST, SOUTH), list())
return ..()
@@ -80,7 +83,7 @@
gib_mob_type = /mob/living/carbon/human
sound_vol = 50
/obj/effect/gibspawner/human/Initialize(mapload)
/obj/effect/gibspawner/human/Initialize(mapload, mob/living/source_mob, list/datum/disease/diseases, blood_dna_info)
if(!gibdirections.len)
gibdirections = list(
list(NORTH, NORTHEAST, NORTHWEST),
@@ -91,6 +94,9 @@
GLOB.alldirs,
list(),
)
if(!iscarbon(source_mob) && isnull(blood_dna_info))
return ..(blood_dna_info = list("Human DNA" = random_human_blood_type()))
return ..()
@@ -7,7 +7,14 @@
plane = GAME_PLANE
var/splatter_type = "splatter"
/obj/effect/temp_visual/dir_setting/bloodsplatter/Initialize(mapload, set_dir)
// set_color arg can be either a color string or a singleton /datum/blood_type to pull the color from
/obj/effect/temp_visual/dir_setting/bloodsplatter/Initialize(mapload, set_dir, set_color = BLOOD_COLOR_RED)
if(set_color)
var/datum/blood_type/blood_type = set_color
if(istype(blood_type))
color = blood_type.color
else
color = set_color
if(ISDIAGONALDIR(set_dir))
icon_state = "[splatter_type][pick(1, 2, 6)]"
else
@@ -44,6 +51,9 @@
/obj/effect/temp_visual/dir_setting/bloodsplatter/xenosplatter
splatter_type = "xsplatter"
/obj/effect/temp_visual/dir_setting/bloodsplatter/xenosplatter/Initialize(mapload, set_dir, set_color = GLOB.blood_types[/datum/blood_type/xeno::name])
return ..()
/obj/effect/temp_visual/dir_setting/speedbike_trail
name = "speedbike trails"
icon_state = "ion_fade"