Files
san7890andGitHub e0827de4b5 [MDB Ignore] Unrestricted Airlocks - Door Delays (#94040)
## About The Pull Request

This PR adds a change to a pretty prevalent feature that's been around
since #66588 (April 28th, 2022). Although it has been controversial to
some extent, I believe that it was a good change in principle for
reasons I will later get into. For now, this is what the PR does:

* All _station_ unrestricted door helpers (e.g. those in maintenance)
have been replaced with a subtype that adds a random 2-3 second delay to
opening the door, using a new "unrestricted latch" feature. **This does
not occur if the user has regular access to that door.**
* The unrestricted side will initiate a do_after and plays a distinct
small hissing sound (because I presume you're disengaging some hydraulic
lever on that side), after which the door will open as players have come
to expect.
* While you're engaging the lever, you will be resistant to pressure
movements as you are presumably holding onto the lever or something. All
other movements (e.g. voluntary, being shoved, etc.) will continue to
function as expected.
* We will statistically tabulate each time someone attempts to use the
unrestricted "lever", and every time one successfully occurs. This is
just for analysis to see player trends as we have lacked this over the
last 3.5 years.

Here's a link to the sound I added:
https://freesound.org/people/Usernameis1337/sounds/632755, the version
in this PR was converted using OGG Vorbis.


https://github.com/user-attachments/assets/4167bda4-63b5-4dc0-a7b2-694a9e3d3201

## Why It's Good For The Game

Back in 2022, I was one of the major proponents for adding such a change
to the game. This is for a few reasons:

* It doesn't really make human sense for a station to have
hallways/spaces that you can enter and then just get completely trapped
in. The station is a deathtrap because of _who_ is on it, not because it
_is_ a death trap. I have always maintained that we should assume that
the station is competently made to give the hijinx a much more jarring
background. To me, maintenance isn't old sections of a station (although
it can have those elements), but vital side pathways in the event of
emergencies, like having extra fire stairs in a building. The doors to
those fire stairs are always unlocked because they need to ensure that
people are able to efflux to save themselves from danger rather than
die.
* It was easily subvertable anyways. You can easily ask the HoP for
maintenance access or AI to let you out of maintenance (although the
latter was a bit harder to articulate). I don't find myself agreeing
with this as much nowadays but I remember the frustration I felt waiting
for minutes for someone to open the door had something occurred for me
to have found myself in maintenance. It's simply not fun and doesn't
make sense for a station to have this.
* I thought that it added a unique element to antagonism. You can't drag
someone in maintenance and then have the whole entire subsection of the
station be your den. It's important for you to secure your base should
you be dragging people in there instead of just always being able to win
via dragging.
* People tend to forget this point, but it actually did cause a lot
_more_ deaths in situations where threats originate outside the
station/in maintenance. It was much easier for the killy-stabby mass
murderers to ingress into main hallways and get to killing faster. This
is why the change felt "equal" in a way.

### Why not remove it outright?

I believe that a lot of balance changes over the last 3.5 years (30000
issues ago!) have been weighting for these interactions, and I think
removing it all cold-turkey like will cause a huge upturn in deadliness
and just not be what players are looking for in the new game loop. To
me, this was a vital step in getting more towards 90-minute rounds, and
I think we would see a drop in that. I also have those above three
reasons that I am still a firm believer in, but I am still interested in
seeing what a different system might entail and how that could play out.
I added stat gathering so after we get 4-6 weeks of data, we can
determine to keep or shelve this feature. I will probably always be on
the "blocking" side of removing it outright unless there's some severe
data that can convince me (which will probably need it's own test-merged
PR, while this is meant to be added into the game right now).

### Why are we changing it?

I think not enough people are dying. I played a few rounds yesterday and
while a decent chunk of people were dying, I saw a few follies in my
original line of thought. Maintenance should be a bit creepier and not
auxiliary hallways (in normal circumstances), I already thought we did a
really good job of that but I noticed that it wasn't as good as memory
served me. I think maintenance should have a little bit more difficulty
to navigate (i.e. if you're being chased down by someone and it's taking
2.5 seconds to open, that would be an excellent heart-pounding tension
moment) and would overall be more interesting than the status quo. It is
a bit sillier to me but some IRL buildings do have to have you hold down
the push bar for a few seconds until it disengages, so this is
agreeable.

