Files
Bubberstation/code/game/objects/structures/maintenance.dm
SyncIt21 6dc40ca522 Standardizes object deconstruction throughout the codebase. (#82280)
## About The Pull Request
When it comes to deconstructing an object we have `proc/deconstruct()` &
`NO_DECONSTRUCT`

Lets talk about the flag first. 

**Problems with `NO_DECONSTRUCTION`**
I know what the comment says on what it should do

b5593bc693/code/__DEFINES/obj_flags.dm (L18)

But everywhere people have decided to give their own meaning/definition
to this flag. Here are some examples on how this flag is used

**1. Make the object just disappear(not drop anything) when
deconstructed**
This is by far the largest use case everywhere. If an object is
deconstructed(either via tools or smashed apart) then if it has this
flag it should not drop any of its contents but just disappear. You have
seen this code pattern used everywhere

b5593bc693/code/game/machinery/constructable_frame.dm (L26-L31)

This behaviour is then leveraged by 2 important components.

When an object is frozen, if it is deconstructed it should just
disappear without leaving any traces behind

b5593bc693/code/datums/elements/frozen.dm (L66-L67)

By hologram objects. Obviously if you destroy an hologram nothing real
should drop out

b5593bc693/code/modules/holodeck/computer.dm (L301-L304)

And there are other use cases as well but we won't go into them as they
aren't as significant as these.

**2. To stop an object from being wrenched ??**
Yeah this one is weird. Like why? I understand in some instances (chair,
table, rack etc) a wrench can be used to deconstruct a object so using
the flag there to stop it from happening makes sense but why can't we
even anchor an object just because of this flag?

b5593bc693/code/game/objects/objs.dm (L368-L369)
This is one of those instances where somebody just decided this
behaviour for their own convenience just like the above example with no
explanation as to why

**3. To stop using tools to deconstruct the object** 
This was the original intent of the flag but it is enforced in few
places far & between. One example is when deconstructing the a machine
via crowbar.

b5593bc693/code/game/machinery/_machinery.dm (L811)

But machines are a special dual use case for this flag. Because if you
look at its deconstruct proc the flag also prevents the machine from
spawning a frame.

b5593bc693/code/game/machinery/_machinery.dm (L820-L822)

How can 1 flag serve 2 purposes within the same type?

**4. Simply forget to check for this flag altogether**
Yup if you find this flag not doing its job for some objects don't be
surprised. People & sometimes even maintainers just forget that it even
exists

b5593bc693/code/game/objects/items/piggy_bank.dm (L66-L67)

**Solution**
These are the main examples i found. As you can see the same flag can
perform 2 different functions within the same type and do something else
in a different object & in some instances don't even work cause people
just forget, etc.

In order to bring consistency to this flag we need to move it to the
atom level where it means the same thing everywhere. Where in the atom
you may ask? .Well, I'll just post what MrMelbert said in
https://github.com/tgstation/tgstation/pull/81656#discussion_r1503086862

> ...Ideally the .deconstruct call would handle NO_DECONSTRUCTION
handling as it wants,

Yup that's the ideal case now. This flag is checked directly in
`deconstruct()`. Now like i said we want to give a universal definition
to this flag and as you have seen from my examples it is used in 3 cases
1) Make an object disappear(doesn't dropping anything) when
deconstructed
2) Stop it from being wrenched
3) Stop it from being deconstructed via tools

We can't enforce points 2 & 3 inside `deconstruct()` which leaves us
with only case 1) i.e. make the object disappear. And that's what i have
done. Therefore after more than a decade or since this flag got
introduced `NO_DECONSTRUCT` now has a new definition as of 2024

_"Make an object disappear(don't dropping anything) when deconstructed
either via tools or forcefully smashed apart"_

Now i very well understand this will open up bugs in places where cases
2 & 3 are required but its worth it. In fact they could even be qol
changes for all we know so who knows it might even benefit us but for
now we need to give a universal definition to this flag to bring some
consistency & that's what this PR does.

**Problem with deconstruct()**
This proc actually sends out a signal which is currently used by the
material container but could be used by other objects later on.

