Files
Alexis 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

974 lines
39 KiB
Plaintext

/**
* Base transport structure. A single tile that can form a modular set with neighbouring tiles
* This type holds elevators and trams
*/
/obj/structure/transport/linear
name = "linear transport module"
desc = "A lightweight lift platform. It moves."
icon = 'icons/obj/smooth_structures/catwalk.dmi'
icon_state = "catwalk-0"
base_icon_state = "catwalk"
density = FALSE
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
armor_type = /datum/armor/transport_module
max_integrity = 50
layer = TRAM_FLOOR_LAYER
plane = GAME_PLANE
smoothing_flags = SMOOTH_BITMASK
smoothing_groups = SMOOTH_GROUP_INDUSTRIAL_LIFT
canSmoothWith = SMOOTH_GROUP_INDUSTRIAL_LIFT
obj_flags = BLOCK_Z_OUT_DOWN
appearance_flags = PIXEL_SCALE|KEEP_TOGETHER //no TILE_BOUND since we're potentially multitile
// If we don't do this, we'll build our overlays early, and fuck up how we're rendered
blocks_emissive = EMISSIVE_BLOCK_NONE
///ID used to determine what transport types we can merge with
var/transport_id = TRANSPORT_TYPE_ELEVATOR
///if true, the elevator works through floors
var/pass_through_floors = FALSE
///what movables on our platform that we are moving
var/list/atom/movable/transport_contents = list()
///weakrefs to the contents we have when we're first created. stored so that admins can clear the tram to its initial state
///if someone put a bunch of stuff onto it.
var/list/datum/weakref/initial_contents = list()
///what glide_size we set our moving contents to.
var/glide_size_override = 8
///movables inside transport_contents who had their glide_size changed since our last movement.
///used so that we dont have to change the glide_size of every object every movement, which scales to cost more than you'd think
var/list/atom/movable/changed_gliders = list()
///decisecond delay between horizontal movements. cannot make the tram move faster than 1 movement per world.tick_lag. only used to give to the transport_controller
var/internal_movement_delay = 0.5
///master datum that controls our movement. in general /transport/linear subtypes control moving themselves, and
/// /datum/transport_controller instances control moving the entire tram and any behavior associated with that.
var/datum/transport_controller/linear/transport_controller_datum
///what subtype of /datum/transport_controller to create for itself if no other platform on this tram has created one yet.
///very important for some behaviors since
var/transport_controller_type = /datum/transport_controller/linear
///how many tiles this platform extends on the x axis
var/width = 1
///how many tiles this platform extends on the y axis (north-south not up-down, that would be the z axis)
var/height = 1
///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_modular_set = 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
/// damage multiplier if a mob is hit by the lift while it is moving horizontally
var/collision_lethality = 1
/// 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
/datum/armor/transport_module
melee = 80
bullet = 90
bomb = 70
fire = 100
acid = 100
/obj/structure/transport/linear/Initialize(mapload)
. = ..()
// 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 Transport")
ADD_TRAIT(src, TRAIT_CHASM_STOPPER, INNATE_TRAIT)
set_movement_registrations()
//since transport_controller datums find all connected platforms when a transport structure first creates it and then
//sets those platforms' transport_controller_datum to itself, this check will only evaluate to true once per tram platform
if(!transport_controller_datum && transport_controller_type)
transport_controller_datum = new transport_controller_type(src)
return INITIALIZE_HINT_LATELOAD
/obj/structure/transport/linear/LateInitialize()
//after everything is initialized the transport controller can order everything
transport_controller_datum.order_platforms_by_z_level()
/obj/structure/transport/linear/Destroy()
transport_controller_datum = null
return ..()
///set the movement registrations to our current turf(s) so contents moving out of our tile(s) are removed from our movement lists
/obj/structure/transport/linear/proc/set_movement_registrations(list/turfs_to_set)
for(var/turf/turf_loc as anything in turfs_to_set || locs)
RegisterSignal(turf_loc, COMSIG_ATOM_EXITED, PROC_REF(uncrossed_remove_item_from_transport))
RegisterSignals(turf_loc, list(COMSIG_ATOM_ENTERED,COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(add_item_on_transport))
///unset our movement registrations from turfs that no longer contain us (or every loc if turfs_to_unset is unspecified)
/obj/structure/transport/linear/proc/unset_movement_registrations(list/turfs_to_unset)
var/static/list/registrations = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON)
for(var/turf/turf_loc as anything in turfs_to_unset || locs)
UnregisterSignal(turf_loc, registrations)
/obj/structure/transport/linear/proc/uncrossed_remove_item_from_transport(datum/source, atom/movable/gone, direction)
SIGNAL_HANDLER
if(!(gone.loc in locs))
remove_item_from_transport(gone)
/obj/structure/transport/linear/proc/remove_item_from_transport(atom/movable/potential_rider)
SIGNAL_HANDLER
if(!(potential_rider in transport_contents))
return
if(isliving(potential_rider) && HAS_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED))
REMOVE_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
transport_contents -= potential_rider
changed_gliders -= potential_rider
UnregisterSignal(potential_rider, list(COMSIG_QDELETING, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE))
/obj/structure/transport/linear/proc/add_item_on_transport(datum/source, atom/movable/new_transport_contents)
SIGNAL_HANDLER
var/static/list/blacklisted_types = typecacheof(list(/obj/structure/fluff/tram_rail, /obj/effect/decal/cleanable, /obj/structure/transport/linear, /mob/eye, /obj/effect/gravity_fluff_field))
if(is_type_in_typecache(new_transport_contents, blacklisted_types) || new_transport_contents.invisibility == INVISIBILITY_ABSTRACT || HAS_TRAIT(new_transport_contents, TRAIT_UNDERFLOOR)) //prevents the tram from stealing things like landmarks
return FALSE
if(new_transport_contents in transport_contents)
return FALSE
if(isliving(new_transport_contents) && !HAS_TRAIT(new_transport_contents, TRAIT_CANNOT_BE_UNBUCKLED))
ADD_TRAIT(new_transport_contents, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
transport_contents += new_transport_contents
RegisterSignal(new_transport_contents, COMSIG_QDELETING, PROC_REF(remove_item_from_transport))
return TRUE
///adds everything on our tile that can be added to our transport_contents and initial_contents lists when we're created
/obj/structure/transport/linear/proc/add_initial_contents()
for(var/turf/turf_loc in locs)
for(var/atom/movable/movable_contents as anything in turf_loc)
if(movable_contents == src)
continue
if(add_item_on_transport(src, movable_contents))
var/datum/weakref/new_initial_contents = WEAKREF(movable_contents)
if(!new_initial_contents)
continue
initial_contents += new_initial_contents
///verify the movables in our list of contents are actually on our loc
/obj/structure/transport/linear/proc/verify_transport_contents()
for(var/atom/movable/movable_contents as anything in transport_contents)
if(!(movable_contents.loc in locs))
remove_item_from_transport(movable_contents)
/obj/structure/transport/linear/proc/check_for_humans()
for(var/atom/movable/movable_contents as anything in transport_contents)
if(ishuman(movable_contents))
return TRUE
return FALSE
///signal handler for COMSIG_MOVABLE_UPDATE_GLIDE_SIZE: when a movable in transport_contents changes its glide_size independently.
///adds that movable to a lazy list, movables in that list have their glide_size updated when the tram next moves
/obj/structure/transport/linear/proc/on_changed_glide_size(atom/movable/moving_contents, new_glide_size)
SIGNAL_HANDLER
if(new_glide_size != glide_size_override)
changed_gliders += moving_contents
///make this tram platform multitile, expanding to cover all the tram platforms adjacent to us and deleting them. makes movement more efficient.
///the platform becoming multitile should be in the lower left corner since thats assumed to be the loc of multitile objects
/obj/structure/transport/linear/proc/create_modular_set(min_x, min_y, max_x, max_y, z)
if(!(min_x && min_y && max_x && max_y && z))
for(var/obj/structure/transport/linear/other_transport as anything in transport_controller_datum.transport_modules)
if(other_transport.z != z)
continue
min_x = min(min_x, other_transport.x)
max_x = max(max_x, other_transport.x)
min_y = min(min_y, other_transport.y)
max_y = max(max_y, other_transport.y)
var/turf/lower_left_corner = locate(min_x, min_y, z)
var/obj/structure/transport/linear/primary_module = locate() in lower_left_corner
if(!primary_module)
stack_trace("no lift in the lower left corner of a lift level!")
return FALSE
if(primary_module != src)
//the loc of a multitile object must always be the lower left corner
return primary_module.create_modular_set()
width = (max_x - min_x) + 1
height = (max_y - min_y) + 1
///list of turfs we dont go over. if for whatever reason we encounter an already multitile lift platform
///we add all of its locs to this list so we dont add that lift platform multiple times as we iterate through its locs
var/list/locs_to_skip = locs.Copy()
bound_width = bound_width * width
bound_height = bound_height * height
//multitile movement code assumes our loc is on the lower left corner of our bounding box
var/first_x = 0
var/first_y = 0
var/last_x = max(max_x - min_x, 0)
var/last_y = max(max_y - min_y, 0)
for(var/y in first_y to last_y)
var/y_pixel_offset = ICON_SIZE_Y * y
for(var/x in first_x to last_x)
var/x_pixel_offset = ICON_SIZE_X * x
var/turf/set_turf = locate(x + min_x, y + min_y, z)
if(!set_turf)
continue
if(set_turf in locs_to_skip)
continue
var/obj/structure/transport/linear/other_transport = locate() in set_turf
if(!other_transport)
continue
locs_to_skip += other_transport.locs.Copy()//make sure we never go over multitile platforms multiple times
other_transport.pixel_x = x_pixel_offset
other_transport.pixel_y = y_pixel_offset
overlays += other_transport
//now we vore all the other lifts connected to us on our z level
for(var/obj/structure/transport/linear/other_transport in transport_controller_datum.transport_modules)
if(other_transport == src || other_transport.z != z)
continue
transport_controller_datum.transport_modules -= other_transport
if(other_transport.transport_contents)
transport_contents |= other_transport.transport_contents
if(other_transport.initial_contents)
initial_contents |= other_transport.initial_contents
qdel(other_transport)
transport_controller_datum.create_modular_set = TRUE
var/turf/old_loc = loc
forceMove(locate(min_x, min_y, z))//move to the lower left corner
set_movement_registrations(locs - old_loc)
blocks_emissive = EMISSIVE_BLOCK_GENERIC
update_appearance()
return TRUE
///returns an unordered list of all lift platforms adjacent to us. used so our transport_controller_datum can control all connected platforms.
///includes platforms directly above or below us as well. only includes platforms with an identical transport_id to our own.
/obj/structure/transport/linear/proc/module_adjacency(datum/transport_controller/transport_controller_datum)
. = list()
for(var/direction in GLOB.cardinals_multiz)
var/obj/structure/transport/linear/neighbor = locate() in get_step_multiz(src, direction)
if(!neighbor || neighbor.transport_id != transport_id)
continue
. += neighbor
///main proc for moving the lift in the direction [travel_direction]. handles horizontal and/or vertical movement for multi platformed lifts and multitile lifts.
/obj/structure/transport/linear/proc/travel(travel_direction)
var/list/things_to_move = transport_contents
var/turf/destination
if(!isturf(travel_direction))
destination = get_step_multiz(src, travel_direction)
else
destination = travel_direction
travel_direction = get_dir_multiz(loc, travel_direction)
var/x_offset = ROUND_UP(bound_width / ICON_SIZE_X) - 1 //how many tiles our horizontally farthest edge is from us
var/y_offset = ROUND_UP(bound_height / ICON_SIZE_Y) - 1 //how many tiles our vertically farthest edge is from us
var/destination_x = destination.x
var/destination_y = destination.y
var/destination_z = destination.z
//the x coordinate of the edge furthest from our future destination, which would be our right hand side
var/back_edge_x = destination_x + x_offset//if we arent multitile this should just be destination.x
var/upper_edge_y = destination_y + y_offset
var/list/dest_locs = block(
destination_x, destination_y, destination_z,
back_edge_x, upper_edge_y, destination_z
)
var/list/entering_locs = dest_locs - locs
var/list/exited_locs = locs - dest_locs
if(travel_direction == DOWN)
for(var/turf/dest_turf as anything in entering_locs)
SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
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]!"))
if(violent_landing)
// Violent landing = gibbed. But the nicest kind of gibbing, keeping everything intact.
crushed.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
crushed.gib(DROP_ALL_REMAINS)
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(travel_direction == 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(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(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
var/atom/throw_target
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(iswallturf(dest_turf))
var/turf/closed/wall/collided_wall = dest_turf
do_sparks(2, FALSE, collided_wall)
collided_wall.dismantle_wall(devastated = TRUE)
for(var/mob/client_mob in SSspatial_grid.orthogonal_range_search(collided_wall, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, 8))
shake_camera(client_mob, duration = 2, strength = 3)
playsound(collided_wall, 'sound/effects/meteorimpact.ogg', 100, TRUE)
if(ismineralturf(dest_turf))
var/turf/closed/mineral/dest_mineral_turf = dest_turf
for(var/mob/client_mob in SSspatial_grid.orthogonal_range_search(dest_mineral_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, 8))
shake_camera(client_mob, duration = 2, strength = 3)
dest_mineral_turf.gets_drilled()
for(var/obj/structure/victim_structure in dest_turf.contents)
if(QDELING(victim_structure))
continue
if(!is_type_in_typecache(victim_structure, transport_controller_datum.ignored_smashthroughs))
if((PLANE_TO_TRUE(victim_structure.plane) == FLOOR_PLANE && victim_structure.layer > TRAM_RAIL_LAYER) || (PLANE_TO_TRUE(victim_structure.plane) == GAME_PLANE && victim_structure.layer > LOW_OBJ_LAYER) )
if(victim_structure.anchored && initial(victim_structure.anchored) == TRUE)
visible_message(span_danger("[src] smashes through [victim_structure]!"))
victim_structure.deconstruct(FALSE)
else
if(!throw_target)
throw_target = get_edge_target_turf(src, turn(travel_direction, pick(45, -45)))
visible_message(span_danger("[src] violently rams [victim_structure] out of the way!"))
victim_structure.anchored = FALSE
victim_structure.take_damage(rand(20, 25) * collision_lethality)
victim_structure.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality)
for(var/obj/machinery/victim_machine in dest_turf.contents)
if(QDELING(victim_machine))
continue
if(is_type_in_typecache(victim_machine, transport_controller_datum.ignored_smashthroughs))
continue
if(istype(victim_machine, /obj/machinery/field)) //graceful break handles this scenario
continue
if(victim_machine.layer >= LOW_OBJ_LAYER) //avoids stuff that is probably flush with the ground
playsound(src, 'sound/effects/bang.ogg', 50, TRUE)
visible_message(span_danger("[src] smashes through [victim_machine]!"))
qdel(victim_machine)
for(var/mob/living/victim_living in dest_turf.contents)
var/damage_multiplier = victim_living.maxHealth * 0.01
var/extra_ouch = FALSE // if emagged you're gonna have a really bad time
if(internal_movement_delay <= 1) // slow trams don't cause extra damage
for(var/obj/structure/tram/spoiler/my_spoiler in transport_contents)
if(istype(victim_living.buckled, /obj/structure/fluff/tram_rail))
extra_ouch = TRUE
break
if(get_dist(my_spoiler, victim_living) != 1)
continue
if(my_spoiler.deployed)
extra_ouch = TRUE
break
if(transport_controller_datum.ignored_smashthroughs[victim_living.type])
continue
to_chat(victim_living, span_userdanger("[src] collides into you!"))
SEND_SIGNAL(victim_living, COMSIG_LIVING_HIT_BY_TRAM, src)
playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
var/damage = 0
log_combat(src, victim_living, "collided with")
if(prob(15)) //sorry buddy, luck wasn't on your side
damage = 29 * collision_lethality * damage_multiplier
else
damage = rand(7, 21) * collision_lethality * damage_multiplier
victim_living.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, wound_bonus = 7)
victim_living.apply_damage(3 * damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = 21)
victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, wound_bonus = 14)
victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, wound_bonus = 14)
victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, wound_bonus = 14)
victim_living.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, wound_bonus = 14)
if (extra_ouch)
playsound(src, 'sound/effects/grillehit.ogg', 50, TRUE)
var/obj/item/bodypart/head/head = victim_living.get_bodypart("head")
if(head)
log_combat(src, victim_living, "beheaded")
head.dismember()
victim_living.regenerate_icons()
add_overlay(mutable_appearance(icon, "blood_overlay"))
register_collision(points = 3)
if(QDELETED(victim_living)) //in case it was a mob that dels on death
continue
if(!throw_target)
throw_target = get_edge_target_turf(src, turn(travel_direction, pick(45, -45)))
var/turf/turf_to_bloody = get_turf(victim_living)
turf_to_bloody.add_mob_blood(victim_living)
victim_living.throw_at()
//if travel_direction EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away
var/datum/callback/land_slam = new(victim_living, TYPE_PROC_REF(/mob/living/, tram_slam_land))
victim_living.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality, callback = land_slam)
//increment the hit counters
if(ismob(victim_living) && victim_living.client && istype(transport_controller_datum, /datum/transport_controller/linear/tram))
register_collision(points = 1)
unset_movement_registrations(exited_locs)
group_move(things_to_move, travel_direction)
set_movement_registrations(entering_locs)
/obj/structure/transport/linear/proc/register_collision(points = 1)
SSpersistence.tram_hits_this_round += points
SSblackbox.record_feedback("amount", "tram_collision", points)
var/datum/transport_controller/linear/tram/tram_controller = transport_controller_datum
ASSERT(istype(tram_controller))
tram_controller.register_collision(points)
///move the movers list of movables on our tile to destination if we successfully move there first.
///this is like calling forceMove() on everything in movers and ourselves, except nothing in movers
///has destination.Entered() and origin.Exited() called on them, as only our movement can be perceived.
///none of the movers are able to react to the movement of any other mover, saving a lot of needless processing cost
///and is more sensible. without this, if you and a banana are on the same platform, when that platform moves you will slip
///on the banana even if youre not moving relative to it.
/obj/structure/transport/linear/proc/group_move(list/atom/movable/movers, movement_direction)
if(movement_direction == NONE)
stack_trace("a transport was told to move to somewhere it already is!")
return FALSE
var/turf/our_dest = get_step(src, movement_direction)
//vars updated per mover
var/turf/mover_old_loc
var/turf/mover_old_area
var/turf/mover_new_loc
var/turf/mover_new_area
if(glide_size != glide_size_override)
set_glide_size(glide_size_override)
forceMove(our_dest)
if(loc != our_dest || QDELETED(src))//check if our movement succeeded, if it didnt then the movers cant be moved
return FALSE
for(var/atom/movable/mover as anything in changed_gliders)
if(QDELETED(mover))
movers -= mover
continue
if(mover.glide_size != glide_size_override)
mover.set_glide_size(glide_size_override)
changed_gliders.Cut()
for(var/atom/movable/mover as anything in movers)
if(QDELETED(mover))
movers -= mover
continue
//another O(n) set of read operations, not ideal given datum var read times.
//ideally we would only need to do this check once per tile with contents (which is constant per tram, while contents can scale infinitely)
//and then the only O(n) process is calling these procs for each contents that actually changes areas
//but that approach is probably a lot buggier. itd be nice to have it figured out though
mover_old_loc = mover.loc
mover_old_area = mover_old_loc.loc
mover.loc = (mover_new_loc = get_step(mover, movement_direction))
mover_new_area = mover_new_loc.loc
if(mover_old_area != mover_new_area)
mover_old_area.Exited(mover, movement_direction)
mover_new_area.Entered(mover, mover_new_area)
mover.Moved(mover_old_loc, movement_direction, TRUE, null, FALSE)
return TRUE
/**
* reset the contents of this lift platform to its original state in case someone put too much shit on it.
* everything that is considered foreign is deleted, you can configure what is considered foreign.
*
* used by an admin via calling reset_lift_contents() on our transport_controller_datum.
*
* Arguments:
* * consider_anything_past - number. if > 0 this platform will only handle foreign contents that exceed this number on each of our locs
* * foreign_objects - bool. if true this platform will consider /atom/movable's that arent mobs as part of foreign contents
* * foreign_non_player_mobs - bool. if true we consider mobs that dont have a mind to be foreign
* * consider_player_mobs - bool. if true we consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
*/
/obj/structure/transport/linear/proc/reset_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
if(!foreign_objects && !foreign_non_player_mobs && !consider_player_mobs)
return FALSE
consider_anything_past = isnum(consider_anything_past) ? max(consider_anything_past, 0) : 0
//just in case someone fucks up the arguments
if(consider_anything_past && length(transport_contents) <= consider_anything_past)
return FALSE
///list of resolve()'d initial_contents that are still in transport_contents
var/list/atom/movable/original_contents = list(src)
///list of objects we consider foreign according to the given arguments
var/list/atom/movable/foreign_contents = list()
for(var/datum/weakref/initial_contents_ref as anything in initial_contents)
if(!initial_contents_ref)
continue
var/atom/movable/resolved_contents = initial_contents_ref.resolve()
if(!resolved_contents)
continue
if(!(resolved_contents in transport_contents))
continue
original_contents += resolved_contents
for(var/turf/turf_loc as anything in locs)
var/list/atom/movable/foreign_contents_in_loc = list()
for(var/atom/movable/foreign_movable as anything in (turf_loc.contents - original_contents))
if(foreign_objects && ismovable(foreign_movable) && !ismob(foreign_movable) && !istype(foreign_movable, /obj/effect/landmark/transport/nav_beacon))
foreign_contents_in_loc += foreign_movable
continue
if(foreign_non_player_mobs && ismob(foreign_movable))
var/mob/foreign_mob = foreign_movable
if(consider_player_mobs || !foreign_mob.mind)
foreign_contents_in_loc += foreign_mob
continue
if(consider_anything_past)
foreign_contents_in_loc.len -= consider_anything_past
//hey cool this works, neat. this takes from the opposite side of the list that youd expect but its easy so idc
//also this means that if you use consider_anything_past then foreign mobs are less likely to be deleted than foreign objects
//because internally the contents list is 2 linked lists of obj contents - mob contents, thus mobs are always last in the order
//when you iterate it.
foreign_contents += foreign_contents_in_loc
for(var/atom/movable/contents_to_delete as anything in foreign_contents)
qdel(contents_to_delete)
return TRUE
/// Callback / general proc to check if the lift is usable by the passed mob.
/obj/structure/transport/linear/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/transport/linear/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/transport/linear/other_platform as anything in transport_controller_datum.transport_modules)
if(REF(user) in other_platform.current_operators)
return
var/list/possible_directions = list()
if(transport_controller_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(transport_controller_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
LAZYADD(current_operators, REF(user))
var/result = show_radial_menu(
user = user,
anchor = src,
choices = possible_directions,
custom_check = CALLBACK(src, PROC_REF(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" && transport_controller_datum.controller_status & 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(!transport_controller_datum.simple_move_wrapper(UP, elevator_vertical_speed, user))
return
show_fluff_message(UP, user)
open_lift_radial(user)
if("Down")
if(!transport_controller_datum.simple_move_wrapper(DOWN, elevator_vertical_speed, user))
return
show_fluff_message(DOWN, user)
open_lift_radial(user)
if("Cancel")
return
/**
* Proc to ensure that the radial menu closes when it should.
* Arguments:
* * user - The person that opened the menu.
* * starting_loc - The location of the lift when the menu was opened, used to prevent the menu from being interacted with after the lift was moved by someone else.
*
* Returns:
* * boolean, FALSE if the menu should be closed, TRUE if the menu is clear to stay opened.
*/
/obj/structure/transport/linear/proc/check_menu(mob/user, starting_loc)
if(user.incapacitated || !user.Adjacent(src) || starting_loc != src.loc)
return FALSE
return TRUE
/obj/structure/transport/linear/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
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/transport/linear/attack_ghost(mob/user)
. = ..()
if(.)
return
if(!radial_travel)
return
if(!isAdminGhostAI(user))
return
return open_lift_radial(user)
/obj/structure/transport/linear/attack_paw(mob/user, list/modifiers)
if(!radial_travel)
return ..()
return open_lift_radial(user)
/obj/structure/transport/linear/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers)
if(!radial_travel)
return ..()
return open_lift_radial(user)
/obj/structure/transport/linear/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:
* * direction - What direction are we going
* * user - The mob that caused the lift to move, for the visible message.
*/
/obj/structure/transport/linear/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."))
if(direction == DOWN)
user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards."))
/obj/machinery/door/poddoor/lift
name = "elevator door"
desc = "Keeps idiots like you from walking into an open elevator shaft."
icon = 'icons/obj/doors/liftdoor.dmi'
opacity = FALSE
glass = TRUE
/obj/machinery/door/poddoor/lift/Initialize(mapload)
if(!isnull(transport_linked_id)) //linter and stuff
elevator_mode = TRUE
return ..()
/obj/machinery/door/poddoor/lift/preopen
icon_state = "open"
density = FALSE
opacity = FALSE
// A subtype intended for "public use"
/obj/structure/transport/linear/public
icon = 'icons/turf/floors.dmi'
icon_state = "rockvault"
base_icon_state = null
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
warns_on_down_movement = TRUE
violent_landing = FALSE
elevator_vertical_speed = 3 SECONDS
radial_travel = FALSE
/obj/structure/transport/linear/debug
name = "transport platform"
desc = "A lightweight platform. It moves in any direction, except up and down."
color = "#5286b9ff"
transport_id = TRANSPORT_TYPE_DEBUG
radial_travel = TRUE
/obj/structure/transport/linear/debug/open_lift_radial(mob/living/user)
var/starting_position = loc
if (!can_open_lift_radial(user,starting_position))
return
//NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST
var/static/list/tool_list = list(
"NORTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
"NORTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
"EAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
"SOUTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
"SOUTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
"SOUTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
"WEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST),
"NORTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST)
)
var/result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, PROC_REF(can_open_lift_radial), user, starting_position), require_near = TRUE, tooltips = FALSE)
if (!can_open_lift_radial(user,starting_position))
return // nice try
if(!isnull(result) && result != "Cancel" && transport_controller_datum.controller_status & CONTROLS_LOCKED)
// Only show this message if they actually wanted to move
balloon_alert(user, "elevator controls locked!")
return
switch(result)
if("NORTH")
transport_controller_datum.move_transport_horizontally(NORTH)
open_lift_radial(user)
if("NORTHEAST")
transport_controller_datum.move_transport_horizontally(NORTHEAST)
open_lift_radial(user)
if("EAST")
transport_controller_datum.move_transport_horizontally(EAST)
open_lift_radial(user)
if("SOUTHEAST")
transport_controller_datum.move_transport_horizontally(SOUTHEAST)
open_lift_radial(user)
if("SOUTH")
transport_controller_datum.move_transport_horizontally(SOUTH)
open_lift_radial(user)
if("SOUTHWEST")
transport_controller_datum.move_transport_horizontally(SOUTHWEST)
open_lift_radial(user)
if("WEST")
transport_controller_datum.move_transport_horizontally(WEST)
open_lift_radial(user)
if("NORTHWEST")
transport_controller_datum.move_transport_horizontally(NORTHWEST)
open_lift_radial(user)
if("Cancel")
return
add_fingerprint(user)
/obj/structure/transport/linear/tram
name = "tram subfloor"
desc = "The subfloor lattice of the tram. You can build a tram wall frame by using <b>titanium sheets,</b> or place down <b>thermoplastic tram floor tiles.</b>"
icon = 'icons/obj/tram/tram_structure.dmi'
icon_state = "subfloor"
base_icon_state = null
density = FALSE
layer = TRAM_STRUCTURE_LAYER
smoothing_flags = NONE
smoothing_groups = null
canSmoothWith = null
//the modular structure is pain to work with, damage is done to the floor on top
transport_id = TRANSPORT_TYPE_TRAM
transport_controller_type = /datum/transport_controller/linear/tram
radial_travel = FALSE
/// Set by the tram control console in late initialize
var/travelling = FALSE
/// Do we want this transport to link with nearby modules to make a multi-tile platform
create_modular_set = TRUE
/obj/structure/transport/linear/tram/corner/northwest
icon_state = "subfloor-corner-nw"
/obj/structure/transport/linear/tram/corner/southwest
icon_state = "subfloor-corner-sw"
/obj/structure/transport/linear/tram/corner/northeast
icon_state = "subfloor-corner-ne"
/obj/structure/transport/linear/tram/corner/southeast
icon_state = "subfloor-corner-se"
/obj/structure/transport/linear/tram/add_item_on_transport(datum/source, atom/movable/item)
. = ..()
if(travelling)
on_changed_glide_size(item, item.glide_size)
/obj/structure/transport/linear/tram/proc/set_travelling(travelling)
if (src.travelling == travelling)
return
for(var/atom/movable/glider as anything in transport_contents)
if(travelling)
glider.set_glide_size(glide_size_override)
RegisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_changed_glide_size))
else
changed_gliders -= glider
UnregisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)
src.travelling = travelling
/obj/structure/transport/linear/tram/set_currently_z_moving()
return FALSE //trams can never z fall and shouldnt waste any processing time trying to do so
/**
* Handles unlocking the tram controls for use after moving
*
* More safety checks to make sure the tram has actually docked properly
* at a location before users are allowed to interact with the tram console again.
* Tram finds its location at this point before fully unlocking controls to the user.
*/
/obj/structure/transport/linear/tram/proc/unlock_controls()
for(var/obj/structure/transport/linear/tram/tram_part as anything in transport_controller_datum.transport_modules) //only thing everyone needs to know is the new location.
tram_part.set_travelling(FALSE)
transport_controller_datum.controls_lock(FALSE)
///debug proc to highlight the locs of the tram platform
/obj/structure/transport/linear/tram/proc/find_dimensions(iterations = 100)
message_admins("num turfs: [length(locs)]")
var/overlay = /obj/effect/overlay/ai_detect_hud
var/list/turfs = list()
for(var/turf/our_turf as anything in locs)
new overlay(our_turf)
turfs += our_turf
addtimer(CALLBACK(src, PROC_REF(clear_turfs), turfs, iterations), 0.1 SECONDS)
/obj/structure/transport/linear/tram/proc/clear_turfs(list/turfs_to_clear, iterations)
for(var/turf/our_old_turf as anything in turfs_to_clear)
var/obj/effect/overlay/ai_detect_hud/hud = locate() in our_old_turf
if(hud)
qdel(hud)
var/overlay = /obj/effect/overlay/ai_detect_hud
for(var/turf/our_turf as anything in locs)
new overlay(our_turf)
iterations--
var/list/turfs = list()
for(var/turf/our_turf as anything in locs)
turfs += our_turf
if(iterations)
addtimer(CALLBACK(src, PROC_REF(clear_turfs), turfs, iterations), 0.1 SECONDS)
/obj/structure/transport/linear/tram/proc/estop_throw(throw_direction)
for(var/mob/living/passenger in transport_contents)
var/mob_throw_chance = transport_controller_datum.throw_chance
if(prob(mob_throw_chance || 17.5) || HAS_TRAIT(passenger, TRAIT_CURSED)) // sometimes you go through a window, especially with bad luck
passenger.AddElement(/datum/element/window_smashing, duration = 1.5 SECONDS)
var/throw_target = get_edge_target_turf(src, throw_direction)
passenger.throw_at(throw_target, 30, 7, force = MOVE_FORCE_OVERPOWERING)
/obj/structure/transport/linear/tram/slow
transport_controller_type = /datum/transport_controller/linear/tram/slow