I also don't think the unidirectional exit design flow standards are
where they were due to drift in application, so this is also a better
way to uniformly apply it.
## Changelog
🆑
balance: After noticing it was too easy for animals to crawl through
maintenance by abusing the Easy-Exit sensors, Nanotrasen has replaced
all unrestricted door sensors (like in maintenance) with a pull-down
lever that should take around 2-3 seconds to engage. It has been
designed to allow the employee to actuate it despite any pressure
differentials within an area.
balance: The above change does not apply to select doors in the medical
section of the station, which will continue to use the "older"
Ready-Exit system.
sound: In order to ensure that staff in high-traffic areas don't go
bonkers due to this new change, sound dampeners have been installed to
prevent the hydraulics from being heard for super long distances.
However, they skimped out on the quality for the ones in maintenance...
/🆑

FYI Mappers/Tweakers: This PR replaces all station map unres mapping
helpers with the new subtype. You are welcome to replace certain doors
with the old helper (the base type) to get the old behavior as you see
fit, just explain why that behavior is desired in that location.
Otherwise, it would be appreciated if we have this new behavior >95% of
the time.
2025-12-05 21:01:15 -05:00

235 lines
8.2 KiB
Plaintext

/datum/wires/airlock
holder_type = /obj/machinery/door/airlock
proper_name = "Generic Airlock"
/datum/wires/airlock/secure
proper_name = "High Security Airlock"
randomize = TRUE
/datum/wires/airlock/maint
dictionary_key = /datum/wires/airlock/maint
proper_name = "Maintenance Airlock"
/datum/wires/airlock/command
dictionary_key = /datum/wires/airlock/command
proper_name = "Command Airlock"
/datum/wires/airlock/service
dictionary_key = /datum/wires/airlock/service
proper_name = "Service Airlock"
/datum/wires/airlock/security
dictionary_key = /datum/wires/airlock/security
proper_name = "Security Airlock"
/datum/wires/airlock/engineering
dictionary_key = /datum/wires/airlock/engineering
proper_name = "Engineering Airlock"
/datum/wires/airlock/medbay
dictionary_key = /datum/wires/airlock/medbay
proper_name = "Medbay Airlock"
/datum/wires/airlock/science
dictionary_key = /datum/wires/airlock/science
proper_name = "Science Airlock"
/datum/wires/airlock/ai
dictionary_key = /datum/wires/airlock/ai
proper_name = "AI Airlock"
/datum/wires/airlock/cargo
dictionary_key = /datum/wires/airlock/cargo
proper_name = "Cargo Airlock"
/datum/wires/airlock/New(atom/holder)
wires = list(
WIRE_AI,
WIRE_BACKUP1,
WIRE_BACKUP2,
WIRE_BOLTS,
WIRE_IDSCAN,
WIRE_FEEDBACK,
WIRE_OPEN,
WIRE_POWER1,
WIRE_POWER2,
WIRE_SAFETY,
WIRE_SHOCK,
WIRE_TIMING,
WIRE_UNRESTRICTED_EXIT,
WIRE_ZAP1,
WIRE_ZAP2,
)
add_duds(2)
..()
/datum/wires/airlock/interact(mob/user)
var/obj/machinery/door/airlock/airlock_holder = holder
if (!HAS_SILICON_ACCESS(user) && airlock_holder.isElectrified() && airlock_holder.shock(user, 100))
return
return ..()
/datum/wires/airlock/interactable(mob/user)
if(!..())
return FALSE
var/obj/machinery/door/airlock/airlock = holder
if(!HAS_SILICON_ACCESS(user) && !isdrone(user) && airlock.isElectrified() && airlock.hasPower())
var/mob/living/carbon/carbon_user = user
if (!istype(carbon_user) || carbon_user.should_electrocute(get_area(airlock)))
return FALSE
if(airlock.is_secure())
return FALSE
if(airlock.panel_open)
return TRUE
/datum/wires/airlock/get_status()
var/obj/machinery/door/airlock/A = holder
var/list/status = list()
status += "The door bolts [A.locked ? "have engaged!" : "have disengaged."]"
status += "The test light is [A.hasPower() ? "on" : "off"]."
status += "The AI connection light is [A.aiControlDisabled || (A.obj_flags & EMAGGED) ? "off" : "on"]."
status += "The check wiring light is [A.safe ? "off" : "on"]."
status += "The timer is powered [A.autoclose ? "on" : "off"]."
status += "The speed light is [A.normalspeed ? "on" : "off"]."
status += "The emergency light is [A.emergency ? "on" : "off"]."
if(A.unres_latch)
status += "The unrestricted exit display is [A.unres_sides ? "indicating that it is letting people pass from the [dir2text(REVERSE_DIR(A.unres_sides))]" : "faintly flickering"]."
else
status += "The unrestricted exit display is completely inactive."
return status
/datum/wires/airlock/on_pulse(wire)
set waitfor = FALSE
var/obj/machinery/door/airlock/A = holder
switch(wire)
if(WIRE_POWER1, WIRE_POWER2) // Pulse to lose power.
A.loseMainPower()
if(WIRE_BACKUP1, WIRE_BACKUP2) // Pulse to lose backup power.
A.loseBackupPower()
if(WIRE_OPEN) // Pulse to open door (only works not emagged and ID wire is cut or no access is required).
if(A.obj_flags & EMAGGED)
return
if(!A.requiresID() || A.check_access(null))
if(A.density)
INVOKE_ASYNC(A, TYPE_PROC_REF(/obj/machinery/door/airlock, open), 1)
else
INVOKE_ASYNC(A, TYPE_PROC_REF(/obj/machinery/door/airlock, close), 1)
if(WIRE_BOLTS) // Pulse to toggle bolts (but only raises if power is on).
if(!A.locked)
A.bolt()
else
if(A.hasPower())
A.unbolt()
A.update_appearance()
if(WIRE_IDSCAN) // Pulse to disable emergency access and flash the red lights.
if(A.hasPower() && A.density)
A.run_animation(DOOR_DENY_ANIMATION)
if(A.emergency)
A.emergency = FALSE
A.update_appearance()
if(WIRE_AI) // Pulse to disable WIRE_AI control for 10 ticks (follows same rules as cutting).
if(A.aiControlDisabled == AI_WIRE_NORMAL)
A.aiControlDisabled = AI_WIRE_DISABLED
else if(A.aiControlDisabled == AI_WIRE_DISABLED_HACKED)
A.aiControlDisabled = AI_WIRE_HACKED
addtimer(CALLBACK(A, TYPE_PROC_REF(/obj/machinery/door/airlock, reset_ai_wire)), 1 SECONDS)
if(WIRE_SHOCK) // Pulse to shock the door for 10 ticks.
if(!A.secondsElectrified)
A.set_electrified(MACHINE_DEFAULT_ELECTRIFY_TIME, usr)
A.shock(usr, 100)
if(WIRE_SAFETY)
A.safe = !A.safe
if(!A.density)
A.close()
if(WIRE_TIMING)
A.normalspeed = !A.normalspeed
if(WIRE_FEEDBACK)
A.feedback = !A.feedback
A.update_appearance()
if(WIRE_UNRESTRICTED_EXIT) // Pulse to switch the direction around by 180 degrees (North goes to South, East goes to West, vice-versa)
if(!A.unres_latch) //only works if the "latch" is installed (a variable that we assign to the door either upon creation of a door with unrestricted directions or if an unrestricted helper is added to a door in mapping)
return
A.unres_sides = REVERSE_DIR(A.unres_sides)
A.update_appearance()
/obj/machinery/door/airlock/proc/reset_ai_wire()
if(aiControlDisabled == AI_WIRE_DISABLED)
aiControlDisabled = AI_WIRE_NORMAL
else if(aiControlDisabled == AI_WIRE_HACKED)
aiControlDisabled = AI_WIRE_DISABLED_HACKED
/datum/wires/airlock/on_cut(wire, mend, source)
var/obj/machinery/door/airlock/A = holder
switch(wire)
if(WIRE_POWER1, WIRE_POWER2) // Cut to lose power, repair all to gain power.
if(mend && !is_cut(WIRE_POWER1) && !is_cut(WIRE_POWER2))
A.regainMainPower()
else
A.loseMainPower()
if(isliving(usr))
A.shock(usr, 50)
if(WIRE_BACKUP1, WIRE_BACKUP2) // Cut to lose backup power, repair all to gain backup power.
if(mend && !is_cut(WIRE_BACKUP1) && !is_cut(WIRE_BACKUP2))
A.regainBackupPower()
else
A.loseBackupPower()
if(isliving(usr))
A.shock(usr, 50)
if(WIRE_BOLTS) // Cut to engage bolts, mend does nothing.
if(!mend)
A.bolt()
if(WIRE_AI) // Cut to disable WIRE_AI control, mend to re-enable.
if(mend)
if(A.aiControlDisabled == AI_WIRE_DISABLED) // 0 = normal, 1 = locked out, 2 = overridden by WIRE_AI, -1 = previously overridden by WIRE_AI
A.aiControlDisabled = AI_WIRE_NORMAL
else if(A.aiControlDisabled == AI_WIRE_HACKED)
A.aiControlDisabled = AI_WIRE_DISABLED_HACKED
else
if(A.aiControlDisabled == AI_WIRE_NORMAL)
A.aiControlDisabled = AI_WIRE_DISABLED
else if(A.aiControlDisabled == AI_WIRE_DISABLED_HACKED)
A.aiControlDisabled = AI_WIRE_HACKED
if(WIRE_SHOCK) // Cut to shock the door, mend to unshock.
if (!isnull(source))
log_combat(source, A, "[mend ? "disabled" : "enabled"] shocking for")
if(mend)
if(A.secondsElectrified)
A.set_electrified(MACHINE_NOT_ELECTRIFIED, usr)
else
if(A.secondsElectrified != MACHINE_ELECTRIFIED_PERMANENT)
A.set_electrified(MACHINE_ELECTRIFIED_PERMANENT, usr)
A.shock(usr, 100)
if(WIRE_SAFETY) // Cut to disable safeties, mend to re-enable.
A.safe = mend
if (!isnull(source))
log_combat(source, A, "[mend ? "enabled" : "disabled"] door safeties for")
if(WIRE_TIMING) // Cut to disable auto-close, mend to re-enable.
A.autoclose = mend
if(A.autoclose && !A.density)
INVOKE_ASYNC(A, TYPE_PROC_REF(/obj/machinery/door/airlock, close))
if(WIRE_FEEDBACK) // Cut to disable lights and sounds, mend to re-enable.
A.feedback = mend
A.update_appearance()
if(WIRE_ZAP1, WIRE_ZAP2) // Ouch.
if(isliving(usr))
A.shock(usr, 50)
if(WIRE_UNRESTRICTED_EXIT) // If this wire is cut, the unrestricted helper goes away. If you mend it, it'll go "haywire" and pick a new direction at random. Might have to cut/mend a time or two to get the direction you want.
if(!A.unres_latch) //only works if the "latch" is installed (a variable that we assign to the door either upon creation of a door with unrestricted directions, or if an unrestricted helper is added to a door in mapping)
return
if(mend)
A.unres_sides = pick(NORTH, SOUTH, EAST, WEST)
A.update_appearance()
else
A.unres_sides = NONE
A.update_appearance()
/datum/wires/airlock/can_reveal_wires(mob/user)
if(HAS_TRAIT(user, TRAIT_KNOW_ENGI_WIRES))
return TRUE
return ..()