[MIRROR] Adds support for "realistic" public elevators (elevator doors, elevator panel, etc) [MDB IGNORE] (#15515)

* Adds support for "realistic" public elevators (elevator doors, elevator panel, etc) (#68888)

* Elevators are a bit more friendly part 1
- Emaggable elevator buttons / support for emaggable device assemblies
- Delayed travel between floors
- Elevators can now show a warning sign to areas below while travelling
- Elevators can now optionally wound heavily instead of gibbing

* Adds supoprt for "lift doors"

* Comment tweak

* Splitting these variables

* Functional prototype

* multiz debug

* Elevator button framework

* Unifies these behaviors

* Emergency doors

* Lift button framework

* UI closes on floor change

* Testing changes

* Fix

* UI Tweaks

* Panel works pretty swell

* Minor tweaks

* Move to static

* Bonus tram change

* User experience

* Slight tweak to mapload stuff

* This is silly

* Some UI tweaks, need to update css

* CSS and ui overhaul

* Documentation updates.

* Multi-z lift support

* Multitile lift fix

* Adds support for "realistic" public elevators (elevator doors, elevator panel, etc)

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
This commit is contained in:
SkyratBot
2022-08-13 22:06:26 -07:00
committed by GitHub
co-authored by MrMelbert
parent 32870bcab4
commit 9bd406b65b
18 changed files with 1578 additions and 351 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -405,7 +405,7 @@
///from base of obj/item/attack_qdeleted(): (atom/target, mob/user, params)
#define COMSIG_ITEM_ATTACK_QDELETED "item_attack_qdeleted"
///from /obj/item/assembly/proc/pulsed()
///from /obj/item/assembly/proc/pulsed(mob/pulser)
#define COMSIG_ASSEMBLY_PULSED "assembly_pulsed"
///from base of /obj/item/mmi/set_brainmob(): (mob/living/brain/new_brainmob)
+4
View File
@@ -18,3 +18,7 @@
#define MAIN_STATION_TRAM "main station tram"
///the specific_lift_id of the tram on the hilbert research station
#define HILBERT_TRAM "tram_hilbert"
// Defines for update_lift_doors
#define OPEN_DOORS "open"
#define CLOSE_DOORS "close"
+6
View File
@@ -9,6 +9,12 @@
/obj/effect/temp_visual/telegraphing/vending_machine_tilt
duration = 1 SECONDS
/obj/effect/temp_visual/telegraphing/lift_travel
/obj/effect/temp_visual/telegraphing/lift_travel/Initialize(mapload, duration)
src.duration = duration
return ..()
/obj/effect/temp_visual/telegraphing/thunderbolt
icon = 'icons/mob/telegraphing/telegraph.dmi'
icon_state = "target_circle"
+23 -12
View File
@@ -230,12 +230,13 @@
var/list/stored_decals = list()
/obj/item/assembly/trapdoor/pulsed(radio)
/obj/item/assembly/trapdoor/pulsed(radio, mob/pulser)
. = ..()
if(linked)
return
if(!COOLDOWN_FINISHED(src, search_cooldown))
visible_message(span_warning("[src] cannot attempt another trapdoor linkup so soon!"))
if(loc && pulser)
loc.balloon_alert(pulser, "linking on cooldown!")
return
attempt_link_up()
COOLDOWN_START(src, search_cooldown, search_cooldown_time)
@@ -305,23 +306,33 @@
/obj/item/trapdoor_remote/attack_self(mob/user, modifiers)
. = ..()
if(.)
return
return TRUE
if(!internals)
to_chat(user, span_warning("[src] has no internals!"))
return
user.balloon_alert(user, "no device!")
return TRUE
if(!internals.linked)
to_chat(user, span_notice("You activate [src]."))
internals.pulsed()
return
internals.pulsed(pulser = user)
// The pulse linked successfully
if(internals.linked)
user.balloon_alert(user, "linked")
// The pulse failed to link
else
user.balloon_alert(user, "link failed!")
return TRUE
if(!COOLDOWN_FINISHED(src, trapdoor_cooldown))
to_chat(user, span_warning("[src] is on a short cooldown."))
return
to_chat(user, span_notice("You activate [src]."))
user.balloon_alert(user, "on cooldown!")
return TRUE
user.balloon_alert(user, "trapdoor triggered")
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
icon_state = "trapdoor_pressed"
addtimer(VARSET_CALLBACK(src, icon_state, initial(icon_state)), trapdoor_cooldown_time)
COOLDOWN_START(src, trapdoor_cooldown, trapdoor_cooldown_time)
internals.pulsed()
internals.pulsed(pulser = user)
return TRUE
#undef TRAPDOOR_LINKING_SEARCH_RANGE
+8 -15
View File
@@ -107,6 +107,7 @@
return ..()
/obj/machinery/button/emag_act(mob/user)
. = ..()
if(obj_flags & EMAGGED)
return
req_access = list()
@@ -114,6 +115,12 @@
playsound(src, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
obj_flags |= EMAGGED
// The device inside can be emagged by swiping the button
// returning TRUE will prevent feedback (so we can do our own)
if(device?.emag_act(user))
return
balloon_alert(user, "access overridden")
/obj/machinery/button/attack_ai(mob/user)
if(!silicon_access_disabled && !panel_open)
return attack_hand(user)
@@ -175,7 +182,7 @@
icon_state = "[skin]1"
if(device)
device.pulsed()
device.pulsed(pulser = user)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_BUTTON_PRESSED,src)
addtimer(CALLBACK(src, /atom/.proc/update_appearance), 15)
@@ -309,20 +316,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/door, 24)
custom_materials = list(/datum/material/iron=MINERAL_MATERIAL_AMOUNT)
pixel_shift = 24
/obj/machinery/button/elevator
name = "elevator button"
desc = "Go back. Go back. Go back. Can you operate the elevator."
icon_state = "launcher"
skin = "launcher"
device_type = /obj/item/assembly/control/elevator
req_access = list()
id = 1
/obj/machinery/button/elevator/examine(mob/user)
. = ..()
. += span_notice("There's a small inscription on the button...")
. += span_notice("THIS CALLS THE ELEVATOR! IT DOES NOT OPERATE IT! Interact with the elevator itself to use it!")
/obj/machinery/button/tram
name = "tram caller"
desc = "A button for calling the tram. It has a speakerbox in it with some internals."
+4 -4
View File
@@ -63,11 +63,11 @@
return TRUE
///Called when another assembly acts on this one, var/radio will determine where it came from for wire calcs
/obj/item/assembly/proc/pulsed(radio = FALSE)
/obj/item/assembly/proc/pulsed(radio = FALSE, mob/pulser)
if(wire_type & WIRE_RECEIVE)
INVOKE_ASYNC(src, .proc/activate)
INVOKE_ASYNC(src, .proc/activate, pulser)
if(radio && (wire_type & WIRE_RADIO_RECEIVE))
INVOKE_ASYNC(src, .proc/activate)
INVOKE_ASYNC(src, .proc/activate, pulser)
SEND_SIGNAL(src, COMSIG_ASSEMBLY_PULSED)
return TRUE
@@ -83,7 +83,7 @@
return TRUE
/// What the device does when turned on
/obj/item/assembly/proc/activate()
/obj/item/assembly/proc/activate(mob/activator)
if(QDELETED(src) || !secured || (next_activate > world.time))
return FALSE
next_activate = world.time + 30
-53
View File
@@ -175,59 +175,6 @@
addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 50)
//how long it spends on each floor when moving somewhere, so it'd take 4 seconds to reach you if it had to travel up 2 floors
#define FLOOR_TRAVEL_TIME 2 SECONDS
/obj/item/assembly/control/elevator
name = "elevator controller"
desc = "A small device used to call elevators to the current floor."
/obj/item/assembly/control/elevator/activate()
if(cooldown)
return
cooldown = TRUE
var/datum/lift_master/lift
for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
if(possible_match.specific_lift_id != id || !check_z(possible_match) || possible_match.controls_locked)
continue
lift = possible_match
break
if(!lift)
addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 2 SECONDS)
return
var/obj/structure/industrial_lift/target = lift.lift_platforms[1]
var/target_z = target.z
lift.set_controls(LIFT_PLATFORM_LOCKED)
///The z level to which the elevator should travel
var/targetZ = (abs(loc.z)) //The target Z (where the elevator should move to) is not our z level (we are just some assembly in nullspace) but actually the Z level of whatever we are contained in (e.g. elevator button)
///The amount of z levels between the our and targetZ
var/difference = abs(targetZ - target_z)
///Direction (up/down) needed to go to reach targetZ
var/direction = target_z < targetZ ? UP : DOWN
///How long it will/should take us to reach the target Z level
var/travel_duration = FLOOR_TRAVEL_TIME * difference //100 / 2 floors up = 50 seconds on every floor, will always reach destination in the same time
addtimer(VARSET_CALLBACK(src, cooldown, FALSE), travel_duration)
for(var/i in 1 to difference)
sleep(FLOOR_TRAVEL_TIME)//hey this should be alright... right?
if(QDELETED(lift) || QDELETED(src))//elevator control or button gone = don't go up anymore
return
lift.MoveLift(direction, null)
lift.set_controls(LIFT_PLATFORM_UNLOCKED)
///check if any of the lift platforms are already here
/obj/item/assembly/control/elevator/proc/check_z(datum/lift_master/lift)
for(var/obj/structure/industrial_lift/platform as anything in lift.lift_platforms)
if(platform.z == loc.z)
return FALSE
return TRUE
#undef FLOOR_TRAVEL_TIME
/obj/item/assembly/control/tram
name = "tram call button"
desc = "A small device used to bring trams to you."
@@ -0,0 +1,164 @@
/obj/item/assembly/control/elevator
name = "elevator controller"
desc = "A small device used to call elevators to the current floor."
/// A weakref to the lift_master datum we control
var/datum/weakref/lift_weakref
/obj/item/assembly/control/elevator/Initialize(mapload)
. = ..()
if(mapload)
return INITIALIZE_HINT_LATELOAD
var/datum/lift_master/lift = get_lift()
if(!lift)
return
lift_weakref = WEAKREF(lift)
/obj/item/assembly/control/elevator/LateInitialize()
var/datum/lift_master/lift = get_lift()
if(!lift)
log_mapping("Elevator call button at [AREACOORD(src)] found no associated lift to link with, this may be a mapping error.")
return
lift_weakref = WEAKREF(lift)
// Emagging elevator buttons will disable safeties
/obj/item/assembly/control/elevator/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
return
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return
for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
lift_platform.violent_landing = TRUE
lift_platform.warns_on_down_movement = FALSE
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed) * 0.5
// Note that we can either be emagged by having the button we are inside swiped,
// or by someone emagging the assembly directly after removing it (to be cheeky)
var/atom/balloon_alert_loc = get(src, /obj/machinery/button) || src
balloon_alert_loc.balloon_alert(user, "safeties overridden")
obj_flags |= EMAGGED
return TRUE
// Multitooling emagged elevator buttons will fix the safeties
/obj/item/assembly/control/elevator/multitool_act(mob/living/user)
if(!(obj_flags & EMAGGED))
return ..()
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return ..()
for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
lift_platform.violent_landing = initial(lift_platform.violent_landing)
lift_platform.warns_on_down_movement = initial(lift_platform.warns_on_down_movement)
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed)
// We can only be multitooled directly so just throw up the balloon alert
balloon_alert(user, "safeties reset")
obj_flags &= ~EMAGGED
return TRUE
/obj/item/assembly/control/elevator/activate(mob/activator)
if(cooldown)
return
cooldown = TRUE
// Actually try to call the elevator - this sleeps.
// If we failed to call it, play a buzz sound.
if(!call_elevator(activator))
playsound(loc, 'sound/machines/buzz-two.ogg', 50, TRUE)
// Finally, give people a chance to get off after it's done before going back off cooldown
addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 2 SECONDS)
/// Actually calls the elevator.
/// Returns FALSE if we failed to setup the move.
/// Returns TRUE if the move setup was a success, EVEN IF the move itself fails afterwards
/obj/item/assembly/control/elevator/proc/call_elevator(mob/activator)
// We can't call an elevator that doesn't exist
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
loc.balloon_alert(activator, "no elevator connected!")
return FALSE
// We can't call an elevator that's moving. You may say "you totally can do that", but that's not modelled
if(lift.controls_locked == LIFT_PLATFORM_LOCKED)
loc.balloon_alert(activator, "elevator is moving!")
return FALSE
// We can't call an elevator if it's already at this destination
var/obj/structure/industrial_lift/prime_lift = lift.return_closest_platform_to_z(loc.z)
if(prime_lift.z == loc.z)
loc.balloon_alert(activator, "elevator is here!")
return FALSE
// At this point, we can start moving.
// Give the user, if supplied, a balloon alert.
if(activator)
loc.balloon_alert(activator, "elevator called")
// Actually try to move the lift. This will sleep.
if(!lift.move_to_zlevel(loc.z, CALLBACK(src, .proc/check_button)))
loc.balloon_alert(activator, "elevator out of service!")
return FALSE
// From here on all returns are TRUE, as we successfully moved the lift, even if we maybe didn't reach our floor
// Our button was destroyed mid transit.
if(!check_button())
return TRUE
// Our lift platform survived, but it didn't reach our landing z.
if(!QDELETED(prime_lift) && prime_lift.z != loc.z)
if(!QDELETED(activator))
loc.balloon_alert(activator, "elevator out of service!")
playsound(loc, 'sound/machines/buzz-sigh.ogg', 50, TRUE)
return TRUE
// Everything went according to plan
playsound(loc, 'sound/machines/ping.ogg', 50, TRUE)
if(!QDELETED(activator))
loc.balloon_alert(activator, "elevator arrived")
return TRUE
/// Callback for move_to_zlevel / general proc to check if we're still in a button
/obj/item/assembly/control/elevator/proc/check_button()
if(QDELETED(src))
return FALSE
if(!istype(loc, /obj/machinery/button))
return FALSE
return TRUE
/// Gets the lift associated with our assembly / button
/obj/item/assembly/control/elevator/proc/get_lift()
for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
if(possible_match.specific_lift_id != id)
continue
return possible_match
return null
/obj/machinery/button/elevator
name = "elevator button"
desc = "Go back. Go back. Go back. Can you operate the elevator."
icon_state = "launcher"
skin = "launcher"
device_type = /obj/item/assembly/control/elevator
req_access = list()
id = 1
/obj/machinery/button/elevator/Initialize(mapload, ndir, built)
. = ..()
// Kind of a cop-out
AddElement(/datum/element/contextual_screentip_bare_hands, lmb_text = "Call Elevator")
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/button/elevator, 28)
@@ -0,0 +1,361 @@
/**
* # Elevator control panel
*
* A wallmounted simple machine that controls elevators,
* allowing users to enter a UI to move it up or down
*
* These can be placed in two methods:
* - You can place the control panel on the same turf as a lift. It will move up and down with the lift
* - You can place the control panel to the side of a lift, NOT attached to the lift. It will remain in position
* I don't recommend using both methods on the same elevator, as it might result in some jank, but it's functional.
*/
/obj/machinery/elevator_control_panel
name = "elevator panel"
// Fire alarm reference.
desc = "<i>\"In case of emergency, please use the stairs.\"</i> Thus, always use the stairs."
density = FALSE
icon = 'icons/obj/airlock_machines.dmi'
icon_state = "airlock_control_standby"
base_icon_state = "airlock_control"
power_channel = AREA_USAGE_ENVIRON
// Indestructible until someone wants to make these constructible, with all the chaos that implies
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
/// Were we instantiated at mapload? Used to determine when we should link / throw errors
var/maploaded = FALSE
/// A weakref to the lift_master datum we control
var/datum/weakref/lift_weakref
/// What specific_lift_id do we link with?
var/linked_elevator_id
/// A list of all possible destinations this elevator can travel.
/// Assoc list of "Floor name" to "z level of destination".
/// By default the floor names will auto-generate ("Floor 1", "Floor 2", etc).
var/list/linked_elevator_destination
/// If you want to override what each floor is named as, you can do so with this list.
/// Make this an assoc list of "z level you want to rename" to "desired name".
/// So, if you want the z-level 2 destination to be named "Cargo", you would do list("2" = "Cargo").
var/list/preset_destination_names
/// What z-level did we move to last? Used for showing the user in the UI which direction we're moving.
var/last_move_target
/// TimerID to our door reset timer, made by emergency opening doors
var/door_reset_timerid
/obj/machinery/elevator_control_panel/Initialize(mapload)
. = ..()
var/static/list/tool_behaviors = list(
TOOL_MULTITOOL = list(SCREENTIP_CONTEXT_LMB = "Reset Panel"),
)
AddElement(/datum/element/contextual_screentip_tools, tool_behaviors)
AddElement(/datum/element/contextual_screentip_bare_hands, lmb_text = "Send Elevator")
// Machinery returns lateload by default via parent,
// this is just here for redundancy's sake.
. = INITIALIZE_HINT_LATELOAD
maploaded = mapload
// Maploaded panels link in LateInitialize...
if(mapload)
return
// And non-mapload panels link in Initialize
var/datum/lift_master/lift = get_associated_lift()
if(!lift)
return
lift_weakref = WEAKREF(lift)
populate_destinations_list(lift)
/obj/machinery/elevator_control_panel/LateInitialize()
. = ..()
// If we weren't maploaded, we probably already linked (or tried to link) in Initialize().
if(!maploaded)
return
// This is exclusively for linking in mapload, just to ensure all elevator parts are created,
// and also so we can throw mapping errors to let people know if they messed up setup.
var/datum/lift_master/lift = get_associated_lift()
if(!lift)
log_mapping("Elevator control panel at [AREACOORD(src)] found no associated lift to link with, this may be a mapping error.")
return
lift_weakref = WEAKREF(lift)
populate_destinations_list(lift)
/obj/machinery/elevator_control_panel/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
return
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return
for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
lift_platform.violent_landing = TRUE
lift_platform.warns_on_down_movement = FALSE
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed) * 0.5
playsound(src, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
balloon_alert(user, "safeties overridden")
obj_flags |= EMAGGED
/obj/machinery/elevator_control_panel/multitool_act(mob/living/user)
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return
balloon_alert(user, "resetting panel...")
playsound(src, 'sound/machines/locktoggle.ogg', 50, TRUE)
if(!do_after(user, 6 SECONDS, src))
balloon_alert(user, "interrupted!")
return TRUE
if(QDELETED(lift) || !length(lift.lift_platforms))
return
// If we were emagged, reset us
if(obj_flags & EMAGGED)
for(var/obj/structure/industrial_lift/lift_platform as anything in lift.lift_platforms)
lift_platform.violent_landing = initial(lift_platform.violent_landing)
lift_platform.warns_on_down_movement = initial(lift_platform.warns_on_down_movement)
lift_platform.elevator_vertical_speed = initial(lift_platform.elevator_vertical_speed)
obj_flags &= ~EMAGGED
// If we had doors open, stop the timer and reset them
if(door_reset_timerid)
deltimer(door_reset_timerid)
reset_doors()
// Be vague about whether something was accomplished or not
balloon_alert(user, "panel reset")
playsound(src, 'sound/machines/locktoggle.ogg', 50, TRUE)
return TRUE
/// Find the elevator associated with our lift button.
/obj/machinery/elevator_control_panel/proc/get_associated_lift()
for(var/datum/lift_master/possible_match as anything in GLOB.active_lifts_by_type[BASIC_LIFT_ID])
if(possible_match.specific_lift_id != linked_elevator_id)
continue
return possible_match
return null
/// Goes through and populates the linked_elevator_destination list with all possible destinations the lift can go.
/obj/machinery/elevator_control_panel/proc/populate_destinations_list(datum/lift_master/linked_lift)
// This list will track all the raw z-levels which we found that we can travel to
var/list/raw_destinations = list()
// Get a list of all the starting locs our elevator starts at
var/list/starting_locs = list()
for(var/obj/structure/industrial_lift/lift_piece as anything in linked_lift.lift_platforms)
starting_locs |= lift_piece.locs
// The raw destination list will start with all the z's we start at
raw_destinations |= lift_piece.z
// Get all destinations below us
add_destinations_in_a_direction_recursively(starting_locs, DOWN, raw_destinations)
// Get all destinations above us
add_destinations_in_a_direction_recursively(starting_locs, UP, raw_destinations)
linked_elevator_destination = list()
for(var/z_level in raw_destinations)
// Check if this z-level has a preset destination associated.
var/preset_name = preset_destination_names?[num2text(z_level)]
// If we don't have a preset name, use Floor z-1 for the title.
// z - 1 is used because the station z-level is 2, and goes up.
linked_elevator_destination["[z_level]"] = preset_name || "Floor [z_level - 1]"
// Reverse the destination list.
// By this point the list will go from bottom floor to top floor,
// which is unintuitive when passed to the UI to show to users.
// This way we have the top floors at the top, and the bottom floors the bottom.
reverse_range(linked_elevator_destination)
update_static_data_for_all_viewers()
/**
* Recursively adds destinations to the list of linked_elevator_destination
* until it fails to find a valid stopping point in the passed direction.
*/
/obj/machinery/elevator_control_panel/proc/add_destinations_in_a_direction_recursively(list/turfs_to_check, direction, list/destinations)
// Only vertical elevators are supported - use trams for horizontal ones.
if(direction != UP && direction != DOWN)
CRASH("[type] was given an invalid direction in add_destinations_in_a_direction_recursively!")
var/list/turf/checked_turfs = list()
// Go through every turf passed in our list of turfs to check.
for(var/turf/place in turfs_to_check)
// If the place we're checking isn't openspace, then we can't go downwards
if(direction == DOWN && !istype(place, /turf/open/openspace))
return
// Check the turf at the next level (either above or below the place we're checking)
var/turf/next_level = get_step_multiz(place, direction)
// No turf = at the edge of a map vertically
if(!next_level)
return
// If the next level above us has a roof, we can't move up
if(direction == UP && !istype(next_level, /turf/open/openspace))
return
// Otherwise, we can feasibly move our direction with this turf
checked_turfs += next_level
// If we somehow found no turfs, BUT made it this far, something definitely went wrong.
if(!length(checked_turfs))
CRASH("[type] found no turfs in add_destinations_in_a_direction_recursively!")
// Add the Zs of all the found turfs as possible destinations
for(var/turf/found as anything in checked_turfs)
// We check all turfs we found in case of multi-z lift memes.
destinations |= found.z
// And recursively call the proc with all the turfs we found on the next level
add_destinations_in_a_direction_recursively(checked_turfs, direction, destinations)
/obj/machinery/elevator_control_panel/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ElevatorPanel", name)
ui.open()
/obj/machinery/elevator_control_panel/ui_status(mob/user)
// We moved up a z-level, probably via the lift itself, so don't preserve the UI.
if(user.z != z)
return UI_CLOSE
// Our lift is gone entirely - look, but don't touch.
if(!lift_weakref?.resolve())
return UI_UPDATE
// We're non-functional - don't do anything.
if(!check_panel())
return UI_DISABLED
// Otherwise, just check default state (is the user conscious and close, etc).
return ..()
/obj/machinery/elevator_control_panel/ui_data(mob/user)
var/list/data = list()
data["emergency_level"] = capitalize(SSsecurity_level.get_current_level_as_text())
data["is_emergency"] = SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED
data["doors_open"] = !!door_reset_timerid
var/datum/lift_master/lift = lift_weakref?.resolve()
if(lift)
data["lift_exists"] = TRUE
data["currently_moving"] = lift.controls_locked == LIFT_PLATFORM_LOCKED
data["currently_moving_to_floor"] = last_move_target
data["current_floor"] = lift.lift_platforms[1].z
else
data["lift_exists"] = FALSE
data["currently_moving"] = FALSE
data["current_floor"] = 0 // 0 shows up as "Floor -1" in the UI, which is fine for what it is
return data
/obj/machinery/elevator_control_panel/ui_static_data(mob/user)
var/list/data = list()
data["all_floor_data"] = list()
for(var/destination in linked_elevator_destination)
data["all_floor_data"] += list(list(
"name" = linked_elevator_destination[destination],
"z_level" = text2num(destination),
))
return data
/obj/machinery/elevator_control_panel/ui_act(action, list/params)
. = ..()
if(.)
return
if(!check_panel())
return TRUE // We shouldn't be usable right now, update UI
switch(action)
if("move_lift")
if(!allowed(usr))
balloon_alert(usr, "access denied!")
return
var/desired_z = params["z"]
// num2text here is required as the z is stored as strings in the list, but passed here as a number.
if(!(num2text(desired_z) in linked_elevator_destination))
return TRUE // Something is inaccurate, update UI
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift || lift.controls_locked == LIFT_PLATFORM_LOCKED)
return TRUE // We shouldn't be moving anything, update UI
INVOKE_ASYNC(lift, /datum/lift_master.proc/move_to_zlevel, desired_z, CALLBACK(src, .proc/check_panel), usr)
last_move_target = desired_z
return TRUE // Succcessfully initiated a move. Regardless of whether it actually works, update the UI
if("emergency_door")
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return TRUE // Something is wrong, update UI
// The emergency door button is only available at red alert or higher.
// This is so people don't keep it in emergency mode 100% of the time.
if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_RED)
return TRUE // The security level might have been lowered since last update, so update UI
// Open all lift doors, it's an emergency dang it!
lift.update_lift_doors(action = OPEN_DOORS)
door_reset_timerid = addtimer(CALLBACK(src, .proc/reset_doors), 3 MINUTES, TIMER_UNIQUE|TIMER_STOPPABLE)
return TRUE // We opened up all the doors, update the UI so the emergency button is replaced correctly
if("reset_doors")
if(!door_reset_timerid)
return TRUE // All the doors were shut since last update, so update UI
deltimer(door_reset_timerid)
reset_doors()
return TRUE // We closed all the doors, update the UI so the door button is replaced correctly
/// Callback for move_to_zlevel to ensure the lift can continue to move.
/obj/machinery/elevator_control_panel/proc/check_panel()
if(QDELETED(src))
return FALSE
if(machine_stat & (NOPOWER|BROKEN))
return FALSE
return TRUE
/// Helper proc to go through all of our desetinations and reset all elevator doors,
/// closing doors on z-levels the lift is away from, and opening doors on the z the lift is
/obj/machinery/elevator_control_panel/proc/reset_doors()
var/datum/lift_master/lift = lift_weakref?.resolve()
if(!lift)
return
var/list/zs_we_are_present_on = lift.get_zs_we_are_on()
var/list/zs_we_are_absent = list()
for(var/z_level in linked_elevator_destination)
z_level = text2num(z_level) // Stored as strings.
if(z_level in zs_we_are_present_on)
continue
zs_we_are_absent |= z_level
// Open all the doors on the zs we should be open on,
// and close all doors we aren't on. Simple enough.
lift.update_lift_doors(zs_we_are_present_on, action = OPEN_DOORS)
lift.update_lift_doors(zs_we_are_absent, action = CLOSE_DOORS)
door_reset_timerid = null
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/elevator_control_panel, 30)
+179 -77
View File
@@ -51,10 +51,26 @@ GLOBAL_LIST_EMPTY(lifts)
///if TRUE, this platform will late initialize and then expand to become a multitile object across all other linked platforms on this z level
var/create_multitile_platform = FALSE
/// Does our elevator warn people (with visual effects) when moving down?
var/warns_on_down_movement = FALSE
/// if TRUE, we will gib anyone we land on top of. if FALSE, we will just apply damage with a serious wound penalty.
var/violent_landing = TRUE
/// How long does it take for the elevator to move vertically?
var/elevator_vertical_speed = 2 SECONDS
/// We use a radial to travel primarily, instead of a button / ui
var/radial_travel = TRUE
/// A lazylist of REFs to all mobs which have a radial open currently
var/list/current_operators
/obj/structure/industrial_lift/Initialize(mapload)
. = ..()
GLOB.lifts.Add(src)
// Yes if it's VV'd it won't be accurate but it probably shouldn't ever be
if(radial_travel)
AddElement(/datum/element/contextual_screentip_bare_hands, lmb_text = "Send Elevator")
set_movement_registrations()
//since lift_master datums find all connected platforms when an industrial lift first creates it and then
@@ -273,30 +289,41 @@ GLOBAL_LIST_EMPTY(lifts)
for(var/turf/dest_turf as anything in entering_locs)
SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
if(istype(dest_turf, /turf/closed/wall))
var/turf/closed/wall/C = dest_turf
do_sparks(2, FALSE, C)
C.dismantle_wall(devastated = TRUE)
for(var/mob/M in urange(8, src))
shake_camera(M, 2, 3)
playsound(C, 'sound/effects/meteorimpact.ogg', 100, TRUE)
if(iswallturf(dest_turf))
var/turf/closed/wall/hit_wall = dest_turf
do_sparks(2, FALSE, hit_wall)
hit_wall.dismantle_wall(devastated = TRUE)
for(var/mob/nearby_witness in urange(8, src))
shake_camera(nearby_witness, 2, 3)
playsound(hit_wall, 'sound/effects/meteorimpact.ogg', 100, TRUE)
for(var/mob/living/crushed in dest_turf.contents)
to_chat(crushed, span_userdanger("You are crushed by [src]!"))
crushed.gib(FALSE,FALSE,FALSE)//the nicest kind of gibbing, keeping everything intact.
if(violent_landing)
// Violent landing = gibbed. But the nicest kind of gibbing, keeping everything intact.
crushed.gib(FALSE, FALSE, FALSE)
else
// Less violent landing simply crushes every bone in your body.
crushed.Paralyze(30 SECONDS, ignore_canstun = TRUE)
crushed.apply_damage(30, BRUTE, BODY_ZONE_CHEST, wound_bonus = 30)
crushed.apply_damage(20, BRUTE, BODY_ZONE_HEAD, wound_bonus = 25)
crushed.apply_damage(15, BRUTE, BODY_ZONE_L_LEG, wound_bonus = 15)
crushed.apply_damage(15, BRUTE, BODY_ZONE_R_LEG, wound_bonus = 15)
crushed.apply_damage(15, BRUTE, BODY_ZONE_L_ARM, wound_bonus = 15)
crushed.apply_damage(15, BRUTE, BODY_ZONE_R_ARM, wound_bonus = 15)
else if(going == UP)
for(var/turf/dest_turf as anything in entering_locs)
///handles any special interactions objects could have with the lift/tram, handled on the item itself
SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
if(istype(dest_turf, /turf/closed/wall))
var/turf/closed/wall/C = dest_turf
do_sparks(2, FALSE, C)
C.dismantle_wall(devastated = TRUE)
if(iswallturf(dest_turf))
var/turf/closed/wall/hit_wall = dest_turf
do_sparks(2, FALSE, hit_wall)
hit_wall.dismantle_wall(devastated = TRUE)
for(var/mob/client_mob in SSspatial_grid.orthogonal_range_search(src, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, 8))
shake_camera(client_mob, 2, 3)
playsound(C, 'sound/effects/meteorimpact.ogg', 100, TRUE)
playsound(hit_wall, 'sound/effects/meteorimpact.ogg', 100, TRUE)
else
///potentially finds a spot to throw the victim at for daring to be hit by a tram. is null if we havent found anything to throw
@@ -507,47 +534,93 @@ GLOBAL_LIST_EMPTY(lifts)
return TRUE
/obj/structure/industrial_lift/proc/use(mob/living/user)
if(!isliving(user) || !in_range(src, user) || user.combat_mode)
/// Callback / general proc to check if the lift is usable by the passed mob.
/obj/structure/industrial_lift/proc/can_open_lift_radial(mob/living/user, starting_position)
// Gotta be a living mob
if(!isliving(user))
return FALSE
// Gotta be awake and aware
if(user.incapacitated())
return FALSE
// Maintain the god given right to fight an elevator
if(user.combat_mode)
return FALSE
// Gotta be by the lift
if(!user.Adjacent(src))
return FALSE
// If the lift moves while the radial is open close that shit
if(starting_position != loc)
return FALSE
return TRUE
/// Opens the radial for the lift, allowing the user to move it around.
/obj/structure/industrial_lift/proc/open_lift_radial(mob/living/user)
var/starting_position = loc
if(!can_open_lift_radial(user, starting_position))
return
// One radial per person
for(var/obj/structure/industrial_lift/other_platform as anything in lift_master_datum.lift_platforms)
if(REF(user) in other_platform.current_operators)
return
var/list/possible_directions = list()
if(lift_master_datum.Check_lift_move(UP))
var/static/image/up_arrow
if(!up_arrow)
up_arrow = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH)
possible_directions["Up"] = up_arrow
if(lift_master_datum.Check_lift_move(DOWN))
var/static/image/down_arrow
if(!down_arrow)
down_arrow = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH)
possible_directions["Down"] = down_arrow
add_fingerprint(user)
if(!length(possible_directions))
balloon_alert(user, "elevator out of service!")
return
var/list/tool_list = list()
if(lift_master_datum.Check_lift_move(UP))
tool_list["Up"] = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH)
if(lift_master_datum.Check_lift_move(DOWN))
tool_list["Down"] = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH)
if(!length(tool_list))
to_chat(user, span_warning("[src] doesn't seem to able to move anywhere!"))
add_fingerprint(user)
return
if(lift_master_datum.controls_locked)
to_chat(user, span_warning("[src] has its controls locked! It must already be trying to do something!"))
add_fingerprint(user)
return
var/result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, .proc/check_menu, user, src.loc), require_near = TRUE, tooltips = TRUE)
if(!isliving(user) || !in_range(src, user) || user.combat_mode)
LAZYADD(current_operators, REF(user))
var/result = show_radial_menu(
user = user,
anchor = src,
choices = possible_directions,
custom_check = CALLBACK(src, .proc/can_open_lift_radial, user, starting_position),
require_near = TRUE,
tooltips = TRUE,
)
LAZYREMOVE(current_operators, REF(user))
if(!can_open_lift_radial(user, starting_position))
return //nice try
if(!isnull(result) && result != "Cancel" && lift_master_datum.controls_locked)
// Only show this message if they actually wanted to move
balloon_alert(user, "elevator controls locked!")
return
switch(result)
if("Up")
// We have to make sure that they don't do illegal actions by not having their radial menu refresh from someone else moving the lift.
if(!lift_master_datum.Check_lift_move(UP))
to_chat(user, span_warning("[src] doesn't seem to able to move up!"))
add_fingerprint(user)
// We have to make sure that they don't do illegal actions
// by not having their radial menu refresh from someone else moving the lift.
if(!lift_master_datum.simple_move_wrapper(UP, elevator_vertical_speed, user))
return
lift_master_datum.MoveLift(UP, user)
show_fluff_message(TRUE, user)
use(user)
show_fluff_message(UP, user)
open_lift_radial(user)
if("Down")
if(!lift_master_datum.Check_lift_move(DOWN))
to_chat(user, span_warning("[src] doesn't seem to able to move down!"))
add_fingerprint(user)
if(!lift_master_datum.simple_move_wrapper(DOWN, elevator_vertical_speed, user))
return
lift_master_datum.MoveLift(DOWN, user)
show_fluff_message(FALSE, user)
use(user)
show_fluff_message(DOWN, user)
open_lift_radial(user)
if("Cancel")
return
add_fingerprint(user)
/**
* Proc to ensure that the radial menu closes when it should.
@@ -567,45 +640,76 @@ GLOBAL_LIST_EMPTY(lifts)
. = ..()
if(.)
return
use(user)
if(!radial_travel)
return ..()
return open_lift_radial(user)
//ai probably shouldn't get to use lifts but they sure are great for admins to crush people with
/obj/structure/industrial_lift/attack_ghost(mob/user)
. = ..()
if(.)
return
if(isAdminGhostAI(user))
use(user)
if(!radial_travel)
return
if(!isAdminGhostAI(user))
return
return open_lift_radial(user)
/obj/structure/industrial_lift/attack_paw(mob/user, list/modifiers)
return use(user)
if(!radial_travel)
return ..()
/obj/structure/industrial_lift/attackby(obj/item/W, mob/user, params)
return use(user)
return open_lift_radial(user)
/obj/structure/industrial_lift/attack_robot(mob/living/silicon/robot/R)
if(R.Adjacent(src))
return use(R)
/obj/structure/industrial_lift/attackby(obj/item/attacking_item, mob/user, params)
if(!radial_travel)
return ..()
return open_lift_radial(user)
/obj/structure/industrial_lift/attack_robot(mob/living/user)
if(!radial_travel)
return ..()
return open_lift_radial(user)
/**
* Shows a message indicating that the lift has moved up or down.
* Arguments:
* * going_up - Boolean on whether or not we're going up, to adjust the message appropriately.
* * direction - What direction are we going
* * user - The mob that caused the lift to move, for the visible message.
*/
/obj/structure/industrial_lift/proc/show_fluff_message(going_up, mob/user)
if(going_up)
/obj/structure/industrial_lift/proc/show_fluff_message(direction, mob/user)
if(direction == UP)
user.visible_message(span_notice("[user] moves the lift upwards."), span_notice("You move the lift upwards."))
else
if(direction == DOWN)
user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards."))
// A subtype intended for "public use"
/obj/structure/industrial_lift/public
icon = 'icons/turf/floors.dmi'
icon_state = "rockvault"
base_icon_state = null
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
warns_on_down_movement = TRUE
violent_landing = FALSE
elevator_vertical_speed = 3 SECONDS
radial_travel = FALSE
/obj/structure/industrial_lift/debug
name = "transport platform"
desc = "A lightweight platform. It moves in any direction, except up and down."
color = "#5286b9ff"
lift_id = DEBUG_LIFT_ID
radial_travel = TRUE
/obj/structure/industrial_lift/debug/use(mob/user)
/obj/structure/industrial_lift/debug/open_lift_radial(mob/user)
if (!in_range(src, user))
return
//NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST
@@ -626,29 +730,29 @@ GLOBAL_LIST_EMPTY(lifts)
switch(result)
if("NORTH")
lift_master_datum.MoveLiftHorizontal(NORTH, z)
use(user)
lift_master_datum.move_lift_horizontally(NORTH, z)
open_lift_radial(user)
if("NORTHEAST")
lift_master_datum.MoveLiftHorizontal(NORTHEAST, z)
use(user)
lift_master_datum.move_lift_horizontally(NORTHEAST, z)
open_lift_radial(user)
if("EAST")
lift_master_datum.MoveLiftHorizontal(EAST, z)
use(user)
lift_master_datum.move_lift_horizontally(EAST, z)
open_lift_radial(user)
if("SOUTHEAST")
lift_master_datum.MoveLiftHorizontal(SOUTHEAST, z)
use(user)
lift_master_datum.move_lift_horizontally(SOUTHEAST, z)
open_lift_radial(user)
if("SOUTH")
lift_master_datum.MoveLiftHorizontal(SOUTH, z)
use(user)
lift_master_datum.move_lift_horizontally(SOUTH, z)
open_lift_radial(user)
if("SOUTHWEST")
lift_master_datum.MoveLiftHorizontal(SOUTHWEST, z)
use(user)
lift_master_datum.move_lift_horizontally(SOUTHWEST, z)
open_lift_radial(user)
if("WEST")
lift_master_datum.MoveLiftHorizontal(WEST, z)
use(user)
lift_master_datum.move_lift_horizontally(WEST, z)
open_lift_radial(user)
if("NORTHWEST")
lift_master_datum.MoveLiftHorizontal(NORTHWEST, z)
use(user)
lift_master_datum.move_lift_horizontally(NORTHWEST, z)
open_lift_radial(user)
if("Cancel")
return
@@ -668,6 +772,7 @@ GLOBAL_LIST_EMPTY(lifts)
lift_id = TRAM_LIFT_ID
lift_master_type = /datum/lift_master/tram
radial_travel = FALSE
/// Set by the tram control console in late initialize
var/travelling = FALSE
@@ -699,9 +804,6 @@ GLOBAL_LIST_EMPTY(lifts)
src.travelling = travelling
SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
/obj/structure/industrial_lift/tram/use(mob/user) //dont click the floor dingus we use computers now
return
/obj/structure/industrial_lift/tram/set_currently_z_moving()
return FALSE //trams can never z fall and shouldnt waste any processing time trying to do so
+233 -6
View File
@@ -31,8 +31,6 @@ GLOBAL_LIST_EMPTY(active_lifts_by_type)
///overridable ID string to link control units to this specific lift_master datum. created by placing a lift id landmark object
///somewhere on the tram, if its anywhere on the tram we'll find it in init and set this to whatever it specifies
var/specific_lift_id
///what directions we're allowed to move
var/allowed_travel_directions = ALL
///if true, the lift cannot be manually moved.
var/controls_locked = FALSE
@@ -239,6 +237,42 @@ GLOBAL_LIST_EMPTY(active_lifts_by_type)
return winner
/// Returns a lift platform on the z-level which is vertically closest to the passed target_z
/datum/lift_master/proc/return_closest_platform_to_z(target_z)
var/obj/structure/industrial_lift/found_platform
for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
// Already at the same Z-level, we can stop
if(lift.z == target_z)
found_platform = lift
break
// Set up an initial lift to compare to
if(!found_platform)
found_platform = lift
continue
// Same level, we can go with the one we currently have
if(lift.z == found_platform.z)
continue
// If the difference between the current found platform and the target
// if less than the distance between the next lift and the target,
// our current platform is closer to the target than the next one, so we can skip it
if(abs(found_platform.z - target_z) < abs(lift.z - target_z))
continue
// The difference is smaller for this lift, so it's closer
found_platform = lift
return found_platform
/// Returns a list of all the z-levels our lift is currently on.
/datum/lift_master/proc/get_zs_we_are_on()
var/list/zs_we_are_present_on = list()
for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
zs_we_are_present_on |= lift.z
return zs_we_are_present_on
///returns all industrial_lifts associated with this tram on the given z level or given atoms z level
/datum/lift_master/proc/get_platforms_on_level(atom/atom_reference_OR_z_level_number)
var/z = atom_reference_OR_z_level_number
@@ -259,13 +293,12 @@ GLOBAL_LIST_EMPTY(active_lifts_by_type)
/**
* Moves the lift UP or DOWN, this is what users invoke with their hand.
* This is a SAFE proc, ensuring every part of the lift moves SANELY.
* It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
*
* Arguments:
* going - UP or DOWN directions, where the lift should go. Keep in mind by this point checks of whether it should go up or down have already been done.
* user - Whomever made the lift movement.
*/
/datum/lift_master/proc/MoveLift(going, mob/user)
set_controls(LIFT_PLATFORM_LOCKED)
/datum/lift_master/proc/move_lift_vertically(going, mob/user)
//lift_platforms are sorted in order of lowest z to highest z, so going upwards we need to move them in reverse order to not collide
if(going == UP)
var/obj/structure/industrial_lift/platform_to_move
@@ -280,14 +313,208 @@ GLOBAL_LIST_EMPTY(active_lifts_by_type)
else if(going == DOWN)
for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
lift_platform.travel(going)
/**
* Moves the lift after a passed delay.
*
* This is a more "user friendly" or "realistic" lift move.
* It includes things like:
* - Allowing lift "travel time"
* - Shutting elevator safety doors
* - Sound effects while moving
* - Safety warnings for anyone below the lift (while it's moving downwards)
*
* Arguments:
* duration - required, how long do we wait to move the lift?
* door_duration - optional, how long should we wait to open the doors after arriving? If null, we won't open or close doors
* direction - which direction are we moving the lift?
* user - optional, who is moving the lift?
*/
/datum/lift_master/proc/move_after_delay(lift_move_duration, door_duration, direction, mob/user)
if(!isnum(lift_move_duration))
CRASH("[type] move_after_delay called with invalid duration ([lift_move_duration]).")
if(lift_move_duration <= 0 SECONDS)
move_lift_vertically(direction, user)
return
// Get the lowest or highest lift according to which direction we're moving
var/obj/structure/industrial_lift/prime_lift = return_closest_platform_to_z(direction == UP ? world.maxz : 0)
// If anyone changes the hydraulic sound effect I sure hope they update this variable...
var/hydraulic_sfx_duration = 2 SECONDS
// ...because we use the duration of the sound effect to make it last for roughly the duration of the lift travel
playsound(prime_lift, 'sound/mecha/hydraulic.ogg', 25, vary = TRUE, frequency = clamp(hydraulic_sfx_duration / lift_move_duration, 0.33, 3))
// Move the lift after a timer
addtimer(CALLBACK(src, .proc/move_lift_vertically, direction, user), lift_move_duration, TIMER_UNIQUE)
// Open doors after the set duration if supplied
if(isnum(door_duration))
addtimer(CALLBACK(src, .proc/open_lift_doors_callback), door_duration, TIMER_UNIQUE)
// Here on we only care about lifts going DOWN
if(direction != DOWN)
return
// Okay we're going down, let's try to display some warnings to people below
var/list/turf/lift_locs = list()
for(var/obj/structure/industrial_lift/going_to_move as anything in lift_platforms)
// This lift has no warnings so we don't even need to worry about it
if(!going_to_move.warns_on_down_movement)
continue
// Collect all the turfs our lift is found at
lift_locs |= going_to_move.locs
for(var/turf/moving in lift_locs)
// Find what's below the turf that's moving
var/turf/below_us = get_step_multiz(moving, DOWN)
// Hold up the turf below us is also in our locs list. Multi-z lift? Don't show a warning
if(below_us in lift_locs)
continue
// Display the warning for until we land
new /obj/effect/temp_visual/telegraphing/lift_travel(below_us, lift_move_duration)
/**
* Simple wrapper for checking if we can move 1 zlevel, and if we can, do said move.
* Locks controls, closes all doors, then moves the lift and re-opens the doors afterwards.
*
* Arguments:
* direction - which direction are we moving?
* lift_move_duration - how long does the move take? can be 0 or null for instant move.
* door_duration - how long does it take for the doors to open after a move?
* user - optional, who moved it?
*/
/datum/lift_master/proc/simple_move_wrapper(direction, lift_move_duration, mob/user)
if(!Check_lift_move(direction))
return FALSE
// Lock controls, to prevent moving-while-moving memes
set_controls(LIFT_PLATFORM_LOCKED)
// Close all lift doors
update_lift_doors(action = CLOSE_DOORS)
if(isnull(lift_move_duration) || lift_move_duration <= 0 SECONDS)
// Do an instant move
move_lift_vertically(direction, user)
// Open doors on the zs we arrive at
update_lift_doors(get_zs_we_are_on(), action = OPEN_DOORS)
// And unlock the controls after
set_controls(LIFT_PLATFORM_UNLOCKED)
return TRUE
// Do a delayed move
move_after_delay(
lift_move_duration = lift_move_duration,
door_duration = lift_move_duration * 1.5,
direction = direction,
user = user,
)
addtimer(CALLBACK(src, .proc/set_controls, LIFT_PLATFORM_UNLOCKED), lift_move_duration * 1.5)
return TRUE
/**
* Moves the lift to the passed z-level.
*
* Checks for validity of the move: Are we moving to the same z-level, can we actually move to that z-level?
* Does NOT check if the lift controls are currently locked.
*
* Moves to the passed z-level by calling move_after_delay repeatedly until the passed z-level is reached.
* This proc sleeps as it moves.
*
* Arguments:
* target_z - required, the Z we want to move to
* loop_callback - optional, an additional callback invoked during the l oop that allows the move to cancel.
* user - optional, who started the move
*/
/datum/lift_master/proc/move_to_zlevel(target_z, datum/callback/loop_callback, mob/user)
if(!isnum(target_z) || target_z <= 0)
CRASH("[type] move_to_zlevel was passed an invalid target_z ([target_z]).")
var/obj/structure/industrial_lift/prime_lift = return_closest_platform_to_z(target_z)
var/lift_z = prime_lift.z
// We're already at the desired z-level!
if(target_z == lift_z)
return FALSE
// The amount of z levels between the our and target_z
var/z_difference = abs(target_z - lift_z)
// Direction (up/down) needed to go to reach target_z
var/direction = lift_z < target_z ? UP : DOWN
// We can't go that way anymore, or possibly ever
if(!Check_lift_move(direction))
return FALSE
// Okay we're ready to start moving now.
set_controls(LIFT_PLATFORM_LOCKED)
var/travel_speed = prime_lift.elevator_vertical_speed
// Close all lift doors
update_lift_doors(action = CLOSE_DOORS)
// Approach the desired z-level one step at a time
for(var/i in 1 to z_difference)
if(!Check_lift_move(direction))
break
if(loop_callback && !loop_callback.Invoke())
break
// move_after_delay will set up a timer and cause us to move after a time
move_after_delay(
lift_move_duration = travel_speed,
direction = direction,
user = user,
)
// and we don't want to send another request until the timer's done
stoplag(travel_speed + 0.1 SECONDS)
if(QDELETED(src) || QDELETED(prime_lift))
return
addtimer(CALLBACK(src, .proc/open_lift_doors_callback), 2 SECONDS)
set_controls(LIFT_PLATFORM_UNLOCKED)
return TRUE
/**
* Updates all blast doors and shutters that share an ID with our lift.
*
* Arguments:
* on_z_level - optional, only open doors on this z-level or list of z-levels
* action - how do we update the doors? OPEN_DOORS to make them open, CLOSE_DOORS to make them shut
*/
/datum/lift_master/proc/update_lift_doors(on_z_level, action)
if(!isnull(on_z_level) && !islist(on_z_level))
on_z_level = list(on_z_level)
var/played_ding = FALSE
for(var/obj/machinery/door/poddoor/elevator_door in GLOB.machines)
if(elevator_door.id != specific_lift_id)
continue
if(on_z_level && !(elevator_door.z in on_z_level))
continue
switch(action)
if(OPEN_DOORS)
INVOKE_ASYNC(elevator_door, /obj/machinery/door/poddoor.proc/open)
if(CLOSE_DOORS)
INVOKE_ASYNC(elevator_door, /obj/machinery/door/poddoor.proc/close)
else
stack_trace("Elevator lift update_lift_doors called with an improper action ([action]).")
if(!played_ding)
playsound(elevator_door, 'sound/machines/ding.ogg', 50, TRUE)
played_ding = TRUE
/// Helper used in callbacks to open all the doors our lift is on
/datum/lift_master/proc/open_lift_doors_callback()
update_lift_doors(get_zs_we_are_on(), action = OPEN_DOORS)
/**
* Moves the lift, this is what users invoke with their hand.
* This is a SAFE proc, ensuring every part of the lift moves SANELY.
* It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
*/
/datum/lift_master/proc/MoveLiftHorizontal(going)
/datum/lift_master/proc/move_lift_horizontally(going)
set_controls(LIFT_PLATFORM_LOCKED)
if(multitile_platform)
@@ -109,7 +109,7 @@
if(tram_part.controls_locked || tram_part.travelling) // someone else started already
return FALSE
tram_part.tram_travel(to_where)
visible_message("The tram has been called to [to_where.name]")
say("The tram has been called to [to_where.name].")
return TRUE
/obj/item/circuit_component/tram_controls
@@ -125,7 +125,7 @@
var/start_time = TICK_USAGE
travel_distance--
MoveLiftHorizontal(travel_direction)
move_lift_horizontally(travel_direction)
var/duration = TICK_USAGE_TO_MS(start_time)
if(slowed_down)
+1 -1
View File
@@ -39,7 +39,7 @@
/obj/item/circuit_component/assembly_input/unregister_shell(atom/movable/shell)
UnregisterSignal(shell, list(COMSIG_ASSEMBLY_PULSED, COMSIG_ITEM_ATTACK_SELF))
/obj/item/circuit_component/assembly_input/proc/on_pulsed()
/obj/item/circuit_component/assembly_input/proc/on_pulsed(datum/source, mob/pulser)
SIGNAL_HANDLER
signal.set_output(COMPONENT_SIGNAL)
+2
View File
@@ -3153,6 +3153,8 @@
#include "code\modules\hydroponics\grown\weeds\nettle.dm"
#include "code\modules\hydroponics\grown\weeds\starthistle.dm"
#include "code\modules\industrial_lift\crossing_signal.dm"
#include "code\modules\industrial_lift\elevator_button.dm"
#include "code\modules\industrial_lift\elevator_panel.dm"
#include "code\modules\industrial_lift\industrial_lift.dm"
#include "code\modules\industrial_lift\lift_master.dm"
#include "code\modules\industrial_lift\tram_controls.dm"
@@ -0,0 +1,205 @@
import { clamp } from 'common/math';
import { BooleanLike } from 'common/react';
import { useBackend } from '../backend';
import { Box, Blink, Button, Dimmer, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';
type FloorData = {
name: string;
z_level: number;
};
type ElevatorPanelData = {
// What Z the lift is on
current_floor: number;
// Station emergency level - "Red", "Green", etc
emergency_level: string;
// State: Is the station in red or delta alert?
is_emergency: BooleanLike;
// State: Are the doors emergency opened?
doors_open: BooleanLike;
// State: Does the lift exist?
lift_exists: BooleanLike;
// State: Is the lift moving?
currently_moving: BooleanLike;
// What floor are we currently moving to?
currently_moving_to_floor: number | null;
// A list of all floors we can move around to
all_floor_data: FloorData[];
};
export const ElevatorPanel = (props, context) => {
const { data, act } = useBackend<ElevatorPanelData>(context);
const {
current_floor,
emergency_level,
is_emergency,
doors_open,
lift_exists,
currently_moving,
all_floor_data,
} = data;
/*
* We want to grow our UI if we have a lot of floors to display,
* while also leaving some whitespace - just so it's not too cluttered.
*
* 400 is chosen for the min, to prevent it from being too tiny.
*
* 600 is chosen for the max, as it fits 10 floors pretty comfortably.
* If you seriously need more than 10 floors for an elevator in this game,
* you should reconsider your map's layout. It's not worth it.
*/
const calculatedHeight = clamp(all_floor_data.length * 90, 400, 600);
return (
<Window width={200} height={calculatedHeight} theme="retro">
<Window.Content>
{!lift_exists && <NoLiftDimmer />}
<Stack height="100%" vertical>
<Stack.Item>
<Section title="Floor" align="center">
<FloorPanel />
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill align="center">
{!!currently_moving && <MovingDimmer />}
<Stack vertical width="90%">
{all_floor_data.map((floor, index) => (
<Stack.Item key={index}>
<Button
style={{
'font-weight': 'bold',
'font-size': '14px',
}}
fluid
ellipsis
textAlign="left"
icon="circle"
disabled={floor.z_level === current_floor}
onClick={() => act('move_lift', { z: floor.z_level })}>
{floor.name}
</Button>
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section>
{doors_open ? (
<Button
width="65%"
icon="door-closed"
tooltip={
'Closes all elevator doors, except \
those on the level of the elevator.'
}
onClick={() => act('reset_doors')}>
Reset Doors
</Button>
) : (
<Button
width="65%"
icon="door-open"
disabled={!is_emergency}
color={'bad'}
tooltip={
is_emergency
? 'In case of emergency, opens all lift doors.'
: `The station is only at ${emergency_level} alert.`
}
onClick={() => act('emergency_door')}>
Emergency
</Button>
)}
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const NoLiftDimmer = () => {
return (
<Dimmer>
<Stack vertical align="center">
<Stack.Item>
<Icon size={8} name="exclamation" />
</Stack.Item>
<Stack.Item fontSize="16px">No elevator connected.</Stack.Item>
</Stack>
</Dimmer>
);
};
const MovingDimmer = () => {
return (
<Dimmer>
<Stack vertical align="center">
<Stack.Item>
<Icon size={8} name="spinner" spin />
</Stack.Item>
<Stack.Item fontSize="16px">Moving...</Stack.Item>
</Stack>
</Dimmer>
);
};
const FloorPanel = (props, context) => {
const { data } = useBackend<ElevatorPanelData>(context);
const { current_floor, currently_moving, currently_moving_to_floor } = data;
return (
<Stack width="50%" backgroundColor="black">
<Stack.Item ml={2} mr={1} mt={1} mb={1}>
<Stack vertical align="center">
<Stack.Item>
<ArrowIcon
icon="arrow-up"
is_moving={
currently_moving &&
currently_moving_to_floor &&
currently_moving_to_floor > current_floor
}
/>
</Stack.Item>
<Stack.Item>
<ArrowIcon
icon="arrow-down"
is_moving={
currently_moving &&
currently_moving_to_floor &&
currently_moving_to_floor < current_floor
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item mt={1}>
<Box
textColor="white"
style={{
'font-family': 'Monospace',
'font-size': '50px',
'font-weight': 'bold',
}}>
{current_floor - 1}
</Box>
</Stack.Item>
</Stack>
);
};
const ArrowIcon = (props) => {
return props.is_moving ? (
<Blink time={500} interval={500}>
<Icon name={props.icon} color={'green'} size={2} />
</Blink>
) : (
<Icon name={props.icon} color={'grey'} size={2} />
);
};
+12 -1
View File
@@ -28,7 +28,7 @@
'../components/Button.scss',
$with: (
'color-default': #e8e4c9,
'color-disabled': #363636,
'color-disabled': #505046,
'color-selected': #9d0808,
'color-caution': #be6209,
'color-danger': #9a9d00
@@ -55,6 +55,17 @@
outline: base.em(1px) solid #161613;
}
.Button--disabled {
color: #c5c5c2;
font-family: monospace;
&:hover {
color: #ffffff;
}
&:focus {
color: #ffffff;
}
}
.Layout__content {
background-image: none;
}