From a5c9662caa52c03bc970b3b9e46be8ca6b463bff Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Wed, 4 Mar 2026 23:55:05 +0000 Subject: [PATCH] You can draw faces on bots (#95258) ## About The Pull Request Adds the ability to use a pen or crayon or spraycan to draw a face or eyes onto certain bots This allows people who miss the pareidolia of the old medkit sprite to fix it themselves You can clean it off again using most cleaning products, if you disagree image I think from looking at this screenshot apparently I haven't enabled pixel perfect 2x on my laptop for which i should be executed, sorry I didn't add any for ones not pictured here because I couldn't think of a good way to draw them but it's simple enough to expand. The face will be coloured with whatever ink you used to draw it, I was just lazy and only used a regular pen in this picture How are all of the non-white drawing implements adding that white circle? Don't worry about it This comes in the form of a component so if you want to make other things easily graffitable then you can do that I ironically called it the defaceable component even though it's used to add faces ## Why It's Good For The Game It seems like something people have been wanting and it seemed like a cute idea ## Changelog :cl: add: You can draw faces onto some bots using a pen, crayon, or spraycan /:cl: --- code/__DEFINES/traits/declarations.dm | 2 + code/_globalvars/traits/_traits.dm | 1 + code/datums/components/defaceable.dm | 103 ++++++++++++++++++ code/modules/mob/living/basic/bots/_bots.dm | 9 ++ .../living/basic/bots/cleanbot/cleanbot.dm | 1 + .../mob/living/basic/bots/medbot/medbot.dm | 33 ++++-- .../mob/living/basic/minebots/minebot.dm | 5 + .../mob/living/simple_animal/bot/ed209bot.dm | 6 + icons/mob/silicon/aibot_faces.dmi | Bin 0 -> 1318 bytes tgstation.dme | 1 + 10 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 code/datums/components/defaceable.dm create mode 100644 icons/mob/silicon/aibot_faces.dmi diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 525ee27eb9e..7f335afed04 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1235,6 +1235,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_ELEVATING_OBJECT "elevating_object" /// From [/datum/element/elevation_core] for purpose of checking if the turf has the trait from an instance of the element #define TRAIT_ELEVATED_TURF "elevated_turf" +/// From [/datum/component/defaceable] marks that something has been... marked +#define TRAIT_DEFACED "defaced" /// This item is currently under the control of telekinesis #define TRAIT_TELEKINESIS_CONTROLLED "telekinesis_controlled" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index d60de14068b..8fff1e9688e 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -16,6 +16,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_CLIMBABLE" = TRAIT_CLIMBABLE, "TRAIT_COMMISSIONED" = TRAIT_COMMISSIONED, "TRAIT_CURRENTLY_CLEANING" = TRAIT_CURRENTLY_CLEANING, + "TRAIT_DEFACED" = TRAIT_DEFACED, "TRAIT_DRIED" = TRAIT_DRIED, "TRAIT_DRYABLE" = TRAIT_DRYABLE, "TRAIT_ELEVATING_OBJECT" = TRAIT_ELEVATING_OBJECT, diff --git a/code/datums/components/defaceable.dm b/code/datums/components/defaceable.dm new file mode 100644 index 00000000000..645df67a533 --- /dev/null +++ b/code/datums/components/defaceable.dm @@ -0,0 +1,103 @@ +/// Lets you graffiti on an object +/datum/component/defaceable + /// Icon file from which to draw our overlay + var/icon + /// Icon states to draw from our file, optionally use as a list key for TRUE to skip recolouring that icon state + var/list/icon_states + /// String description of what you have drawn + var/drawing_of + /// What colour have we been drawn with? + var/ink_colour + /// Optional callback called when we are drawn on, we pass in the ink colour + var/datum/callback/on_defaced + +/datum/component/defaceable/Initialize(icon, list/icon_states, drawing_of, datum/callback/on_defaced) + if (!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + src.icon = icon + src.icon_states = icon_states + src.drawing_of = drawing_of + src.on_defaced = on_defaced + +/datum/component/defaceable/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_drawn)) + RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_hovered)) + var/atom/atom_parent = parent + atom_parent.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1 + +/datum/component/defaceable/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, COMSIG_ATOM_EXAMINE, COMSIG_COMPONENT_CLEAN_ACT, COMSIG_ATOM_UPDATE_OVERLAYS)) + var/atom/atom_parent = parent + atom_parent.update_appearance(UPDATE_OVERLAYS) + +/datum/component/defaceable/Destroy(force = FALSE) + on_defaced = null + return ..() + +/// Inform people that they can mess us up +/datum/component/defaceable/proc/on_hovered(atom/source, list/context, obj/item/held_item, mob/user) + if (HAS_TRAIT(source, TRAIT_DEFACED)) + if (is_type_in_list(held_item, list(/obj/item/reagent_containers/spray, /obj/item/soap, /obj/item/rag))) + context[SCREENTIP_CONTEXT_LMB] = "Clean" + else + if (istype(held_item, /obj/item/pen) || istype(held_item, /obj/item/toy/crayon)) + context[SCREENTIP_CONTEXT_LMB] = "Draw [drawing_of ? drawing_of : "on"]" + return CONTEXTUAL_SCREENTIP_SET + +/// See if someone can draw on us +/datum/component/defaceable/proc/on_drawn(atom/source, mob/living/user, obj/item/tool) + SIGNAL_HANDLER + if (user.combat_mode) + return + if(istype(tool, /obj/item/toy/crayon)) + var/obj/item/toy/crayon/crayon = tool + ink_colour = crayon.paint_color + else if(istype(tool, /obj/item/pen)) + var/obj/item/pen/pen = tool + ink_colour = pen.colour + + if (!ink_colour) + return + + ADD_TRAIT(parent, TRAIT_DEFACED, ref(src)) + + on_defaced?.Invoke(ink_colour) + + RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(on_cleaned)) + + if (drawing_of) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined)) + + if (icon && icon_states) + RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays)) + source.update_appearance(UPDATE_OVERLAYS) + + UnregisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION) + + return ITEM_INTERACT_SUCCESS + +/// Render our beautiful drawing +/datum/component/defaceable/proc/on_update_overlays(atom/source, list/overlays) + SIGNAL_HANDLER + for (var/state in icon_states) + var/mutable_appearance/appearance = mutable_appearance(icon, state) + if (!icon_states[state]) + appearance.color = ink_colour + overlays += appearance + +/// Wash it off +/datum/component/defaceable/proc/on_cleaned(atom/source, clean_types) + SIGNAL_HANDLER + if (!(clean_types & (CLEAN_WASH|CLEAN_SCRUB))) + return + + REMOVE_TRAIT(parent, TRAIT_DEFACED, ref(src)) + RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_drawn)) + UnregisterSignal(parent, list(COMSIG_ATOM_EXAMINE, COMSIG_COMPONENT_CLEAN_ACT, COMSIG_ATOM_UPDATE_OVERLAYS)) + source.update_appearance(UPDATE_OVERLAYS) + +/// See it there +/datum/component/defaceable/proc/on_examined(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += span_notice("Someone has crudely drawn [drawing_of] on [source.p_them()].") diff --git a/code/modules/mob/living/basic/bots/_bots.dm b/code/modules/mob/living/basic/bots/_bots.dm index 40fa7e51dac..15d983b8745 100644 --- a/code/modules/mob/living/basic/bots/_bots.dm +++ b/code/modules/mob/living/basic/bots/_bots.dm @@ -108,6 +108,8 @@ GLOBAL_LIST_INIT(command_strings, list( var/list/original_allies /// If true we will offer this COOLDOWN_DECLARE(offer_ghosts_cooldown) + /// List of overlays to add to the bot if someone has drawn on it, index to a boolean of whether it should ignore paint colour + var/list/facepaint_overlays /mob/living/basic/bot/Initialize(mapload) . = ..() @@ -149,6 +151,13 @@ GLOBAL_LIST_INIT(command_strings, list( if(mapload && is_station_level(z) && (bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT) && (bot_mode_flags & BOT_MODE_ROUNDSTART_POSSESSION)) enable_possession(mapload = mapload) + if (length(facepaint_overlays)) + AddComponent(/datum/component/defaceable, \ + icon = 'icons/mob/silicon/aibot_faces.dmi', \ + icon_states = facepaint_overlays, \ + drawing_of = "a face", \ + ) + pa_system = (isnull(announcement_type)) ? new(src, automated_announcements = generate_speak_list()) : new announcement_type(src, automated_announcements = generate_speak_list()) pa_system.Grant(src) ai_controller.set_blackboard_key(BB_ANNOUNCE_ABILITY, pa_system) diff --git a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm index 64f0b2cfd6a..4d26f285838 100644 --- a/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm +++ b/code/modules/mob/living/basic/bots/cleanbot/cleanbot.dm @@ -19,6 +19,7 @@ possessed_message = "You are a cleanbot! Clean the station to the best of your ability!" ai_controller = /datum/ai_controller/basic_controller/bot/cleanbot path_image_color = "#993299" + facepaint_overlays = list("cleanbot" = FALSE, "cleanbot_highlight" = TRUE) ///the bucket used to build us. var/obj/item/reagent_containers/cup/bucket/build_bucket ///Flags indicating what kind of cleanables we should scan for to set as our target to clean. diff --git a/code/modules/mob/living/basic/bots/medbot/medbot.dm b/code/modules/mob/living/basic/bots/medbot/medbot.dm index 1d2b157ce6c..3e6a22b0c6d 100644 --- a/code/modules/mob/living/basic/bots/medbot/medbot.dm +++ b/code/modules/mob/living/basic/bots/medbot/medbot.dm @@ -28,6 +28,9 @@ announcement_type = /datum/action/cooldown/bot_announcement/medbot path_image_color = "#d9d9f4" + ///Because of our animation we need to handle drawn-on overlays ourself + var/painted_face_colour + ///anouncements when we find a target to heal var/static/list/wait_announcements = list( MEDIBOT_VOICED_HOLD_ON = 'sound/mobs/non-humanoids/medbot/coming.ogg', @@ -140,6 +143,11 @@ post_tipped_callback = CALLBACK(src, PROC_REF(after_tip_over)), \ post_untipped_callback = CALLBACK(src, PROC_REF(after_righted))) + AddComponent(/datum/component/defaceable, \ + drawing_of = "eyes", \ + on_defaced = CALLBACK(src, PROC_REF(on_defaced)), \ + ) + var/static/list/hat_offsets = list(4,-9) var/static/list/remove_hat = list(SIGNAL_ADDTRAIT(TRAIT_MOB_TIPPED)) var/static/list/prevent_checks = list(TRAIT_MOB_TIPPED) @@ -193,13 +201,24 @@ if(!(medical_mode_flags & MEDBOT_STATIONARY_MODE)) . += mutable_appearance(icon, "[base_icon_state]_overlay_wheels") - if(HAS_TRAIT(src, TRAIT_INCAPACITATED)) - . += mutable_appearance(icon, "[base_icon_state]_overlay_incapacitated") - . += emissive_appearance(icon, "[base_icon_state]_overlay_incapacitated", src, alpha = src.alpha) - else if(bot_mode_flags & BOT_MODE_ON) - var/mode_suffix = mode == BOT_HEALING ? "active" : "idle" - . += mutable_appearance(icon, "[base_icon_state]_overlay_on_[mode_suffix]") - . += emissive_appearance(icon, "[base_icon_state]_overlay_on_[mode_suffix]", src, alpha = src.alpha) + var/mode_suffix = mode == BOT_HEALING ? "active" : "idle" + if (HAS_TRAIT(src, TRAIT_DEFACED)) + var/mutable_appearance/face = mutable_appearance('icons/mob/silicon/aibot_faces.dmi', "medbot_[mode_suffix]") + face.color = painted_face_colour + . += face + . += mutable_appearance('icons/mob/silicon/aibot_faces.dmi', "medbot_highlight_[mode_suffix]") + else + if(HAS_TRAIT(src, TRAIT_INCAPACITATED)) + . += mutable_appearance(icon, "[base_icon_state]_overlay_incapacitated") + . += emissive_appearance(icon, "[base_icon_state]_overlay_incapacitated", src, alpha = src.alpha) + else if(bot_mode_flags & BOT_MODE_ON) + . += mutable_appearance(icon, "[base_icon_state]_overlay_on_[mode_suffix]") + . += emissive_appearance(icon, "[base_icon_state]_overlay_on_[mode_suffix]", src, alpha = src.alpha) + +/// Set our drawn-on ink colour +/mob/living/basic/bot/medbot/proc/on_defaced(ink_colour) + SIGNAL_HANDLER + painted_face_colour = ink_colour //this is sin /mob/living/basic/bot/medbot/generate_speak_list() diff --git a/code/modules/mob/living/basic/minebots/minebot.dm b/code/modules/mob/living/basic/minebots/minebot.dm index 212edad7da7..0409095f8a8 100644 --- a/code/modules/mob/living/basic/minebots/minebot.dm +++ b/code/modules/mob/living/basic/minebots/minebot.dm @@ -69,6 +69,11 @@ AddElement(/datum/element/death_drops, /obj/effect/decal/cleanable/blood/gibs/robot_debris/old) add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE), INNATE_TRAIT) AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE) + AddComponent(/datum/component/defaceable, \ + icon = 'icons/mob/silicon/aibot_faces.dmi', \ + icon_states = list("minebot" = FALSE, "minebot_highlight" = TRUE), \ + drawing_of = "a face", \ + ) var/static/list/innate_actions = list( /datum/action/cooldown/mob_cooldown/missile_launcher = BB_MINEBOT_MISSILE_ABILITY, diff --git a/code/modules/mob/living/simple_animal/bot/ed209bot.dm b/code/modules/mob/living/simple_animal/bot/ed209bot.dm index 5a615407f9a..945d0c2eba9 100644 --- a/code/modules/mob/living/simple_animal/bot/ed209bot.dm +++ b/code/modules/mob/living/simple_animal/bot/ed209bot.dm @@ -27,6 +27,12 @@ . = ..() set_weapon() //giving it the right projectile and firing sound. + AddComponent(/datum/component/defaceable, \ + icon = 'icons/mob/silicon/aibot_faces.dmi', \ + icon_states = list("ed209" = FALSE, "ed209_highlight" = TRUE), \ + drawing_of = "a face", \ + ) + /mob/living/simple_animal/bot/secbot/ed209/bot_reset() ..() set_weapon() diff --git a/icons/mob/silicon/aibot_faces.dmi b/icons/mob/silicon/aibot_faces.dmi new file mode 100644 index 0000000000000000000000000000000000000000..5bb05a1d60d86198c892b862e94bc5df53a30264 GIT binary patch literal 1318 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zcalN2)?1N?cNllZ!G7N;32F7#J$% ztnEL@)#M=1c7C_N;R{ozv(B>LguE&)gm+w>d#CX#Z*0%Kea$TrD_IPuOaA!RUteRf zeEOOOmGeIuV*R(VGJdps@W$~}ra(c5#buAJWsfgfzZ77p;9YfTm$qiJTzc0oIhK|= zcdc$d*ipY}R#xtcYx|d`Jk5w$&_74huE<(*yOEAp?g?pr99-geXi4K#}|FAyXwq{Y?^7ak}1{PUQ7srr_IdAV4 z2F-SmXivQKQB1Ba`>ODZjfWC#+W#)Tb;)Og%IUPoy7R`DU#`d%DvVcMYxI`x(}MHkT7UX5za% z`+a?g#M*L(DL0iGrg!hx->bK0|Ei@1T3MTz1rla1za%|l*~?ssw(Z*TRg!i3T}uu6 z9@Wo({<@&{ch1}Szh}xbO26UR6)1CHQvM-}DY9=EX1&X}cmMwXmT!Ol+=hDDqWq%+g> z0-mQi7sT5=pHjr^-r!^{<$o{7ruyvd^=p4KToQk9;m0qY>KzWAC;z{qzL!gO75)0W|9I%t#p^r+q!Kx5jC|9*FHa7 z=<-qUz@ug9SNHL1@l+KoQ9oEI(c@g;d;`dz)z3uN0C(dV% zXjoHtgCT(zo0AF_7s}XAi_7?S_xPopt-4wH!HfcLOYG)H+qdPN{q7T2yYo%9_1e2` z+yDI*0!Bly0`5qd&2r&a=!Ce&lY3XmFv`ich%YMC*Kbq4bvewuWashi;mg=(&t*C* zzUQdFzL_->Pjjn(eU0=K0x^15=0N%W7mv(0|GSkHePdba&sK>=maIzX=^<~E$(J=> zOAb^6lSrL_n~6IpvCKYm{*Kg;}W*ztRYh$MIG z8RL~%LU&$&d1$hApWD{F>w*owS7-5szb^A_+kMfZPyYHo3Gesm_gNgaXf1b$_T_n3 z`^n&U_*;$*SC2fHr_R5*-mkIa*=`3Sv+S05)80hhV*