Cargo elevator overhaul (#21430)

## About PR

Implements an animation logic heavily inspired from CM-SS13 elevators.
Elevator hatch and shaft sprites are courtesy of @KingOfThePing!

Currently if something is standing on the hatches when the elevator is
arriving, they'll be sent to CC level, fall, get paralyzed and severely
injured enough to have broken bones/arterial bleeding.

This PR also fixes cargo announcement screens not displaying elevator
ETA.

## Images

<details><summary>Elevator arriving</summary>
<p>



https://github.com/user-attachments/assets/34879994-d10b-4cb6-959e-314ec20411b8

</p>
</details> 

<details><summary>Elevator departing</summary>
<p>


https://github.com/user-attachments/assets/c345fd3b-07eb-4922-9b28-31cf40842da1

</p>
</details> 

<details><summary>Unfortunate victims falling to the pit</summary>
<p>



https://github.com/user-attachments/assets/98feac04-5d65-4383-8abc-363ce51a028a


</p>
</details> 

### Asset Licenses
The following assets that **have not** been created by myself are
included in this PR:

| Path | Original Author | License |
| --- | --- | --- |
| icons/effect/224x192.dmi | KingOfThePing | CC BY-SA 3.0 |
| icons/obj/doors/retractable_railing.dmi | CM-SS13 | CC BY-SA 3.0 |
| sound/machines/industrial_lift_raising.ogg | CM-SS13 | CC BY-SA 3.0 |
| sound/machines/industrial_lift_lowering.ogg | CM-SS13 | CC BY-SA 3.0 |
| sound/machines/warning-buzzer-2.ogg | Unknown -
https://pixabay.com/sound-effects/incorrect-buzzer-sound-147336/ |
Pixabay Licence |
This commit is contained in:
Kano
2025-10-05 17:26:49 +03:00
committed by GitHub
parent 451eb5dcef
commit 2d9d4ce4b6
15 changed files with 1078 additions and 543 deletions
+1
View File
@@ -351,6 +351,7 @@
icon_state = "stairs_railing"
anchored = TRUE
density = TRUE
layer = ABOVE_ABOVE_HUMAN_LAYER
/obj/structure/stairs_railing/CanPass(atom/movable/mover, turf/target, height=0, air_group=0)
if(mover?.movement_type & PHASING)
+239 -2
View File
@@ -1,12 +1,84 @@
/// Used for placing '/obj/effect/elevator' to the northern corners of the shaft.
#define SHAFT_WIDTH 6
/// Used for locating center of the shaft.
#define SHAFT_CENTER_OFFSET 3
#define ELEVATOR_DEPARTING_X 224
#define ELEVATOR_DEPARTING_Y -96
/datum/shuttle/autodock/ferry/supply
category = /datum/shuttle/autodock/ferry/supply
var/away_location = 1 //the location to hide at while pretending to be in-transit
/// The location to hide at while pretending to be in-transit.
var/away_location = 1
var/late_chance = 80
var/max_late_time = 300
/// A list for step_triggers, used for toggling fall functionality. Making the elevator shaft tiles safe to stand or not depening on where the elevator is.
var/list/step_trigger_group = list()
/// Elevator effects for each northern corners of the shaft. This way the entire elevator won't disappear if there's an opaque obstacle in the sight line of original turf.
/// Later on hardcoded to have their icons positioned on top of each other.
/// These contain elevator shaft sprite. '/obj/effect/elevator/animation_overlay/elevator_animation' is used as a masked layer onto this.
var/obj/effect/elevator/NW
var/obj/effect/elevator/NE
/// This effect will contain an image, copy of the turfs at CC supply shuttle zone. Added onto the elevator shaft effect as an off-centered overlay.
var/obj/effect/elevator/animation_overlay/elevator_animation
/// Hatch layers. Works similar to 'elevator_animation'.
var/obj/effect/elevator/animation_overlay/hatch/left/hatch_left
var/obj/effect/elevator/animation_overlay/hatch/right/hatch_right
/// Locations caches for Horizon elevator bay.
var/target_dest_x
var/target_dest_y
var/target_dest_z
/// Area cache of Horizon elevator bay.
var/area/horizon_elevator_area
/datum/shuttle/autodock/ferry/supply/New(var/_name, var/obj/effect/shuttle_landmark/start_waypoint)
..(_name, start_waypoint)
SScargo.shuttle = src
addtimer(CALLBACK(src, PROC_REF(prepare_elevator)), 1 MINUTE) // pseudo initialize. We give mapload some time before initializing rest of the properties
/datum/shuttle/autodock/ferry/supply/proc/prepare_elevator()
var/obj/dest_helper = locate(/obj/effect/landmark/destination_helper/cargo_elevator)
target_dest_x = dest_helper.x
target_dest_y = dest_helper.y
target_dest_z = dest_helper.z
horizon_elevator_area = get_area(dest_helper)
var/center_dest_x = target_dest_x + SHAFT_CENTER_OFFSET
var/group_number
for(var/obj/effect/step_trigger/cargo_elevator/ST in horizon_elevator_area) // sorts step triggers by their distance to the shaft center
group_number = "distance_[abs(ST.x - center_dest_x)]"
LAZYADDASSOCLIST(step_trigger_group, group_number, ST)
elevator_animation = new /obj/effect/elevator/animation_overlay()
elevator_animation.pixel_x = ELEVATOR_DEPARTING_X // 7 tiles
elevator_animation.pixel_y = ELEVATOR_DEPARTING_Y // 3 tiles
hatch_left = new /obj/effect/elevator/animation_overlay/hatch/left()
hatch_right = new /obj/effect/elevator/animation_overlay/hatch/right()
// North-West corner
NW = new /obj/effect/elevator(get_turf(src))
NW.pixel_y = -192 // 6 tiles
NW.vis_contents += elevator_animation
NW.vis_contents += hatch_left
NW.vis_contents += hatch_right
// North-East corner
NE = new /obj/effect/elevator(get_turf(src))
NE.pixel_x = -192
NE.pixel_y = -192
NE.vis_contents += elevator_animation
NE.vis_contents += hatch_left
NE.vis_contents += hatch_right
// We position the shaft beforehand
NW.forceMove(locate(target_dest_x, target_dest_y, target_dest_z))
NE.forceMove(locate(target_dest_x + SHAFT_WIDTH, target_dest_y, target_dest_z))
/datum/shuttle/autodock/ferry/supply/short_jump(var/area/destination)
if(moving_status != SHUTTLE_IDLE)
@@ -26,9 +98,13 @@
moving_status = SHUTTLE_IDLE
return
if (!at_station()) //at centcom
if (!at_station()) //at centcom
if(!SScargo.buy()) //Check if the shuttle can be sent
moving_status = SHUTTLE_IDLE //Dont move the shuttle
return
flip_rotating_alarms() // elevator is coming up, prepare the lights
playsound(locate(target_dest_x + SHAFT_CENTER_OFFSET, target_dest_y - SHAFT_CENTER_OFFSET, target_dest_z), 'sound/machines/warning-buzzer-2.ogg', 60, FALSE)
//We pretend it's a long_jump by making the shuttle stay at centcom for the "in-transit" period.
var/obj/effect/shuttle_landmark/away_waypoint = get_location_waypoint(away_location)
@@ -37,6 +113,7 @@
//If we are at the away_landmark then we are just pretending to move, otherwise actually do the move
if (next_location == away_waypoint)
attempt_move(away_waypoint)
play_elevator_animation(TRUE) // returning to CC, play the animation after elevator physically leaves
//wait ETA here.
arrive_time = world.time + SScargo.movetime
@@ -48,6 +125,7 @@
if (prob(late_chance))
sleep(rand(0,max_late_time))
play_elevator_animation() // coming from CC, play the animation before elevator physically arrives, delay the process
attempt_move(destination)
moving_status = SHUTTLE_IDLE
@@ -75,3 +153,162 @@
/datum/shuttle/autodock/ferry/supply/proc/eta_minutes()
var/ticksleft = arrive_time - world.time
return round(ticksleft/600,1)
/**************************
Elevator Animations
***************************/
/datum/shuttle/autodock/ferry/supply/proc/play_elevator_animation(returning_to_CC = FALSE)
// Positioning the shafts
NW.forceMove(locate(target_dest_x, target_dest_y, target_dest_z))
NE.forceMove(locate(target_dest_x + SHAFT_WIDTH, target_dest_y, target_dest_z))
for(var/area/A in shuttle_area) // an image copy of the elevator platform is prepared here
for(var/turf/T in A)
elevator_animation.vis_contents += T
if(!returning_to_CC)
handle_arrival_sequence() // coming to Horizon
else
handle_departure_sequence() // leaving the Horizon
elevator_animation.vis_contents.Cut() // animation is done, we can get rid of the elevator platform image
/datum/shuttle/autodock/ferry/supply/proc/handle_arrival_sequence()
var/obj/effect/step_trigger/cargo_elevator/trigger
playsound(locate(target_dest_x + SHAFT_CENTER_OFFSET, target_dest_y - SHAFT_CENTER_OFFSET, target_dest_z), 'sound/machines/industrial_lift_raising.ogg', 100, FALSE)
INVOKE_ASYNC(src, PROC_REF(handle_step_triggers))
animate(hatch_left, pixel_x = -112, time = 5 SECONDS)
animate(hatch_right, pixel_x = 112, time = 5 SECONDS)
sleep(4 SECONDS)
animate(elevator_animation, pixel_x = 0, pixel_y = 0, time = 4 SECONDS)
sleep(4.2 SECONDS)
for(var/group_key in step_trigger_group)
for(trigger in step_trigger_group[group_key])
trigger.safe_to_walk = TRUE
// We move the shafts away to avoid layering issues with turfs
NW.moveToNullspace()
NE.moveToNullspace()
addtimer(CALLBACK(src, PROC_REF(flip_rotating_alarms)), 4 SECONDS)
addtimer(CALLBACK(src, PROC_REF(toggle_railings)), 2 SECONDS)
/datum/shuttle/autodock/ferry/supply/proc/handle_departure_sequence()
flip_rotating_alarms()
toggle_railings()
sleep(2 SECONDS)
// checking for standers is redundant here, since the elevator won't leave if a forbidden type is standing
// we also want to avoid sending people to CC level in a case where we don't immediately retrieve them back after doing so
playsound(locate(target_dest_x + SHAFT_CENTER_OFFSET, target_dest_y - SHAFT_CENTER_OFFSET, target_dest_z), 'sound/machines/industrial_lift_lowering.ogg', 100, FALSE)
animate(elevator_animation, pixel_x = ELEVATOR_DEPARTING_X, pixel_y = ELEVATOR_DEPARTING_Y, time = 4 SECONDS)
sleep(4.2 SECONDS)
animate(hatch_left, pixel_x = 0, time = 5 SECONDS)
animate(hatch_right, pixel_x = 0, time = 5 SECONDS)
addtimer(CALLBACK(src, PROC_REF(flip_rotating_alarms)), 9 SECOND)
/// This attempts to sync up with hatch animation, by activating groups of step triggers in delayed order.
/datum/shuttle/autodock/ferry/supply/proc/handle_step_triggers()
var/obj/effect/step_trigger/cargo_elevator/trigger
var/group_key
for(var/i in 0 to 3)
group_key = "distance_[i]"
for(trigger in step_trigger_group[group_key]) // checking for the things standing on top of the hatch, then we send them to falling
for(var/atom/movable/AM in get_turf(trigger))
if(istype(AM, /obj/effect)) // avoid picking up step_triggers
continue
trigger.handle_falling(AM)
trigger.safe_to_walk = FALSE
sleep(1 SECOND)
/datum/shuttle/autodock/ferry/supply/proc/flip_rotating_alarms()
for(var/obj/machinery/rotating_alarm/RA in horizon_elevator_area)
RA.toggle_state()
/datum/shuttle/autodock/ferry/supply/proc/toggle_railings()
for(var/obj/structure/railing/retractable/R in horizon_elevator_area)
R.toggle_state()
/obj/effect/landmark/destination_helper/cargo_elevator
name = "cargo elevator destination helper"
/obj/effect/step_trigger/cargo_elevator
name = "cargo elevator shaft"
icon = 'icons/effects/map_effects.dmi'
simulated = FALSE // this prevents the effects getting transported along with the elevator
/// List of turfs within the 'area/supply/dock'. Shared between all instances.
var/static/list/possible_turfs_to_land_on = list()
/// Boolean, determines whether the movables should fall or not.
var/safe_to_walk = TRUE
/obj/effect/step_trigger/cargo_elevator/Initialize()
. = ..()
if(!possible_turfs_to_land_on.len)
possible_turfs_to_land_on = get_area_turfs(/area/supply/dock)
/obj/effect/step_trigger/cargo_elevator/Destroy()
LAZYNULL(possible_turfs_to_land_on)
return ..()
/obj/effect/step_trigger/cargo_elevator/Trigger(atom/movable/AM)
if(safe_to_walk)
return
INVOKE_ASYNC(src, PROC_REF(handle_falling), AM)
/obj/effect/step_trigger/cargo_elevator/proc/handle_falling(atom/movable/AM)
if(!isliving(AM) && !isobj(AM)) // only mobs and objects
return
if(isliving(AM))
var/mob/living/L = AM
if(L.CanAvoidGravity())
return
L.apply_effect(2, WEAKEN)
INVOKE_ASYNC(src, PROC_REF(animate_falling), AM) // this needs to run asynchronously, otherwise will cause issues
/obj/effect/step_trigger/cargo_elevator/proc/animate_falling(atom/movable/AM)
sleep(0.1 SECOND) // allowing some time for mobs to get weaken effect applied
var/oldalpha = AM.alpha
var/oldcolor = AM.color
var/old_pixel_y = AM.pixel_y
animate(AM, transform = matrix() - matrix(), alpha = 0, color = rgb(0, 0, 0), time = 1 SECOND)
for(var/i in 1 to 5)
//Make sure the item is still there after our sleep
if(!AM || QDELETED(AM))
return
AM.pixel_y--
sleep(0.2 SECONDS)
//Make sure the item is still there after our sleep
if(!AM || QDELETED(AM))
return
AM.visible_message(SPAN_WARNING("[AM] falls into [src]!"), SPAN_DANGER("You stumble and stare into \the [src] as you fall. It will be a long ride..."))
AM.forceMove(pick(possible_turfs_to_land_on))
AM.alpha = oldalpha
AM.color = oldcolor
AM.transform = matrix()
AM.pixel_y = old_pixel_y
if(isliving(AM))
var/mob/living/L = AM
L.adjustBruteLoss(rand(30,50))
L.dir = pick(GLOB.cardinals)
#undef SHAFT_WIDTH
#undef SHAFT_CENTER_OFFSET
#undef ELEVATOR_DEPARTING_X
#undef ELEVATOR_DEPARTING_Y