3e84c3e6da/code/game/objects/obj_defense.dm (L160)

So objects that override this proc should call its parent. Sadly that
isn't the case in many instances like such

3e84c3e6da/code/game/machinery/deployable.dm (L20-L23)

Instead of `return ..()` which would delete the object & send the signal
it deletes the object directly thus the signal never gets sent.

**Solution**
Make this proc non overridable. For objects to add their own custom
deconstruction behaviour a new proc has been introduced
`atom_deconstruct()` Subtypes should now override this proc to handle
object deconstruction.

If objects have certain important stuff inside them (like mobs in
machines for example) they want to drop by handling `NO_DECONSTRUCT`
flag in a more carefully customized way they can do this by overriding
`handle_deconstruct()` which by default delegates to
`atom_deconstruct()` if the `NO_DECONSTRUCT` flag is absent. This proc
will allow you to handle the flag in a more customized way if you ever
need to.

## Why It's Good For The Game
1) I'm goanna post the full comment from MrMelbert
https://github.com/tgstation/tgstation/pull/81656#discussion_r1503086862

> ...Ideally the .deconstruct call would handle NO_DECONSTRUCTION
handling as it wants, but there's a shocking lack of consistency around
NO_DECONSTRUCTION, where some objects treat it as "allow deconstruction,
but make it drop no parts" and others simply "disallow deconstruction at
all"

This PR now makes `NO_DECONSTRUCTION` handled by `deconstruct()` & gives
this flag the consistency it deserves. Not to mention as shown in case 4
there are objects that simply forgot to check for this flag. Now it
applies for those missing instances as well.

2) No more copying pasting the most overused code pattern in this code
base history `if(obj_flags & NO_DECONSTRUCTION)`. Just makes code
cleaner everywhere

3) All objects now send the `COMSIG_OBJ_DECONSTRUCT` signal on object
deconstruction which is now available for use should you need it

## Changelog
🆑
refactor: refactors how objects are deconstructed in relation to the
`NO_DECONSTRUCTION` flag. Certain objects & machinery may display
different tool interactions & behaviours when destroyed/deconstructed.
Report these changes if you feel like they are bugs
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>
2024-04-04 18:55:51 -06:00

334 lines
13 KiB
Plaintext

