From 57862898213e0b2a77593ccdde796a526980ed7d Mon Sep 17 00:00:00 2001 From: nikothedude <59709059+nikothedude@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:51:17 -0500 Subject: [PATCH] [BOUNTY] Dirty quirk (#5186) ## About The Pull Request Adds a dirty quirk, as requested in a bounty in https://discord.com/channels/1059199070016655462/1415440129362886727/1415440129362886727. This quirk does a few things. 1. Walking around, you have a small chance to spread dirt to the floor. 2. If you are barefoot, you track colored footprints that fade out over 7 seconds. 3. If you are attacked, you have a puff of colored, opaque smoke fly off you. **This cannot be spammed and must be refreshed by digging in trash.** 4. People around you get a small mood debuff. 5. You get a small examine blurb telling people that you're dirty. Both the dirt color and flavor text can be customized in the quirk. If you are cleaned with soap... 1. You scream, flail, and get a -6 mood debuff. 2. **All above effects are disabled.** 3. You can refresh your filth by rummaging in trash Hygeinebots also always try to clean dirty people. ## Why It's Good For The Game We have filth goblins on station, and this lets them reflect who they are mechanically. Its also a small, but minor, way to give janitors just a bit more things to do - and a small source of conflict where you may have people running around trying to clean you because god you just smell so bad. ## Proof Of Testing
Screenshots/Videos https://github.com/user-attachments/assets/6c627e3f-0874-4609-84e8-d7137628ecf1
## Changelog :cl: add: Dirty quirk - spreads dirt, makes everyone annoyed at you, defeated by soap. /:cl: --------- Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com> --- code/__DEFINES/~~bubber_defines/signals.dm | 5 + .../~~bubber_defines/traits/declarations.dm | 1 + code/datums/components/cleaner.dm | 5 + .../basic/bots/hygienebot/hygienebot_ai.dm | 7 + .../datums/quirks/negative_quirks/dirty.dm | 230 ++++++++++++++++++ .../game/objects/structures/trash_pile.dm | 2 + tgstation.dme | 1 + .../character_preferences/bubbers/dirty.tsx | 13 + 8 files changed, 264 insertions(+) create mode 100644 modular_zubbers/code/datums/quirks/negative_quirks/dirty.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/bubbers/dirty.tsx diff --git a/code/__DEFINES/~~bubber_defines/signals.dm b/code/__DEFINES/~~bubber_defines/signals.dm index b231b2e46e5..b316f85569e 100644 --- a/code/__DEFINES/~~bubber_defines/signals.dm +++ b/code/__DEFINES/~~bubber_defines/signals.dm @@ -49,3 +49,8 @@ /// returns the bitflag if it indeed reached max blood, otherwise NONE #define COMSIG_MOB_REACHED_MAX_BLOOD "mob_reached_max_blood" #define REACHED_MAX_BLOOD (1 << 0) + +/// From /obj/structure/trash_pile/attack_hand - trigged on successful search. (atom/source, obj/structure/trash_pile) +#define COMSIG_LIVING_SEARCHED_TRASH_PILE "living_searched_trash_pile" +/// From /datum/component/cleaner/proc/clean - triggered on successful clean. (/datum/component/cleaner/source, mob/living/user) +#define COMSIG_ATOM_POST_CLEAN "atom_post_clean" diff --git a/code/__DEFINES/~~bubber_defines/traits/declarations.dm b/code/__DEFINES/~~bubber_defines/traits/declarations.dm index 091cb52f9c8..594783f8609 100644 --- a/code/__DEFINES/~~bubber_defines/traits/declarations.dm +++ b/code/__DEFINES/~~bubber_defines/traits/declarations.dm @@ -5,6 +5,7 @@ #define TRAIT_CHANGELING_ZOMBIE "changelingzombie" #define TRAIT_RESEARCH_CYBORG "research_cyborg" #define TRAIT_BILINGUAL "Bilingual" +#define TRAIT_DIRTY "dirty" /// Cyborgs with unique sprites /// 32x32 quadruped skins with resting, sitting, and belly up sprites diff --git a/code/datums/components/cleaner.dm b/code/datums/components/cleaner.dm index e7efd2fc52c..014006842a2 100644 --- a/code/datums/components/cleaner.dm +++ b/code/datums/components/cleaner.dm @@ -127,6 +127,11 @@ //do the cleaning var/clean_succeeded = FALSE if(do_after(user, cleaning_duration, target = target)) + + // BUBBER EDIT ADDITION BEGIN - Dirty quirk + SEND_SIGNAL(target, COMSIG_ATOM_POST_CLEAN, user) + // BUBBER EDIT ADDITION END + clean_succeeded = TRUE for(var/obj/effect/decal/cleanable/cleanable_decal in target) //it's important to do this before you wash all of the cleanables off if(call_wash && grant_xp) diff --git a/code/modules/mob/living/basic/bots/hygienebot/hygienebot_ai.dm b/code/modules/mob/living/basic/bots/hygienebot/hygienebot_ai.dm index 1d0c689bff4..f8e8e1edcdf 100644 --- a/code/modules/mob/living/basic/bots/hygienebot/hygienebot_ai.dm +++ b/code/modules/mob/living/basic/bots/hygienebot/hygienebot_ai.dm @@ -81,6 +81,13 @@ if(LAZYACCESS(ignore_list, wash_potential)) continue + + // BUBBER EDIT ADDITION BEGIN - Dirty quirk + if (HAS_TRAIT(wash_potential, TRAIT_DIRTY)) + found_target = wash_potential + break + // BUGGER EDIT ADDITION END + if(our_access_flags & BOT_COVER_EMAGGED) controller.add_to_blacklist(wash_potential) found_target = wash_potential diff --git a/modular_zubbers/code/datums/quirks/negative_quirks/dirty.dm b/modular_zubbers/code/datums/quirks/negative_quirks/dirty.dm new file mode 100644 index 00000000000..cf27ed3813b --- /dev/null +++ b/modular_zubbers/code/datums/quirks/negative_quirks/dirty.dm @@ -0,0 +1,230 @@ +#define DIRTY_TILE_CHANCE 0.25 +#define REDIRTY_CHANCE 30 + +/datum/quirk/dirty + name = "Dirty" + desc = "You haven't taken a shower in a loooong time, and as a result, you are caked in an everpresent layer of grime and track it everywhere." + icon = FA_ICON_TRASH_ALT + value = -2 + mob_trait = TRAIT_DIRTY + quirk_flags = QUIRK_PROCESSES + gain_text = span_danger("You feel dirty!") + lose_text = span_notice("You're finally clean...") + medical_record_text = "Patient has atrocious hygiene standards." + hardcore_value = 2 + mail_goodies = list( + /obj/item/soap + ) + /// The text displayed on examine, set in preferences. + var/dirty_text + /// The hexcolor that all dirt graphics will use. + var/dirt_color + /// Next time we're hit, will a bunch of dust fly off us? + var/cloud_charged = TRUE + + /// Have we been cleaned, thereby disabling our dirt mechanics? + var/cleaned = FALSE + +/datum/quirk_constant_data/dirty + associated_typepath = /datum/quirk/dirty + customization_options = list( + /datum/preference/text/dirty_text, + /datum/preference/color/dirty_color, + ) + +/datum/preference/text/dirty_text + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "dirty_quirk_text" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 100 + +/datum/preference/text/dirty_text/serialize(input) + return htmlrendertext(input) + +/datum/preference/text/dirty_text/deserialize(input, datum/preferences/preferences) + return htmlrendertext(input) + +/datum/preference/text/dirty_text/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return + +/datum/preference/text/dirty_text/create_default_value() + return "They are positively covered in filth, and are tracking it everywhere!" + +/datum/preference/color/dirty_color + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "dirty_quirk_color" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/color/dirty_color/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences) + return + +/datum/preference/color/dirty_color/create_default_value() + return "#000000" + +/datum/quirk/dirty/process(seconds_per_tick) + if (cleaned) + return + for (var/mob/living/iter_mob in get_hearers_in_view(4, quirk_holder)) + if (HAS_TRAIT(iter_mob, TRAIT_DIRTY) || HAS_TRAIT(iter_mob, TRAIT_ANOSMIA)) + continue + iter_mob.add_mood_event("dirty_smell", /datum/mood_event/dirty_smell) + +/datum/quirk/dirty/add(client/client_source) + RegisterSignal(quirk_holder, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignals(quirk_holder, list(COMSIG_ATOM_POST_CLEAN, COMSIG_COMPONENT_CLEAN_ACT), PROC_REF(on_cleaned)) + RegisterSignal(quirk_holder, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + RegisterSignal(quirk_holder, COMSIG_LIVING_SEARCHED_TRASH_PILE, PROC_REF(searched_trash_pile)) + RegisterSignal(quirk_holder, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(on_check_block)) + +/datum/quirk/dirty/post_add() + dirty_text = quirk_holder.client.prefs.read_preference(/datum/preference/text/dirty_text) + dirt_color = quirk_holder.client.prefs.read_preference(/datum/preference/color/dirty_color) + + to_chat(quirk_holder, span_notice("You're dirty! You'll spread dirt when you walk, and can add a mood debuff to people around you. \ + Being cleaned will remove your filth - rummage through a trash pile to get it back.")) + + return ..() + +/datum/quirk/dirty/proc/searched_trash_pile(atom/signal_source, obj/structure/trash_pile/trash) + SIGNAL_HANDLER + + if (!cloud_charged) + to_chat(quirk_holder, span_notice("You find yourself covered in a thick layer of grime - a solid impact might knock it off in a cloud.")) + cloud_charged = TRUE + + if (!cleaned) + return + + if (!prob(REDIRTY_CHANCE)) + to_chat(quirk_holder, span_warning("You didn't manage to scrounge enough filth to right this wrong. Try again.")) + return + + set_cleaned(FALSE) + +/obj/effect/decal/cleanable/blood/footprints/dirty + name = "dirty footprints" + desc = "Footprints caked in grime. They smell awful." + dried = TRUE + bloodiness = 0 + +/datum/quirk/dirty/proc/on_moved(atom/movable/mover, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + + if (cleaned) + return + if (quirk_holder.body_position == LYING_DOWN || (quirk_holder.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)) + return + + var/turf/new_turf = get_turf(quirk_holder) + + if (prob(DIRTY_TILE_CHANCE)) + quirk_holder.balloon_alert_to_viewers("spreads dirt...") + new_turf.spawn_unique_cleanable(/obj/effect/decal/cleanable/dirt) + + var/mob/living/carbon/human/human_holder = quirk_holder + if (!istype(human_holder)) + return + + if (!isnull(human_holder.shoes)) + return + + var/obj/effect/decal/cleanable/blood/footprints/dirty/old_loc_prints = locate() in new_turf + + if (old_loc_prints) + return + + var/obj/effect/decal/cleanable/blood/footprints/dirty/new_loc_prints = new /obj/effect/decal/cleanable/blood/footprints/dirty(new_turf, null, null) + + // Find any leg of our human and add that to the footprint, instead of the default which is to just add the human type + for(var/obj/item/bodypart/leg/affecting in human_holder.bodyparts) + if(!affecting.bodypart_disabled) + LAZYSET(new_loc_prints.species_types, affecting.limb_id, TRUE) + + new_loc_prints.color = dirt_color + new_loc_prints.entered_dirs |= quirk_holder.dir + new_loc_prints.exited_dirs |= quirk_holder.dir + new_loc_prints.update_appearance() + + new_loc_prints.fade_into_nothing() + +/datum/effect_system/fluid_spread/smoke/dirty + var/color + +/datum/effect_system/fluid_spread/smoke/dirty/start(log) + var/start_loc = holder ? get_turf(holder) : src.location + var/obj/effect/particle_effect/fluid/smoke/smoke = new /obj/effect/particle_effect/fluid/smoke(start_loc, new /datum/fluid_group(amount)) + smoke.add_atom_colour(color, FIXED_COLOUR_PRIORITY) + smoke.spread() // Making the smoke spread immediately. + +/datum/quirk/dirty/proc/on_check_block(atom/hit_by, damage, attack_text, attack_type, armour_penetration, damage_type) + SIGNAL_HANDLER + + if (!cloud_charged || cleaned) + return + quirk_holder.visible_message( + span_warning("The impact knocks a cloud of grime and dust off [quirk_holder]!"), + span_notice("The impact knocks a cloud of grime and dust off you!") + ) + + var/datum/effect_system/fluid_spread/smoke/dirty/smoke = new /datum/effect_system/fluid_spread/smoke/dirty() + smoke.color = dirt_color + smoke.set_up(0, holder = quirk_holder, location = get_turf(quirk_holder)) + smoke.start() + + cloud_charged = FALSE + +/datum/quirk/dirty/proc/on_examine(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + if (cleaned) + return + if(!istype(user)) + return + + examine_list += span_warning(dirty_text) + +/// Setter proc for [cleaned]. Handles messages and other reactions. +/datum/quirk/dirty/proc/on_cleaned(datum/source) + SIGNAL_HANDLER + + INVOKE_ASYNC(quirk_holder, TYPE_PROC_REF(/mob, emote), "scream") + + set_cleaned(TRUE) + +/datum/quirk/dirty/proc/set_cleaned(new_cleaned) + + if (cleaned && !new_cleaned) + quirk_holder.visible_message( + span_warning("[quirk_holder] is once again covered in filth, [quirk_holder.p_their()] hygiene forever accursed."), + span_notice("Your find yourself lathered in filth once again. A sense of relief washes over you.") + ) + quirk_holder.clear_mood_event("dirty_quirk_washed") + ADD_TRAIT(quirk_holder, TRAIT_DIRTY, QUIRK_TRAIT) + else if (!cleaned && new_cleaned) + quirk_holder.visible_message( + span_warning("[quirk_holder] squirms and writhes, fighting against the cleansing!"), + span_userdanger("No! NO! Your filth! It's gone!") + ) + quirk_holder.add_mood_event("dirty_quirk_washed", /datum/mood_event/dirty_washed) + REMOVE_TRAIT(quirk_holder, TRAIT_DIRTY, QUIRK_TRAIT) + + cleaned = new_cleaned + +/datum/mood_event/dirty_smell + description = "Something... or someone... smells rotten around here..." + mood_change = -2 + timeout = 25 SECONDS + +/datum/mood_event/dirty_smell/add_effects(...) + . = ..() + + to_chat(owner, span_warning("You catch the distinct scent of someone who hasn't showered in years. Disgusting.")) + +/datum/mood_event/dirty_washed + description = "What is this smell...? Lavender soap? A hint of citrus?! ELDERBERRIES?! DISGUSTING! I need to wash myself in dirt!" + mood_change = -6 + +#undef DIRTY_TILE_CHANCE +#undef REDIRTY_CHANCE diff --git a/modular_zubbers/code/game/objects/structures/trash_pile.dm b/modular_zubbers/code/game/objects/structures/trash_pile.dm index 2d3b2ed760e..9f1200d7227 100644 --- a/modular_zubbers/code/game/objects/structures/trash_pile.dm +++ b/modular_zubbers/code/game/objects/structures/trash_pile.dm @@ -116,6 +116,8 @@ searched_by_ckeys[user.ckey] = 0 return + SEND_SIGNAL(user, COMSIG_LIVING_SEARCHED_TRASH_PILE, src) + if(searched_by_ckeys[user.ckey]) balloon_alert(user, "empty...") return TRUE diff --git a/tgstation.dme b/tgstation.dme index b8d18fcd22c..815dfc301ad 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -8949,6 +8949,7 @@ #include "modular_zubbers\code\datums\quirks\negative_quirks\blooddeficiency.dm" #include "modular_zubbers\code\datums\quirks\negative_quirks\bloodloss_dusting.dm" #include "modular_zubbers\code\datums\quirks\negative_quirks\brainproblems.dm" +#include "modular_zubbers\code\datums\quirks\negative_quirks\dirty.dm" #include "modular_zubbers\code\datums\quirks\negative_quirks\gifted.dm" #include "modular_zubbers\code\datums\quirks\negative_quirks\heavy_sleeper.dm" #include "modular_zubbers\code\datums\quirks\negative_quirks\loose_limbs.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/bubbers/dirty.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/bubbers/dirty.tsx new file mode 100644 index 00000000000..f33bf46ba70 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/bubbers/dirty.tsx @@ -0,0 +1,13 @@ +import { FeatureColorInput, FeatureTextInput, type Feature } from '../../base'; + +export const dirty_quirk_color: Feature = { + name: 'Dirt Color', + component: FeatureColorInput, +}; + +export const dirty_quirk_text: Feature = { + name: 'Flavor text', + description: + 'Displayed when you are dirty.', + component: FeatureTextInput, +};