From e1052fc62a0c572df57adda6d74809ec3dc1014e Mon Sep 17 00:00:00 2001 From: Ghom <42542238+Ghommie@users.noreply.github.com> Date: Wed, 24 May 2023 20:06:33 +0200 Subject: [PATCH] Massive Spraycan and Crayon Powercreep: smaller graffiti cost less to draw. (#75588) ## About The Pull Request Currently, drawing a small circle on the floor consumes as much charge as any other drawable but the larger horizontal ones that cost five times as much, which is a bit dull for non-cyborg players, having to burn through a ton of spraycans just to cover the floor in several small dots if ever attempting to do some floor art. This PR aims to reduce the cost of some of the smaller drawables, like "shortline", "smallbrush", "pawprints" etc., in virtue of the smaller area they cover. The balances changes may seem bit of an arbitrary and not include ones that are a bit on the edge between "small" and not "small", but I'd rather keep it simple than have other values such like "semi-small" integrated and snowball this balance PR into a more finnicky, disputable or not agreeable with version of itself. This also covers re-coloring and existent graffiti. Currently it always costs 2 charges to change its color regardless if it costed 1 or 5 to spray in the first place. With this PR, the costs should match that of when they were drawn. To boot, It also takes into account the tagger quirk now. Code had to be cleaned up and improved a bit for a smoother implementation of these changes. A fairly unrelated but small change: Trying to suicide with an empty spraycan will now fail, because it's empty. It's that simple. ## Why It's Good For The Game Enables non-cyborg players to engage in jolly floor-vandalizing-art in a more consistent, slightly more enjoyable way. ## Changelog :cl: balance: Drawing small graffiti like paw/foot/claw prints, small brush dots and short lines should cost half as many charges off your spraycan or crayon. balance: The cost of recoloring graffiti decals is now consistent with that of drawing them. balance: Trying to suicide with an empty spraycan will now fail, because it's empty. It's just that simple. /:cl: --- code/__DEFINES/art.dm | 5 + code/__DEFINES/dcs/signals/signals_object.dm | 3 +- code/game/objects/effects/decals/crayon.dm | 9 + code/game/objects/items/crayons.dm | 166 +++++++++---------- code/game/objects/structures/window.dm | 4 +- code/modules/power/floodlight.dm | 4 +- 6 files changed, 98 insertions(+), 93 deletions(-) diff --git a/code/__DEFINES/art.dm b/code/__DEFINES/art.dm index 83341ad5251..b6e9b885d91 100644 --- a/code/__DEFINES/art.dm +++ b/code/__DEFINES/art.dm @@ -9,3 +9,8 @@ #define TAB_LIBRARY 1 #define TAB_SECURE 2 #define TAB_PRIVATE 3 + +///cost defines for drawing graffiti: how many charges of a crayon or spraycan are used. +#define CRAYON_COST_SMALL 0.5 +#define CRAYON_COST_DEFAULT 1 +#define CRAYON_COST_LARGE 5 diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 7a336ec1d4e..03c680c88de 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -9,8 +9,9 @@ #define COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH "obj_default_unfasten_wrench" ///from base of /turf/proc/levelupdate(). (intact) true to hide and false to unhide #define COMSIG_OBJ_HIDE "obj_hide" -/// from /obj/item/toy/crayon/spraycan/afterattack: (color_is_dark) +/// from /obj/item/toy/crayon/spraycan/afterattack: (user, spraycan, color_is_dark) #define COMSIG_OBJ_PAINTED "obj_painted" + #define DONT_USE_SPRAYCAN_CHARGES (1<<0) // /obj/machinery signals diff --git a/code/game/objects/effects/decals/crayon.dm b/code/game/objects/effects/decals/crayon.dm index 40171d3cddd..17ff9c6d695 100644 --- a/code/game/objects/effects/decals/crayon.dm +++ b/code/game/objects/effects/decals/crayon.dm @@ -29,6 +29,15 @@ if(main) paint_colour = main add_atom_colour(paint_colour, FIXED_COLOUR_PRIORITY) + RegisterSignal(src, COMSIG_OBJ_PAINTED, PROC_REF(on_painted)) /obj/effect/decal/cleanable/crayon/NeverShouldHaveComeHere(turf/T) return isgroundlessturf(T) + +/obj/effect/decal/cleanable/crayon/proc/on_painted(datum/source, mob/user, obj/item/toy/crayon/spraycan/spraycan, is_dark_color) + SIGNAL_HANDLER + var/cost = spraycan.all_drawables[icon_state] || CRAYON_COST_DEFAULT + if (HAS_TRAIT(user, TRAIT_TAGGER)) + cost *= 0.5 + spraycan.use_charges(user, cost, requires_full = FALSE) + return DONT_USE_SPRAYCAN_CHARGES diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 2cc0f536e4b..724b35b80b5 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -14,6 +14,8 @@ #define AVAILABLE_SPRAYCAN_SPACE 8 // enough to fill one radial menu page +#define INFINITE_CHARGES -1 + /* * Crayons */ @@ -49,7 +51,7 @@ /// Dictates how large of an area we cover with our paint var/paint_mode = PAINT_NORMAL - /// Number of times this item can be used, -1 for unlimited + /// Number of times this item can be used, INFINITE_CHARGES for unlimited var/charges = 30 /// Number of remaining charges var/charges_left @@ -83,7 +85,12 @@ /// Whether to play a sound after using var/post_noise = FALSE - /// List of selectable graffiti options + /** + * List of selectable graffiti options + * If an associated value is present, the graffiti has its own cost + * otherwise it'll be the default value. + * Ditto with the other other lists below. + */ var/static/list/graffiti = list( "amyjon", "antilizard", @@ -138,7 +145,7 @@ "largebrush", "scroll", "shotgun", - "smallbrush", + "smallbrush" = CRAYON_COST_SMALL, "snake", "splatter", "stickman", @@ -148,14 +155,14 @@ ) /// List of selectable orientable options var/static/list/oriented = list( - "arrow", + "arrow" = CRAYON_COST_SMALL, "body", - "chevron", - "clawprint", - "footprint", + "chevron" = CRAYON_COST_SMALL, + "clawprint" = CRAYON_COST_SMALL, + "footprint" = CRAYON_COST_SMALL, "line", - "pawprint", - "shortline", + "pawprint" = CRAYON_COST_SMALL, + "shortline" = CRAYON_COST_SMALL, "thinline", ) /// List of selectable rune options @@ -181,10 +188,10 @@ ) /// List of selectable large options var/static/list/graffiti_large_h = list( - "furrypride", - "paint", - "secborg", - "yiffhell", + "furrypride" = CRAYON_COST_LARGE, + "paint" = CRAYON_COST_LARGE, + "secborg" = CRAYON_COST_LARGE, + "yiffhell" = CRAYON_COST_LARGE, ) /// Combined lists var/static/list/all_drawables = graffiti + symbols + drawings + oriented + runes + graffiti_large_h @@ -216,7 +223,7 @@ update_appearance() /obj/item/toy/crayon/proc/refill() - if(charges == -1) + if(charges == INFINITE_CHARGES) charges_left = 100 else charges_left = charges @@ -237,15 +244,13 @@ /obj/item/toy/crayon/proc/use_charges(mob/user, amount = 1, requires_full = TRUE) // Returns number of charges actually used - if(charges == -1) - . = amount + if(charges == INFINITE_CHARGES) refill() - else - if(check_empty(user, amount, requires_full)) - return 0 - else - . = min(charges_left, amount) - charges_left -= . + return TRUE + if(check_empty(user, amount, requires_full)) + return FALSE + charges_left -= min(charges_left, amount) + return TRUE /obj/item/toy/crayon/proc/check_empty(mob/user, amount = 1, requires_full = TRUE) // When eating a crayon, check_empty() can be called twice producing @@ -253,8 +258,8 @@ if(QDELETED(src)) return TRUE - // -1 is unlimited charges - if(charges == -1) + // INFINITE_CHARGES is unlimited charges + if(charges == INFINITE_CHARGES) return FALSE if(!charges_left) balloon_alert(user, "empty!") @@ -387,28 +392,16 @@ /// Attempts to color the target. Returns how many charges were used. /obj/item/toy/crayon/proc/use_on(atom/target, mob/user, params) var/static/list/punctuation = list("!","?",".",",","/","+","-","=","%","#","&") - var/istagger = HAS_TRAIT(user, TRAIT_TAGGER) - - var/cost = 1 - if(paint_mode == PAINT_LARGE_HORIZONTAL) - cost = 5 - if(istype(target, /obj/item/canvas)) - cost = 0 - if(ishuman(user)) - if (istagger) - cost *= 0.5 - if(check_empty(user, cost)) - return 0 if(istype(target, /obj/effect/decal/cleanable)) target = target.loc if(!isturf(target)) - return 0 + return if(!isValidSurface(target)) target.balloon_alert(user, "can't use there!") - return 0 + return var/drawing = drawtype switch(drawtype) @@ -431,6 +424,14 @@ if(RANDOM_ANY) drawing = pick(all_drawables) + var/istagger = HAS_TRAIT(user, TRAIT_TAGGER) + var/cost = all_drawables[drawing] || CRAYON_COST_DEFAULT + if(istype(target, /obj/item/canvas)) + cost = 0 + if (istagger) + cost *= 0.5 + if(check_empty(user, cost)) + return var/temp = "rune" var/ascii = (length(drawing) == 1) @@ -478,14 +479,11 @@ if(paint_mode == PAINT_LARGE_HORIZONTAL) wait_time *= 3 - if(!instant) - if(!do_after(user, 50, target = target)) - return 0 + if(!instant && !do_after(user, 50, target = target)) + return - var/charges_used = use_charges(user, cost) - if(!charges_used) - return 0 - . = charges_used + if(!use_charges(user, cost)) + return if(length(text_buffer)) drawing = text_buffer[1] @@ -536,22 +534,20 @@ for(var/turf/draw_turf as anything in affected_turfs) reagents.expose(draw_turf, methods = TOUCH, volume_modifier = volume_multiplier) check_empty(user) - return . /obj/item/toy/crayon/afterattack(atom/target, mob/user, proximity, params) . = ..() if(!proximity) - return . + return if (isitem(target)) . |= AFTERATTACK_PROCESSED_ITEM if (!check_allowed_items(target)) - return . + return use_on(target, user, params) - return . /obj/item/toy/crayon/attack(mob/target, mob/user) if(!edible || (target != user)) @@ -651,7 +647,7 @@ paint_color = "#FFFFFF" crayon_color = "mime" reagent_contents = list(/datum/reagent/consumable/nutriment = 0.5, /datum/reagent/colorful_reagent/powder/invisible = 1.5) - charges = -1 + charges = INFINITE_CHARGES dye_color = DYE_MIME /obj/item/toy/crayon/rainbow @@ -661,7 +657,7 @@ crayon_color = "rainbow" reagent_contents = list(/datum/reagent/consumable/nutriment = 0.5, /datum/reagent/colorful_reagent = 1.5) drawtype = RANDOM_ANY // just the default starter. - charges = -1 + charges = INFINITE_CHARGES dye_color = DYE_RAINBOW /obj/item/toy/crayon/rainbow/afterattack(atom/target, mob/user, proximity, params) @@ -765,7 +761,8 @@ /obj/item/toy/crayon/spraycan/suicide_act(mob/living/user) var/mob/living/carbon/human/H = user - if(is_capped || !actually_paints) + var/used = min(charges_left, 10) + if(is_capped || !actually_paints || !use_charges(user, 10, FALSE)) 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 @@ -779,7 +776,6 @@ update_appearance() if(actually_paints) H.update_lips("spray_face", paint_color) - var/used = use_charges(user, 10, FALSE) reagents.trans_to(user, used, volume_multiplier, transfered_by = user, methods = VAPOR) return OXYLOSS @@ -792,19 +788,20 @@ /obj/item/toy/crayon/spraycan/examine(mob/user) . = ..() - if(charges_left) - . += "It has [charges_left] use\s left." - else - . += "It is empty." + if(charges != INFINITE_CHARGES) + if(charges_left) + . += "It's roughly [PERCENT(charges_left/charges)]% full." + else + . += "It is empty." . += span_notice("Alt-click [src] to [ is_capped ? "take the cap off" : "put the cap on"]. Right-click a colored object to match its existing color.") /obj/item/toy/crayon/spraycan/use_on(atom/target, mob/user, params) if(is_capped) balloon_alert(user, "take the cap off first!") - return 0 + return if(check_empty(user)) - return 0 + return if(iscarbon(target)) if(pre_noise || post_noise) @@ -823,28 +820,31 @@ if(ishuman(carbon_target) && actually_paints) var/mob/living/carbon/human/human_target = carbon_target human_target.update_lips("spray_face", paint_color) - . = use_charges(user, 10, FALSE) + use_charges(user, 10, FALSE) var/fraction = min(1, . / reagents.maximum_volume) reagents.expose(carbon_target, VAPOR, fraction * volume_multiplier) - return . + else if(actually_paints && target.atom_colours[atom_colours] == paint_color) + balloon_alert(user, "[target.p_theyre()] already of that color.") + return FALSE if(ismob(target) && (HAS_TRAIT(target, TRAIT_SPRAY_PAINTABLE))) if(actually_paints) target.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) SEND_SIGNAL(target, COMSIG_LIVING_MOB_PAINTED) - . = use_charges(user, 2, requires_full = FALSE) + use_charges(user, 2, requires_full = FALSE) reagents.trans_to(target, ., volume_multiplier, transfered_by = user, methods = VAPOR) if(pre_noise || post_noise) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) - return . + return if(isobj(target) && !(target.flags_1 & UNPAINTABLE_1)) + var/color_is_dark = FALSE if(actually_paints) - var/color_is_dark = is_color_dark(paint_color) + color_is_dark = is_color_dark(paint_color) if (color_is_dark && !(target.flags_1 & ALLOW_DARK_PAINTS_1)) to_chat(user, span_warning("A color that dark on an object like this? Surely not...")) @@ -858,14 +858,14 @@ holder.update_held_items() else holder.update_clothing(target_item.slot_flags) - SEND_SIGNAL(target, COMSIG_OBJ_PAINTED, color_is_dark) - . = use_charges(user, 2, requires_full = FALSE) + if(!(SEND_SIGNAL(target, COMSIG_OBJ_PAINTED, user, src, color_is_dark) & DONT_USE_SPRAYCAN_CHARGES)) + use_charges(user, 2, requires_full = FALSE) reagents.trans_to(target, ., volume_multiplier, transfered_by = user, methods = VAPOR) if(pre_noise || post_noise) playsound(user.loc, 'sound/effects/spray.ogg', 5, TRUE, 5) user.visible_message(span_notice("[user] coats [target] with spray paint!"), span_notice("You coat [target] with spray paint.")) - return 0 + return return ..() @@ -920,37 +920,21 @@ /obj/item/toy/crayon/spraycan/borg name = "cyborg spraycan" desc = "A metallic container containing shiny synthesised paint." - charges = -1 + charges = INFINITE_CHARGES -/obj/item/toy/crayon/spraycan/borg/afterattack(atom/target,mob/user,proximity, params) - if (!proximity) - return - - if (isitem(target)) - . = AFTERATTACK_PROCESSED_ITEM - - if (!check_allowed_items(target)) - return . - - var/diff = use_on(target, user, params) +/obj/item/toy/crayon/spraycan/borg/use_charges(mob/user, amount = 1, requires_full = TRUE) if(!iscyborg(user)) to_chat(user, span_notice("How did you get this?")) qdel(src) - return . + return FALSE var/mob/living/silicon/robot/borgy = user - - if(!diff) - return . // 25 is our cost per unit of paint, making it cost 25 energy per // normal tag, 50 per window, and 250 per attack - var/cost = diff * 25 - // Cyborgs shouldn't be able to use modules without a cell. But if they do - // it's free. - if(borgy.cell) - borgy.cell.use(cost) + if(!borgy.cell?.use(amount * 25)) + return FALSE + return ..() - return . /obj/item/toy/crayon/spraycan/hellcan name = "hellcan" @@ -1002,7 +986,7 @@ /obj/item/toy/crayon/spraycan/infinite name = "infinite spraycan" - charges = -1 + charges = INFINITE_CHARGES desc = "Now with 30% more bluespace technology." #undef RANDOM_GRAFFITI @@ -1020,3 +1004,5 @@ #undef PAINT_NORMAL #undef PAINT_LARGE_HORIZONTAL #undef PAINT_LARGE_HORIZONTAL_ICON + +#undef INFINITE_CHARGES diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index ca68a00adfb..ea332049e3c 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -361,8 +361,10 @@ /obj/structure/window/proc/AfterRotation(mob/user, degrees) air_update_turf(TRUE, FALSE) -/obj/structure/window/proc/on_painted(obj/structure/window/source, is_dark_color) +/obj/structure/window/proc/on_painted(obj/structure/window/source, mob/user, obj/item/toy/crayon/spraycan/spraycan, is_dark_color) SIGNAL_HANDLER + if(!spraycan.actually_paints) + return if (is_dark_color && fulltile) //Opaque directional windows restrict vision even in directions they are not placed in, please don't do this set_opacity(255) else diff --git a/code/modules/power/floodlight.dm b/code/modules/power/floodlight.dm index ed35495106e..bb2e07fb45b 100644 --- a/code/modules/power/floodlight.dm +++ b/code/modules/power/floodlight.dm @@ -150,8 +150,10 @@ RegisterSignal(src, COMSIG_OBJ_PAINTED, TYPE_PROC_REF(/obj/machinery/power/floodlight, on_color_change)) //update light color when color changes register_context() -/obj/machinery/power/floodlight/proc/on_color_change(obj/machinery/power/flood_light, is_dark_color) +/obj/machinery/power/floodlight/proc/on_color_change(obj/machinery/power/flood_light, mob/user, obj/item/toy/crayon/spraycan/spraycan, is_dark_color) SIGNAL_HANDLER + if(!spraycan.actually_paints) + return if(setting > FLOODLIGHT_OFF) update_light_state()