Files
MrMelbertandGitHub 62ad8d3520 Allows arbitrary/custom/"unlimited" bodypart overlay layers (#96684)
## About The Pull Request

`EXTERNAL_FRONT`, `EXTERNAL_ADJACENT`, and `EXTERNAL_BEHIND` are no
longer bitflags
Instead they are strings, that correspond to the sprite's icon state's
postfix ie `wings_FRONT` / `wings_ADJ`

Bodypart overlays now define their layers in terms of `postfix` to
`rendering layer`
For example wings are defined as:
```dm
	layers = list(
		EXTERNAL_FRONT = BODY_FRONT_LAYER,
		EXTERNAL_ADJACENT = BODY_ADJ_LAYER,
		EXTERNAL_BEHIND = BODY_BEHIND_LAYER,
	)
```
Which translates to
```dm
	layers = list(
		"FRONT" = 2.5,
		"ADJ" = 22.9,
		"BEHIND" = 32.2,
	)
```
## Why It's Good For The Game

What does this mean?

One, you are no longer constricted to the three existing layers when
adding a bodypart overlay. You can decide to put it on whatever layer
you need...
```dm
	layers = list(
		"FRONT" = 2.4, // I specifically need this to render above other front overlays!
	)
```

Two, you can easily add a new layer without needing to mess with core
bodypart code at all
```dm
	layers = list(
		"FRONT_HIGHER" = 2.4, // I need a special layer
		"FRONT" = 2.3, 
	)
```

Three, you don't have to use the `EXTERNAL_FRONT` `EXTERNAL_ADJACENT`
etc. at all if you don't want to. You can organize your layers however
you want...
```dm
	layers = list(
		// I can make my icon states `wings_top` and `wings_bottom` instead of `wings_FRONT` and `wings_BEHIND` to make it easier to parse
		"top" = BODY_ADJ_LAYER, 
		"bottom" = BODY_BEHIND_LAYER, 
	)
```

Ultimately, this makes it a ton easier to work with bodypart overlays,
as you no longer need to learn what FRONT/ADJ/BEHIND means. You can just
add your sprites and define your layers and you're done

## Changelog

🆑 Melbert
refactor: Surprise, a follow up refactor to species part rendering,
report any oddities with them (wings/snouts/tails/etc)
/🆑
2026-07-02 11:41:31 +02:00

242 lines
8.3 KiB
Plaintext

#define SYNDIE_DRAW_TIME 3 SECONDS
// Extending the existing spraycan item was more trouble than it was worth, I don't want or need this to be able to draw arbitrary shapes.
/obj/item/traitor_spraycan
name = "seditious spraycan"
desc = "This spraycan deploys a subversive pattern containing subliminal priming agents over a 3x3 area. Contains enough primer for just one final coating."
icon = 'icons/obj/art/crayons.dmi'
icon_state = "deathcan"
worn_icon_state = "spraycan"
inhand_icon_state = "spraycan"
lefthand_file = 'icons/mob/inhands/equipment/hydroponics_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/hydroponics_righthand.dmi'
w_class = WEIGHT_CLASS_SMALL
var/paint_color = "#780000"
var/static/list/no_draw_turfs = typecacheof(list(/turf/open/space, /turf/open/openspace, /turf/open/lava, /turf/open/chasm))
/// Are we currently drawing? Used to prevent spam clicks for do_while
var/drawing_rune = FALSE
/// Set to true if we finished drawing something, this spraycan is now useless
var/expended = FALSE
/obj/item/traitor_spraycan/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if (!check_allowed_items(interacting_with) || !isliving(user))
return NONE
if (expended)
user.balloon_alert(user, "all out of paint...")
return ITEM_INTERACT_BLOCKING
if (drawing_rune)
user.balloon_alert(user, "already busy!")
return ITEM_INTERACT_BLOCKING
if (isturf(interacting_with))
try_draw_new_rune(user, interacting_with)
return ITEM_INTERACT_SUCCESS
if (istype(interacting_with, /obj/effect/decal/cleanable/traitor_rune))
try_complete_rune(user, interacting_with)
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
/**
* Attempt to draw a rune on [target_turf].
* Shamelessly adapted from the heretic rune drawing process.
*
* Arguments
* * user - the mob drawing the rune
* * target_turf - the place the rune's being drawn
*/
/obj/item/traitor_spraycan/proc/try_draw_new_rune(mob/living/user, turf/target_turf)
for(var/turf/nearby_turf as anything in RANGE_TURFS(1, target_turf))
if (!isopenturf(nearby_turf) || is_type_in_typecache(nearby_turf, no_draw_turfs))
user.balloon_alert(user, "you need a clear 3x3 area!")
return
draw_rune(user, target_turf)
/**
* Draw your stage one rune on the ground and store it.
*
* Arguments
* * user - the mob drawing the rune
* * target_turf - the place the rune's being drawn
*/
/obj/item/traitor_spraycan/proc/draw_rune(mob/living/user, turf/target_turf)
if (!try_draw_step("drawing outline...", user, target_turf))
return
try_complete_rune(user, new /obj/effect/decal/cleanable/traitor_rune(target_turf))
/**
* Holder for repeated code to do something after a message and a set amount of time.
*
* Arguments
* * output - a string to show when you start the process
* * user - the mob drawing the rune
* * target - what they're trying to draw, or the place they are trying to draw on
*/
/obj/item/traitor_spraycan/proc/try_draw_step(start_output, mob/living/user, atom/target)
drawing_rune = TRUE
user.balloon_alert(user, "[start_output]")
var/wait_time = SYNDIE_DRAW_TIME
if(HAS_TRAIT(user, TRAIT_TAGGER))
wait_time *= 0.5
if(!do_after(user, wait_time, target, hidden = TRUE, extra_checks = CALLBACK(src, PROC_REF(adjacency_check), user, target)))
user.balloon_alert(user, "interrupted!")
drawing_rune = FALSE
return FALSE
playsound(src, 'sound/effects/spray.ogg', 5, TRUE, 5)
drawing_rune = FALSE
return TRUE
#define RUNE_STAGE_OUTLINE 0
#define RUNE_STAGE_COLOURED 1
#define RUNE_STAGE_COMPLETE 2
#define RUNE_STAGE_REMOVABLE 3
/**
* Try to upgrade a floor rune to its next stage.
*
* Arguments
* * user - the mob drawing the rune
* * target_turf - the place the rune's being drawn
*/
/obj/item/traitor_spraycan/proc/try_complete_rune(mob/living/user, obj/effect/decal/cleanable/traitor_rune/rune)
switch(rune.drawn_stage)
if (RUNE_STAGE_OUTLINE)
if (!try_draw_step("... finalising design...", user, rune))
return
if (!rune)
user.balloon_alert(user, "graffiti was destroyed!")
return
rune.set_stage(RUNE_STAGE_COLOURED)
try_complete_rune(user, rune)
if (RUNE_STAGE_COLOURED)
if (!try_draw_step("... applying final coating...", user, rune))
return
if (!rune)
user.balloon_alert(user, "graffiti was destroyed!")
return
user.balloon_alert(user, "finished!")
rune.set_stage(RUNE_STAGE_COMPLETE)
expended = TRUE
desc = "A suspicious looking spraycan, it's all out of paint."
SEND_SIGNAL(src, COMSIG_TRAITOR_GRAFFITI_DRAWN, rune)
if (RUNE_STAGE_COMPLETE, RUNE_STAGE_REMOVABLE)
user.balloon_alert(user, "all done!")
/// Copying the functionality from normal spraycans, but doesn't need all the optional checks
/obj/item/traitor_spraycan/suicide_act(mob/living/user)
if(expended)
user.visible_message(span_suicide("[user] shakes up [src] with a rattle and lifts it to [user.p_their()] mouth, but nothing happens!"))
user.say("MEDIOCRE!!", forced="spraycan suicide")
return SHAME
var/mob/living/carbon/human/suicider = user
user.visible_message(span_suicide("[user] shakes up [src] with a rattle and lifts it to [user.p_their()] mouth, spraying paint across [user.p_their()] teeth!"))
user.say("WITNESS ME!!", forced="spraycan suicide")
playsound(src, 'sound/effects/spray.ogg', 5, TRUE, 5)
suicider.AddComponent(/datum/component/face_decal, "spray", list(EXTERNAL_ADJACENT = BODY_ADJ_LAYER), paint_color)
return OXYLOSS
///Checks if the user is still adjacent to the target (used for do_after extra_checks)
/obj/item/traitor_spraycan/proc/adjacency_check(mob/user, atom/target)
if(!user.Adjacent(target))
user.balloon_alert(user, "moved too far away!")
return FALSE
return TRUE
/obj/effect/decal/cleanable/traitor_rune
name = "syndicate graffiti"
desc = "It looks like it's going to be... the Syndicate logo?"
icon = 'icons/effects/96x96.dmi'
icon_state = "traitor_rune_outline"
pixel_x = -32
pixel_y = -32
gender = NEUTER
mergeable_decal = FALSE
resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF
clean_type = CLEAN_TYPE_HARD_DECAL
plane = FLOOR_PLANE
layer = RUNE_LAYER
var/slip_time = 6 SECONDS
var/slip_flags = NO_SLIP_WHEN_WALKING
/// The stage of drawing we have reached
var/drawn_stage = RUNE_STAGE_OUTLINE
/// Proximity sensor to make people sad if they're nearby
var/datum/proximity_monitor/advanced/demoraliser/demoraliser
/// Whether we protect the rune from being cleaned up
var/clean_proof = FALSE
/// Timer until the rune can be cleaned up off the floor
var/protected_timer
/obj/effect/decal/cleanable/traitor_rune/Destroy()
deltimer(protected_timer)
QDEL_NULL(demoraliser)
return ..()
/obj/effect/decal/cleanable/traitor_rune/HasProximity(atom/movable/proximity_check_mob)
if (isliving(proximity_check_mob) && get_dist(proximity_check_mob, src) <= 1)
slip(proximity_check_mob)
return ..()
/**
* Makes someone fall over. If it's not the traitor, this counts as demoralising the crew.
*
* Arguments
* * victim - whoever just slipped, point and laugh at them
*/
/obj/effect/decal/cleanable/traitor_rune/proc/slip(mob/living/victim)
if(victim.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return
if (!victim.slip(slip_time, src, slip_flags))
return
SEND_SIGNAL(src, COMSIG_TRAITOR_GRAFFITI_SLIPPED, victim.mind)
/**
* Sets the "drawing stage" of the rune.
* This affects the appearance, behaviour, and description of the effect.
*
* Arguments
* * stage - new stage to apply
*/
/obj/effect/decal/cleanable/traitor_rune/proc/set_stage(stage)
drawn_stage = stage
switch(drawn_stage)
if (RUNE_STAGE_OUTLINE)
icon_state = "traitor_rune_outline"
desc = "It looks like it's going to be... the Syndicate logo?"
if (RUNE_STAGE_COLOURED, RUNE_STAGE_REMOVABLE)
icon_state = "traitor_rune_done"
desc = "A large depiction of the Syndicate logo."
clean_proof = FALSE
if (RUNE_STAGE_COMPLETE)
icon_state = "traitor_rune_sheen"
desc = "A large depiction of the Syndicate logo. It looks slippery."
var/datum/demoralise_moods/graffiti/mood_category = new()
demoraliser = new(src, 7, TRUE, mood_category)
clean_proof = TRUE
protected_timer = addtimer(CALLBACK(src, PROC_REF(set_stage), RUNE_STAGE_REMOVABLE), 5 MINUTES)
/obj/effect/decal/cleanable/traitor_rune/wash(clean_types)
if (clean_proof)
return NONE
return ..()
#undef SYNDIE_DRAW_TIME
#undef RUNE_STAGE_COLOURED
#undef RUNE_STAGE_COMPLETE
#undef RUNE_STAGE_OUTLINE
#undef RUNE_STAGE_REMOVABLE