Files
5786289821 [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
<details>
<summary>Screenshots/Videos</summary>


https://github.com/user-attachments/assets/6c627e3f-0874-4609-84e8-d7137628ecf1

</details>

## Changelog
🆑
add: Dirty quirk - spreads dirt, makes everyone annoyed at you, defeated
by soap.
/🆑

---------

Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
2026-02-02 00:51:17 -05:00

229 lines
6.9 KiB
Plaintext

/obj/structure/trash_pile
name = "trash pile"
desc = "A heap of dense garbage. Perhaps there is something interesting inside?"
icon = 'modular_skyrat/master_files/icons/obj/trash_piles.dmi'
icon_state = "randompile"
density = TRUE
anchored = TRUE
layer = TABLE_LAYER
obj_flags = CAN_BE_HIT
pass_flags = LETPASSTHROW
max_integrity = 50
var/hide_person_time = 3 SECONDS
var/hide_item_time = 1 SECONDS
var/list/searched_by_ckeys = list() //Assoc list of people who searched this trash pile (value is 1), or started searching (value is 0)
var/trash_delay = 0.5 SECONDS
var/funny_sound_delay = 0.2 SECONDS
COOLDOWN_DECLARE(trash_cooldown)
COOLDOWN_DECLARE(funny_sound_cooldown)
var/static/list/funny_sounds = list( //Assoc list of funny sounds (funny sound to weight)
'sound/effects/adminhelp.ogg' = 1,
'sound/effects/footstep/clownstep1.ogg' = 5,
'sound/effects/footstep/clownstep2.ogg' = 5,
'sound/effects/footstep/meowstep1.ogg' = 1,
'sound/effects/blob/attackblob.ogg' = 10,
'sound/effects/bang.ogg' = 10,
'sound/effects/bin/bin_close.ogg' = 20,
'sound/effects/bin/bin_open.ogg' = 20,
'sound/effects/boing.ogg' = 5,
'sound/effects/cartoon_sfx/cartoon_splat.ogg' = 1,
'sound/effects/cashregister.ogg' = 1,
'sound/effects/glass/glassbash.ogg' = 50,
'sound/effects/glass/glassbr1.ogg' = 20,
'sound/effects/glass/glassbr2.ogg' = 20,
'sound/effects/glass/glassbr3.ogg' = 20,
'sound/effects/grillehit.ogg' = 20,
'sound/effects/hit_on_shattered_glass.ogg' = 20,
'sound/effects/jingle.ogg' = 50,
'sound/effects/meatslap.ogg' = 50,
'sound/effects/quack.ogg' = 20,
'sound/effects/rustle/rustle1.ogg' = 100,
'sound/effects/rustle/rustle2.ogg' = 100,
'sound/effects/rustle/rustle3.ogg' = 100,
'sound/effects/rustle/rustle4.ogg' = 100,
'sound/effects/rustle/rustle5.ogg' = 100,
)
/obj/structure/trash_pile/Destroy()
var/turf/T = get_turf(src)
if(T)
for(var/atom/movable/M in contents)
M.forceMove(T)
. = ..()
/obj/structure/trash_pile/Initialize(mapload)
. = ..()
AddElement(/datum/element/climbable)
AddElement(/datum/element/elevation, pixel_shift = 12)
icon_state = pick(
"pile1",
"pile2",
"pilechair",
"piletable",
"pilevending",
"brtrashpile",
"microwavepile",
"rackpile",
"boxfort",
"trashbag",
"brokecomp",
)
/obj/structure/trash_pile/attack_hand(mob/living/user)
if(user in contents)
eject_mob(user)
return
if(!ishuman(user) || user.combat_mode || !user.ckey)
return ..()
user.visible_message("[user] searches through \the [src]...", span_notice("You search through \the [src]..."))
playsound(get_turf(src), pick('sound/effects/rustle/rustle1.ogg','sound/effects/rustle/rustle2.ogg','sound/effects/rustle/rustle3.ogg','sound/effects/rustle/rustle4.ogg','sound/effects/rustle/rustle5.ogg'), 50)
var/content_length = length(contents)
if(content_length) //Something hidden inside (mob/item)
var/atom/hidden_atom = contents[content_length] // Get the most recent hidden thing
if(!do_after(user,3 SECONDS, target = src, extra_checks = CALLBACK(src,PROC_REF(throw_trash),user)))
if(!isnum(searched_by_ckeys[user.ckey]))
searched_by_ckeys[user.ckey] = 0
return
else
if(!isnum(searched_by_ckeys[user.ckey]))
searched_by_ckeys[user.ckey] = 0
if(istype(hidden_atom, /mob/living))
var/mob/living/hidden_mob = hidden_atom
balloon_alert(user, "someone is inside!")
eject_mob(hidden_mob)
else if (istype(hidden_atom, /obj/item))
var/obj/item/hidden_item = hidden_atom
balloon_alert(user, "found something!")
hidden_item.forceMove(get_turf(src))
return
if(!do_after(user,3 SECONDS, target = src, extra_checks = CALLBACK(src,PROC_REF(throw_trash),user)))
if(!isnum(searched_by_ckeys[user.ckey]))
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
var/obj/item/spawned_item = prob(25) ? pick_weight_recursive(GLOB.common_loot) : pick_weight_recursive(GLOB.trash_pile_loot)
spawned_item = new spawned_item(get_turf(src))
if(spawned_item)
balloon_alert(user, "found [spawned_item.name]!")
else
balloon_alert(user, "found nothing...")
searched_by_ckeys[user.ckey] = 1
/obj/structure/trash_pile/attackby(obj/item/attacking_item, mob/living/user, params)
if(user in contents)
eject_mob(user)
return
if(user.combat_mode)
return ..()
if(length(contents) >= 10)
balloon_alert(user, "it's full!")
return
balloon_alert(user, "hiding item...")
if(!do_after(user, hide_item_time, user))
return
if(QDELETED(src))
return
if(user.transferItemToLoc(attacking_item, src))
balloon_alert(user, "item hidden!")
/obj/structure/trash_pile/mouse_drop_receive(atom/movable/dropped_atom, mob/user, params)
if(user != dropped_atom || !iscarbon(dropped_atom))
return ..()
var/mob/living/carbon/dropped_mob = dropped_atom
if(!(dropped_mob.mobility_flags & MOBILITY_MOVE))
return
user.visible_message(
span_warning("[user] starts diving into [src]."),
span_notice("You start diving into [src]...")
)
var/adjusted_dive_time = hide_person_time
if(HAS_TRAIT(user, TRAIT_RESTRAINED)) // hiding takes twice as long when restrained.
adjusted_dive_time *= 2
if(!do_after(user, adjusted_dive_time, user))
return
if(QDELETED(src))
return
for(var/mob/hidden_mob in contents)
balloon_alert(user, "someone is inside!")
eject_mob(hidden_mob)
return
user.forceMove(src)
/obj/structure/trash_pile/container_resist_act(mob/user)
user.forceMove(src.loc)
/obj/structure/trash_pile/relaymove(mob/user)
container_resist_act(user)
/obj/structure/trash_pile/proc/throw_trash(mob/user)
if(QDELETED(src) || QDELETED(user)) //Check if valid.
return FALSE
if(isnum(searched_by_ckeys[user.ckey])) //Don't spawn trash!
return TRUE
var/turf/T = get_turf(user)
if(get_dist(T,src) > 1) //Distance check for TK fuckery
return TRUE
if(COOLDOWN_FINISHED(src, trash_cooldown))
COOLDOWN_START(src, trash_cooldown, trash_delay*0.5 + rand()*trash_delay) // x0.5 to x1.5
var/obj/item/spawned_item
if(length(GLOB.one_of_a_kind_loot) && prob(0.1)) //1 in 1000
spawned_item = pick_n_take(GLOB.one_of_a_kind_loot)
else
spawned_item = pick_weight_recursive(GLOB.trash_loot)
spawned_item = new spawned_item(T)
var/turf/throw_at = get_ranged_target_turf_direct(src, user, 7, rand(-60,60))
if(spawned_item.safe_throw_at(throw_at, rand(2,4), rand(1,3), user, spin = TRUE))
playsound(T, 'sound/items/weapons/punchmiss.ogg', 10)
if(COOLDOWN_FINISHED(src,funny_sound_cooldown))
COOLDOWN_START(src, funny_sound_cooldown, funny_sound_delay*0.5 + rand()*funny_sound_delay) // x0.5 to x1.5
playsound(T,pick_weight(funny_sounds), 25)
return TRUE
/obj/structure/trash_pile/proc/eject_mob(mob/living/hidden_mob)
var/turf/T = get_turf(src)
hidden_mob.forceMove(T)
playsound(T, 'sound/machines/chime.ogg', 50, FALSE, -5)
hidden_mob.do_alert_animation(hidden_mob)
return TRUE