/** This structure acts as a source of moisture loving cell lines,
as well as a location where a hidden item can somtimes be retrieved
at the cost of risking a vicious bite.**/
/obj/structure/moisture_trap
name = "moisture trap"
desc = "A device installed in order to control moisture in poorly ventilated areas.\nThe stagnant water inside basin seems to produce serious biofouling issues when improperly maintained.\nThis unit in particular seems to be teeming with life!\nWho thought mother Gaia could assert herself so vigoriously in this sterile and desolate place?"
icon_state = "moisture_trap"
anchored = TRUE
density = FALSE
///This var stores the hidden item that might be able to be retrieved from the trap
var/obj/item/hidden_item
///This var determines if there is a chance to receive a bite when sticking your hand into the water.
var/critter_infested = TRUE
///weighted loot table for what loot you can find inside the moisture trap.
///the actual loot isn't that great and should probably be improved and expanded later.
var/static/list/loot_table = list(
/obj/item/food/meat/slab/human/mutant/skeleton = 35,
/obj/item/food/meat/slab/human/mutant/zombie = 15,
/obj/item/trash/can = 15,
/obj/item/clothing/head/helmet/skull = 10,
/obj/item/restraints/handcuffs = 4,
/obj/item/restraints/handcuffs/cable/red = 1,
/obj/item/restraints/handcuffs/cable/blue = 1,
/obj/item/restraints/handcuffs/cable/green = 1,
/obj/item/restraints/handcuffs/cable/pink = 1,
/obj/item/restraints/handcuffs/alien = 2,
/obj/item/coin/bananium = 10,
/obj/item/knife/butcher = 5,
)
/obj/structure/moisture_trap/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_FISH_SAFE_STORAGE, TRAIT_GENERIC)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_MOIST, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 20)
if(prob(40))
critter_infested = FALSE
if(prob(75))
var/picked_item = pick_weight(loot_table)
hidden_item = new picked_item(src)
var/datum/fish_source/moisture_trap/fish_source = new
if(prob(50)) // 50% chance there's another item to fish out of there
var/picked_item = pick_weight(loot_table)
fish_source.fish_table[picked_item] = 5
fish_source.fish_counts[picked_item] = 1;
AddComponent(/datum/component/fishing_spot, fish_source)
/obj/structure/moisture_trap/Destroy()
if(hidden_item)
QDEL_NULL(hidden_item)
return ..()
///This proc checks if we are able to reach inside the trap to interact with it.
/obj/structure/moisture_trap/proc/CanReachInside(mob/user)
if(!isliving(user))
return FALSE
var/mob/living/living_user = user
if(living_user.body_position == STANDING_UP && ishuman(living_user)) //I dont think monkeys can crawl on command.
return FALSE
return TRUE
/obj/structure/moisture_trap/attack_hand(mob/user, list/modifiers)
. = ..()
if(iscyborg(user) || isalien(user))
return
if(!CanReachInside(user))
to_chat(user, span_warning("You need to lie down to reach into [src]."))
return
to_chat(user, span_notice("You reach down into the cold water of the basin."))
if(!do_after(user, 2 SECONDS, target = src))
return
if(hidden_item)
user.put_in_hands(hidden_item)
to_chat(user, span_notice("As you poke around inside [src] you feel the contours of something hidden below the murky waters.</span>\n<span class='nicegreen'>You retrieve [hidden_item] from [src]."))
hidden_item = null
return
if(critter_infested && prob(50) && iscarbon(user))
var/mob/living/carbon/bite_victim = user
var/obj/item/bodypart/affecting = bite_victim.get_bodypart("[(user.active_hand_index % 2 == 0) ? "r" : "l" ]_arm")
to_chat(user, span_danger("You feel a sharp pain as an unseen creature sinks it's [pick("fangs", "beak", "proboscis")] into your arm!"))
if(affecting?.receive_damage(30))
bite_victim.update_damage_overlays()
playsound(src,'sound/weapons/bite.ogg', 70, TRUE)
return
to_chat(user, span_warning("You find nothing of value..."))
/obj/structure/moisture_trap/attackby(obj/item/I, mob/user, params)
if(iscyborg(user) || isalien(user) || !CanReachInside(user))
return ..()
add_fingerprint(user)
if(is_reagent_container(I))
if(istype(I, /obj/item/food/monkeycube))
var/obj/item/food/monkeycube/cube = I
cube.Expand()
return
var/obj/item/reagent_containers/reagent_container = I
if(reagent_container.is_open_container())
reagent_container.reagents.add_reagent(/datum/reagent/water, min(reagent_container.volume - reagent_container.reagents.total_volume, reagent_container.amount_per_transfer_from_this))
to_chat(user, span_notice("You fill [reagent_container] from [src]."))
return
if(hidden_item)
to_chat(user, span_warning("There is already something inside [src]."))
return
if(!user.transferItemToLoc(I, src))
to_chat(user, span_warning("\The [I] is stuck to your hand, you cannot put it in [src]!"))
return
hidden_item = I
to_chat(user, span_notice("You hide [I] inside the basin."))
#define ALTAR_INACTIVE 0
#define ALTAR_STAGEONE 1
#define ALTAR_STAGETWO 2
#define ALTAR_STAGETHREE 3
#define ALTAR_TIME (9.5 SECONDS)
/obj/structure/destructible/cult/pants_altar
name = "strange structure"
desc = "What is this? Who put it on this station? And why does it emanate <span class='hypnophrase'>strange energy?</span>"
icon_state = "altar"
cult_examine_tip = "Even you don't understand the eldritch magic behind this."
break_message = "<span class='warning'>The structure shatters, leaving only a demonic screech!</span>"
break_sound = 'sound/magic/demon_dies.ogg'
light_color = LIGHT_COLOR_BLOOD_MAGIC
light_range = 2
use_cooldown_duration = 1 MINUTES
/// Color of the pants that will come out
var/pants_color = COLOR_WHITE
/// Stage of the pants making process
var/status = ALTAR_INACTIVE
/obj/structure/destructible/cult/pants_altar/attackby(obj/attacking_item, mob/user, params)
if(istype(attacking_item, /obj/item/melee/cultblade/dagger) && IS_CULTIST(user) && status)
to_chat(user, span_notice("[src] is creating something, you can't move it!"))
return
return ..()
/obj/structure/destructible/cult/pants_altar/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
var/list/altar_options = list(
"Change Color" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_recolor"),
"Create Artefact" = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_create")
)
var/altar_result = show_radial_menu(user, src, altar_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE)
switch(altar_result)
if("Change Color")
var/chosen_color = input(user, "", "Choose Color", pants_color) as color|null
if(!isnull(chosen_color) && user.can_perform_action(src))
pants_color = chosen_color
if("Create Artefact")
if(!COOLDOWN_FINISHED(src, use_cooldown) || status != ALTAR_INACTIVE)
to_chat(user, span_warning("[src] is not ready to create something new yet..."))
return
pants_stageone()
return TRUE
/obj/structure/destructible/cult/pants_altar/update_icon_state()
. = ..()
if(!COOLDOWN_FINISHED(src, use_cooldown))
icon_state = "altar_off"
else
icon_state = "altar"
/obj/structure/destructible/cult/pants_altar/update_overlays()
. = ..()
var/overlayicon
switch(status)
if(ALTAR_INACTIVE)
return
if(ALTAR_STAGEONE)
overlayicon = "altar_pants1"
if(ALTAR_STAGETWO)
overlayicon = "altar_pants2"
if(ALTAR_STAGETHREE)
overlayicon = "altar_pants3"
var/mutable_appearance/pants_overlay = mutable_appearance(icon, overlayicon)
pants_overlay.appearance_flags = RESET_COLOR
pants_overlay.color = pants_color
. += pants_overlay
/// Starts creating the pants, plays the sound.
/obj/structure/destructible/cult/pants_altar/proc/pants_stageone()
status = ALTAR_STAGEONE
update_icon()
visible_message(span_warning("[src] starts creating something..."))
playsound(src, 'sound/magic/pantsaltar.ogg', 60)
addtimer(CALLBACK(src, PROC_REF(pants_stagetwo)), ALTAR_TIME)
/// Continues the creation, making every mob nearby nauseous.
/obj/structure/destructible/cult/pants_altar/proc/pants_stagetwo()
status = ALTAR_STAGETWO
update_icon()
visible_message(span_warning("You start feeling nauseous..."))
for(var/mob/living/viewing_mob in viewers(7, src))
viewing_mob.set_eye_blur_if_lower(20 SECONDS)
viewing_mob.adjust_confusion(10 SECONDS)
addtimer(CALLBACK(src, PROC_REF(pants_stagethree)), ALTAR_TIME)
/// Continues the creation, making every mob nearby dizzy
/obj/structure/destructible/cult/pants_altar/proc/pants_stagethree()
status = ALTAR_STAGETHREE
update_icon()
visible_message(span_warning("You start feeling horrible..."))
for(var/mob/living/viewing_mob in viewers(7, src))
viewing_mob.set_dizzy_if_lower(20 SECONDS)
addtimer(CALLBACK(src, PROC_REF(pants_create)), ALTAR_TIME)
/// Finishes the creation, creating the item itself, setting the cooldowns and flashing every mob nearby.
/obj/structure/destructible/cult/pants_altar/proc/pants_create()
status = ALTAR_INACTIVE
update_icon()
visible_message(span_danger("[src] emits a flash of light and creates... pants?"))
for(var/mob/living/viewing_mob in viewers(7, src))
viewing_mob.flash_act()
var/obj/item/clothing/under/pants/slacks/altar/pants = new(get_turf(src))
pants.add_atom_colour(pants_color, ADMIN_COLOUR_PRIORITY)
COOLDOWN_START(src, use_cooldown, use_cooldown_duration)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon)), 1 MINUTES + 0.1 SECONDS)
update_icon()
/obj/structure/destructible/cult/pants_altar/proc/check_menu(mob/user)
if(!istype(user))
return FALSE
if(user.incapacitated() || !user.Adjacent(src))
return FALSE
return TRUE
/obj/item/clothing/under/pants/slacks/altar
name = "strange pants"
desc = "A pair of pants. They do not look or feel natural, and smell like fresh blood."
greyscale_colors = "#ffffff#ffffff#ffffff"
flags_1 = NONE //If IS_PLAYER_COLORABLE gets added color-changing support (i.e. spraycans), these won't end up getting it too. Plus, it already has its own recolor.
#undef ALTAR_INACTIVE
#undef ALTAR_STAGEONE
#undef ALTAR_STAGETWO
#undef ALTAR_STAGETHREE
#undef ALTAR_TIME
/**
* Spawns in maint shafts, and blocks lines of sight perodically when active.
*/
/obj/structure/steam_vent
name = "steam vent"
desc = "A device periodically filtering out moisture particles from the nearby walls and windows. It's only possible due to the moisture traps nearby."
icon_state = "steam_vent"
anchored = TRUE
density = FALSE
/// How often does the vent reset the blow_steam cooldown.
var/steam_speed = 20 SECONDS
/// Is the steam vent active?
var/vent_active = TRUE
/// The cooldown for toggling the steam vent to prevent infinite steam vent looping.
COOLDOWN_DECLARE(steam_vent_interact)
/obj/structure/steam_vent/Initialize(mapload)
. = ..()
if(prob(75))
vent_active = FALSE
var/static/list/loc_connections = list(
COMSIG_ATOM_EXIT = PROC_REF(blow_steam),
)
AddElement(/datum/element/connect_loc, loc_connections)
register_context()
update_icon_state()
/obj/structure/steam_vent/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(!COOLDOWN_FINISHED(src, steam_vent_interact))
balloon_alert(user, "not ready to adjust!")
return
vent_active = !vent_active
update_icon_state()
if(vent_active)
balloon_alert(user, "vent on")
else
balloon_alert(user, "vent off")
return
blow_steam()
/obj/structure/steam_vent/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(isnull(held_item))
context[SCREENTIP_CONTEXT_LMB] = vent_active ? "Close valve" : "Open valve"
return CONTEXTUAL_SCREENTIP_SET
if(held_item.tool_behaviour == TOOL_WRENCH)
context[SCREENTIP_CONTEXT_RMB] = "Deconstruct"
return CONTEXTUAL_SCREENTIP_SET
return .
/obj/structure/steam_vent/wrench_act_secondary(mob/living/user, obj/item/tool)
. = ..()
if(vent_active)
balloon_alert(user, "must be off!")
return
if(tool.use_tool(src, user, 3 SECONDS))
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
deconstruct()
return TRUE
/obj/structure/steam_vent/atom_deconstruct(disassembled = TRUE)
new /obj/item/stack/sheet/iron(loc, 1)
new /obj/item/stock_parts/water_recycler(loc, 1)
/**
* Creates "steam" smoke, and determines when the vent needs to block line of sight via reset_opacity.
*/
/obj/structure/steam_vent/proc/blow_steam(datum/source, atom/movable/leaving, direction)
SIGNAL_HANDLER
if(!vent_active)
return
if(!COOLDOWN_FINISHED(src, steam_vent_interact))
return
if(!ismob(leaving))
return
var/datum/effect_system/fluid_spread/smoke/smoke = new
smoke.set_up(range = 1, amount = 1, location = src)
smoke.start()
playsound(src, 'sound/machines/steam_hiss.ogg', 75, TRUE, -2)
COOLDOWN_START(src, steam_vent_interact, steam_speed)
/obj/structure/steam_vent/update_icon_state()
. = ..()
icon_state = "steam_vent[vent_active ? "": "_off"]"
/obj/structure/steam_vent/fast
desc = "A device periodically filtering out moisture particles from the nearby walls and windows. It's only possible due to the moisture traps nearby. It's faster than most."
steam_speed = 10 SECONDS