From 18a56ffeeb4073a794cf22f2ab72366757fa2037 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sat, 25 Jun 2022 02:24:00 +0200 Subject: [PATCH] [MIRROR] (code bounty) The tram is now unstoppably powerful. it cannot be stopped, it cannot be slowed, it cannot be reasoned with. YOU HAVE NO IDEA HOW READY YOU ARE [MDB IGNORE] (#14477) * (code bounty) The tram is now unstoppably powerful. it cannot be stopped, it cannot be slowed, it cannot be reasoned with. YOU HAVE NO IDEA HOW READY YOU ARE * fex * fex Co-authored-by: Kylerace Co-authored-by: Gandalf <9026500+Gandalf2k15@users.noreply.github.com> --- .../SpaceRuins/hilbertresearchfacility.dmm | 105 ++- .../map_files/IceBoxStation/IceBoxStation.dmm | 136 +++- _maps/map_files/tramstation/tramstation.dmm | 68 +- .../signals/signals_atom/signals_atom_main.dm | 4 + code/__DEFINES/dcs/signals/signals_object.dm | 3 + .../__DEFINES/important_recursive_contents.dm | 2 + code/__DEFINES/industrial_lift.dm | 20 + code/__DEFINES/obj_flags.dm | 1 - code/__DEFINES/traits.dm | 5 + code/__HELPERS/icons.dm | 16 +- code/_globalvars/bitfields.dm | 1 - code/_onclick/click.dm | 4 +- code/controllers/subsystem/explosions.dm | 10 +- code/controllers/subsystem/mapping.dm | 52 +- .../subsystem/processing/tramprocess.dm | 12 +- code/controllers/subsystem/spatial_gridmap.dm | 100 ++- code/datums/components/bloodysoles.dm | 42 +- code/datums/components/drift.dm | 3 +- code/datums/components/overlay_lighting.dm | 9 +- code/datums/components/storage/storage.dm | 70 +- code/datums/components/wet_floor.dm | 3 +- code/datums/elements/_element.dm | 2 +- code/datums/elements/atmos_sensitive.dm | 1 + code/datums/elements/chewable.dm | 1 + code/datums/elements/footstep.dm | 32 +- code/datums/elements/forced_gravity.dm | 14 +- code/datums/elements/frozen.dm | 47 ++ code/datums/elements/light_eater.dm | 3 +- code/datums/elements/obj_regen.dm | 1 + code/game/area/areas.dm | 28 +- code/game/atoms.dm | 51 +- code/game/atoms_movable.dm | 116 ++- code/game/machinery/dance_machine.dm | 2 +- code/game/objects/objs.dm | 7 - .../objects/structures/industrial_lift.dm | 620 --------------- code/game/sound.dm | 141 ++-- code/game/turfs/open/_open.dm | 13 +- code/game/turfs/open/openspace.dm | 3 + code/game/turfs/open/space/space.dm | 2 + code/game/turfs/turf.dm | 12 +- code/modules/assembly/doorcontrol.dm | 68 +- .../atmospherics/environmental/LINDA_fire.dm | 1 + .../atmospherics/gasmixtures/gas_mixture.dm | 5 +- .../industrial_lift}/crossing_signal.dm | 28 +- .../industrial_lift/industrial_lift.dm | 752 ++++++++++++++++++ code/modules/industrial_lift/lift_master.dm | 397 +++++++++ .../industrial_lift}/tram_controls.dm | 52 +- code/modules/industrial_lift/tram_landmark.dm | 47 ++ .../industrial_lift/tram_lift_master.dm | 174 ++++ .../industrial_lift/tram_override_objects.dm | 39 + .../industrial_lift}/tram_walls.dm | 2 +- code/modules/instruments/songs/play_legacy.dm | 2 +- code/modules/lighting/lighting_atom.dm | 11 +- code/modules/lighting/lighting_source.dm | 43 +- .../ruins/lavalandruin_code/biodome_winter.dm | 5 +- .../ruins/objects_and_mobs/necropolis_gate.dm | 2 +- .../ruins/spaceruin_code/hilbertshotel.dm | 9 +- .../mapping/space_management/traits.dm | 11 +- .../space_management/zlevel_manager.dm | 2 + code/modules/meteors/meteors.dm | 2 +- code/modules/mob/living/carbon/human/human.dm | 12 +- code/modules/mob/living/living.dm | 3 +- code/modules/mob/living/living_defines.dm | 2 + code/modules/mob/living/living_fov.dm | 29 +- code/modules/mob/living/living_movement.dm | 7 +- code/modules/mob/mob.dm | 1 + code/modules/mob/mob_defines.dm | 3 + code/modules/mob/mob_movement.dm | 23 +- code/modules/mod/modules/modules_maint.dm | 2 +- code/modules/power/gravitygenerator.dm | 9 +- code/modules/shuttle/on_move.dm | 2 +- code/modules/shuttle/shuttle.dm | 8 +- code/modules/unit_tests/create_and_destroy.dm | 2 + icons/effects/landmarks_static.dmi | Bin 20347 -> 20455 bytes .../modules/jukebox/code/jukebox_subsystem.dm | 4 +- .../code/liquid_systems/liquid_interaction.dm | 1 + tgstation.dme | 14 +- 77 files changed, 2350 insertions(+), 1186 deletions(-) create mode 100644 code/__DEFINES/industrial_lift.dm create mode 100644 code/datums/elements/frozen.dm delete mode 100644 code/game/objects/structures/industrial_lift.dm rename code/{game/machinery => modules/industrial_lift}/crossing_signal.dm (88%) create mode 100644 code/modules/industrial_lift/industrial_lift.dm create mode 100644 code/modules/industrial_lift/lift_master.dm rename code/{game/machinery/computer => modules/industrial_lift}/tram_controls.dm (79%) create mode 100644 code/modules/industrial_lift/tram_landmark.dm create mode 100644 code/modules/industrial_lift/tram_lift_master.dm create mode 100644 code/modules/industrial_lift/tram_override_objects.dm rename code/{game/objects/structures => modules/industrial_lift}/tram_walls.dm (99%) diff --git a/_maps/RandomRuins/SpaceRuins/hilbertresearchfacility.dmm b/_maps/RandomRuins/SpaceRuins/hilbertresearchfacility.dmm index 9b0765f7f15..14b9fb6f249 100644 --- a/_maps/RandomRuins/SpaceRuins/hilbertresearchfacility.dmm +++ b/_maps/RandomRuins/SpaceRuins/hilbertresearchfacility.dmm @@ -324,13 +324,12 @@ /obj/structure/fluff/tram_rail{ dir = 1 }, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_white"; - initial_id = "middle_part_hilbert" - }, /obj/machinery/door/window/survival_pod{ dir = 1 }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_white" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "hQ" = ( @@ -467,13 +466,13 @@ /turf/open/floor/glass, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "kK" = ( -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_white"; - initial_id = "middle_part_hilbert" - }, /obj/structure/window/reinforced/survival_pod{ dir = 4 }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_white" + }, +/obj/effect/landmark/lift_id/hilbert, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "kO" = ( @@ -664,20 +663,18 @@ /area/ruin/space/has_grav/powered/hilbertresearchfacility) "pE" = ( /obj/effect/landmark/tram/middle_part/hilbert, -/obj/structure/industrial_lift/tram/central{ - icon_state = "titanium_white"; - initial_id = "middle_part_hilbert"; - tram_id = "tram_hilbert" - }, -/obj/machinery/computer/tram_controls{ - dir = 8; - tram_id = "tram_hilbert" - }, /obj/machinery/light/floor{ brightness = 2; bulb_colour = "#deefff"; bulb_power = 0.6 }, +/obj/machinery/computer/tram_controls{ + dir = 8; + specific_lift_id = "tram_hilbert" + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_white" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "pI" = ( @@ -1652,13 +1649,12 @@ /turf/open/floor/mineral/titanium/tiled/purple, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "Kv" = ( -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_white"; - initial_id = "middle_part_hilbert" - }, /obj/structure/window/reinforced/survival_pod{ dir = 8 }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_white" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "KC" = ( @@ -1666,27 +1662,25 @@ /area/ruin/space/has_grav/powered/hilbertresearchfacility) "KF" = ( /obj/structure/fluff/tram_rail, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_purple"; - initial_id = "middle_part_hilbert" - }, -/obj/structure/shuttle/engine/propulsion/in_wall{ - dir = 8; - pixel_x = -32 - }, /obj/structure/window/reinforced/survival_pod{ dir = 8 }, /obj/structure/window/reinforced/survival_pod, +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ + dir = 8; + pixel_x = -32 + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_purple" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "KI" = ( /obj/structure/fluff/tram_rail, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_white"; - initial_id = "middle_part_hilbert" - }, /obj/machinery/door/window/survival_pod, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_white" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "KJ" = ( @@ -1695,18 +1689,17 @@ /area/ruin/unpowered/no_grav) "KK" = ( /obj/structure/fluff/tram_rail, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_purple"; - initial_id = "middle_part_hilbert" - }, -/obj/structure/shuttle/engine/propulsion/in_wall{ - dir = 4; - pixel_x = 32 - }, /obj/structure/window/reinforced/survival_pod{ dir = 4 }, /obj/structure/window/reinforced/survival_pod, +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ + dir = 4; + pixel_x = 32 + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_purple" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "KL" = ( @@ -1787,20 +1780,19 @@ /obj/structure/fluff/tram_rail{ dir = 1 }, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_purple"; - initial_id = "middle_part_hilbert" - }, -/obj/structure/shuttle/engine/propulsion/in_wall{ - dir = 8; - pixel_x = -32 - }, /obj/structure/window/reinforced/survival_pod{ dir = 8 }, /obj/structure/window/reinforced/survival_pod{ dir = 1 }, +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ + dir = 8; + pixel_x = -32 + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_purple" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "MX" = ( @@ -1994,20 +1986,19 @@ /obj/structure/fluff/tram_rail{ dir = 1 }, -/obj/structure/industrial_lift/tram{ - icon_state = "titanium_purple"; - initial_id = "middle_part_hilbert" - }, -/obj/structure/shuttle/engine/propulsion/in_wall{ - dir = 4; - pixel_x = 32 - }, /obj/structure/window/reinforced/survival_pod{ dir = 4 }, /obj/structure/window/reinforced/survival_pod{ dir = 1 }, +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ + dir = 4; + pixel_x = 32 + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium_purple" + }, /turf/open/floor/engine, /area/ruin/space/has_grav/powered/hilbertresearchfacility) "RE" = ( diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index 578f9119595..31ef1f01aeb 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -2890,6 +2890,18 @@ /obj/item/circuitboard/machine/monkey_recycler, /turf/open/floor/plating, /area/station/maintenance/port/fore) +"aYe" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/structure/table/reinforced, +/obj/item/hfr_box/body/fuel_input, +/obj/item/hfr_box/body/interface{ + pixel_x = 6; + pixel_y = 9 + }, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "aYm" = ( /obj/structure/cable, /obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2, @@ -3261,6 +3273,22 @@ /obj/machinery/light/directional/north, /turf/open/floor/iron/dark, /area/station/security/execution/education) +"bdH" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/structure/table/reinforced, +/obj/item/stack/sheet/glass/fifty{ + pixel_x = 2; + pixel_y = 0 + }, +/obj/item/stack/sheet/iron/fifty, +/obj/item/stack/cable_coil{ + pixel_x = -3; + pixel_y = 3 + }, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "bdK" = ( /turf/open/floor/circuit, /area/station/ai_monitored/turret_protected/ai_upload) @@ -3273,10 +3301,11 @@ /turf/open/floor/plating/icemoon, /area/station/engineering/atmos) "bed" = ( -/obj/structure/industrial_lift{ - id = "publicElevator" - }, /obj/machinery/light/floor, +/obj/structure/industrial_lift, +/obj/effect/landmark/lift_id{ + specific_lift_id = "publicElevator" + }, /turf/open/openspace, /area/station/commons/storage/mining) "ben" = ( @@ -9830,6 +9859,24 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/cyan/visible, /turf/closed/wall/r_wall, /area/station/engineering/supermatter) +"dfs" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/machinery/door/poddoor/shutters/preopen{ + dir = 8; + id = "Atmospherics HFR Shutters"; + name = "Atmospherics HFR Shutters" + }, +/obj/effect/turf_decal/trimline/yellow/warning{ + dir = 4 + }, +/obj/effect/turf_decal/stripes/line{ + dir = 8 + }, +/obj/machinery/door/firedoor/heavy, +/turf/closed/mineral/random/snow, +/area/icemoon/underground/explored) "dfz" = ( /obj/structure/closet/secure_closet/security/sec, /obj/structure/cable, @@ -13780,6 +13827,24 @@ /obj/structure/flora/bush/lavendergrass/style_random, /turf/open/floor/grass, /area/station/maintenance/starboard/aft) +"ete" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/machinery/atmospherics/components/binary/pump/off{ + dir = 1; + name = "Infiltrate to Exfiltrate" + }, +/obj/effect/turf_decal/tile/neutral/fourcorners, +/obj/effect/turf_decal/loading_area{ + color = "#439C1E"; + dir = 1 + }, +/obj/effect/turf_decal/trimline/dark_red/warning{ + dir = 1 + }, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "etw" = ( /obj/effect/turf_decal/stripes/white/line, /obj/effect/decal/cleanable/dirt, @@ -27984,6 +28049,24 @@ }, /turf/open/floor/iron/white/smooth_large, /area/station/medical/chemistry) +"iXW" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/machinery/door/poddoor/shutters/preopen{ + dir = 8; + id = "Atmospherics HFR Shutters"; + name = "Atmospherics HFR Shutters" + }, +/obj/effect/turf_decal/trimline/yellow/warning{ + dir = 4 + }, +/obj/effect/turf_decal/stripes/line{ + dir = 8 + }, +/obj/machinery/door/firedoor/heavy, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "iYb" = ( /turf/closed/wall, /area/station/maintenance/central/greater) @@ -51007,6 +51090,17 @@ /obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2, /turf/open/floor/iron, /area/station/commons/fitness) +"qnH" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/effect/turf_decal/tile/neutral/fourcorners, +/obj/machinery/atmospherics/components/unary/portables_connector/visible{ + dir = 8; + name = "Exfiltrate Port" + }, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "qnU" = ( /obj/structure/extinguisher_cabinet/directional/south, /obj/effect/turf_decal/tile/blue{ @@ -66956,6 +67050,22 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2, /turf/open/floor/iron/smooth, /area/station/engineering/lobby) +"vxk" = ( +/obj{ + name = "---Merge conflict marker---" + }, +/obj/structure/table/reinforced, +/obj/item/hfr_box/body/waste_output{ + pixel_x = 5; + pixel_y = -1 + }, +/obj/item/hfr_box/body/moderator_input{ + pixel_x = -5; + pixel_y = 8 + }, +/obj/item/radio/intercom/directional/west, +/turf/open/genturf, +/area/icemoon/underground/unexplored/rivers) "vxx" = ( /obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2, /obj/effect/turf_decal/stripes/line{ @@ -177815,12 +177925,12 @@ nxM xMq tjo tjo +aYe +vxk tjo tjo tjo -tjo -tjo -tjo +bdH tjo tjo tjo @@ -179871,13 +179981,13 @@ qck ilN iDt iDt -xMq -tjo -tjo -tjo -tjo +dfs +iXW +iXW tjo tjo +iXW +iXW tjo tjo tjo @@ -181671,7 +181781,7 @@ gjq nfG iDt tjo -tjo +qnH tjo tjo tjo @@ -182186,7 +182296,7 @@ alM xMq xMq xMq -tjo +ete tjo tjo tjo diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm index 574a0da14e3..026682942b5 100644 --- a/_maps/map_files/tramstation/tramstation.dmm +++ b/_maps/map_files/tramstation/tramstation.dmm @@ -176,9 +176,7 @@ /obj/structure/industrial_lift/tram{ icon_state = "titanium_white" }, -/obj/machinery/door/window/right/directional/south{ - id_tag = "tramdoor" - }, +/obj/machinery/door/window/right/tram, /turf/open/openspace, /area/station/hallway/primary/tram/center) "adM" = ( @@ -2325,8 +2323,8 @@ /obj/structure/industrial_lift/tram{ icon_state = "plating" }, -/obj/structure/window/reinforced/shuttle, -/obj/structure/grille, +/obj/structure/grille/tram, +/obj/structure/window/reinforced/shuttle/tram, /turf/open/openspace, /area/station/hallway/primary/tram/center) "aZX" = ( @@ -22762,8 +22760,6 @@ /obj/structure/industrial_lift/tram{ icon_state = "plating" }, -/obj/structure/window/reinforced/shuttle, -/obj/structure/grille, /obj/effect/turf_decal/stripes{ dir = 1 }, @@ -22771,6 +22767,8 @@ /obj/effect/turf_decal/caution/stand_clear/red{ dir = 4 }, +/obj/structure/grille/tram, +/obj/structure/window/reinforced/shuttle/tram, /turf/open/floor/vault, /area/station/hallway/primary/tram/center) "hOh" = ( @@ -25395,11 +25393,11 @@ /obj/structure/industrial_lift/tram{ icon_state = "plating" }, -/obj/structure/window/reinforced/shuttle, -/obj/structure/grille, /obj/structure/fluff/tram_rail{ dir = 1 }, +/obj/structure/grille/tram, +/obj/structure/window/reinforced/shuttle/tram, /turf/open/openspace, /area/station/hallway/primary/tram/center) "iHr" = ( @@ -27210,7 +27208,7 @@ icon_state = "plating" }, /obj/structure/tramwall/titanium, -/obj/structure/shuttle/engine/propulsion/in_wall{ +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ dir = 4; pixel_x = 32 }, @@ -31992,7 +31990,7 @@ icon_state = "plating" }, /obj/structure/tramwall/titanium, -/obj/structure/shuttle/engine/propulsion/in_wall{ +/obj/structure/shuttle/engine/propulsion/in_wall/tram{ dir = 8; pixel_x = -32 }, @@ -35020,9 +35018,7 @@ /obj/structure/industrial_lift/tram{ icon_state = "titanium_white" }, -/obj/machinery/door/window/left/directional/south{ - id_tag = "tramdoor" - }, +/obj/machinery/door/window/left/tram, /turf/open/openspace, /area/station/hallway/primary/tram/center) "lKo" = ( @@ -50120,6 +50116,11 @@ /obj/structure/cable, /turf/open/floor/iron, /area/station/hallway/secondary/service) +"qMs" = ( +/obj/effect/decal/cleanable/dirt, +/obj/structure/lattice/catwalk, +/turf/open/floor/plating, +/area/station/maintenance/tram/mid) "qMx" = ( /obj/machinery/door/airlock/external{ name = "Port Docking Bay 3" @@ -50161,10 +50162,10 @@ dir = 1 }, /obj/effect/landmark/tram/middle_part, -/obj/structure/industrial_lift/tram/central{ +/obj/machinery/computer/tram_controls, +/obj/structure/industrial_lift/tram{ icon_state = "titanium" }, -/obj/machinery/computer/tram_controls, /turf/open/floor/vault, /area/station/hallway/primary/tram/center) "qNd" = ( @@ -57516,9 +57517,9 @@ /obj/structure/industrial_lift/tram{ icon_state = "plating" }, -/obj/structure/window/reinforced/shuttle, -/obj/structure/grille, /obj/structure/fluff/tram_rail, +/obj/structure/grille/tram, +/obj/structure/window/reinforced/shuttle/tram, /turf/open/openspace, /area/station/hallway/primary/tram/center) "trr" = ( @@ -60943,9 +60944,7 @@ /obj/structure/industrial_lift/tram{ icon_state = "titanium_white" }, -/obj/machinery/door/window/left/directional/north{ - id_tag = "tramdoor" - }, +/obj/machinery/door/window/left/tram/directional/north, /turf/open/openspace, /area/station/hallway/primary/tram/center) "uyA" = ( @@ -61085,6 +61084,17 @@ /obj/effect/turf_decal/trimline/red/filled/line, /turf/open/floor/iron, /area/station/security/prison) +"uBC" = ( +/obj/effect/turf_decal/stripes, +/obj/effect/turf_decal/stripes{ + dir = 1 + }, +/obj/structure/industrial_lift/tram{ + icon_state = "titanium" + }, +/obj/effect/landmark/lift_id, +/turf/open/floor/vault, +/area/station/hallway/primary/tram/center) "uBH" = ( /obj/effect/turf_decal/trimline/red/filled/line{ dir = 4 @@ -62327,8 +62337,6 @@ /obj/structure/industrial_lift/tram{ icon_state = "plating" }, -/obj/structure/window/reinforced/shuttle, -/obj/structure/grille, /obj/effect/turf_decal/stripes{ dir = 1 }, @@ -62336,6 +62344,8 @@ /obj/effect/turf_decal/caution/stand_clear/red{ dir = 8 }, +/obj/structure/grille/tram, +/obj/structure/window/reinforced/shuttle/tram, /turf/open/floor/vault, /area/station/hallway/primary/tram/center) "uXK" = ( @@ -65633,9 +65643,7 @@ /obj/structure/industrial_lift/tram{ icon_state = "titanium_white" }, -/obj/machinery/door/window/right/directional/north{ - id_tag = "tramdoor" - }, +/obj/machinery/door/window/right/tram/directional/north, /turf/open/openspace, /area/station/hallway/primary/tram/center) "wgJ" = ( @@ -103706,9 +103714,9 @@ rKD cMT iui mgX -mgX -mgX -mgX +qMs +qMs +qMs mgX rCM pcu @@ -168986,7 +168994,7 @@ dhw vSM wgz rPU -sOY +uBC biX adD vIq diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm index 653d4e6167b..6d946d09f88 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm @@ -45,6 +45,8 @@ #define COMSIG_ATOM_SMOOTHED_ICON "atom_smoothed_icon" ///from base of atom/Entered(): (atom/movable/arrived, atom/old_loc, list/atom/old_locs) #define COMSIG_ATOM_ENTERED "atom_entered" +///from base of atom/movable/Moved(): (atom/movable/arrived, atom/old_loc, list/atom/old_locs) +#define COMSIG_ATOM_ABSTRACT_ENTERED "atom_abstract_entered" /// Sent from the atom that just Entered src. From base of atom/Entered(): (/atom/destination, atom/old_loc, list/atom/old_locs) #define COMSIG_ATOM_ENTERING "atom_entering" ///from base of atom/Exit(): (/atom/movable/leaving, direction) @@ -52,6 +54,8 @@ #define COMPONENT_ATOM_BLOCK_EXIT (1<<0) ///from base of atom/Exited(): (atom/movable/gone, direction) #define COMSIG_ATOM_EXITED "atom_exited" +///from base of atom/movable/Moved(): (atom/movable/gone, direction) +#define COMSIG_ATOM_ABSTRACT_EXITED "atom_abstract_exited" ///from base of atom/Bumped(): (/atom/movable) #define COMSIG_ATOM_BUMPED "atom_bumped" ///from base of atom/handle_atom_del(): (atom/deleted) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index f0503bbff73..498daf9f95c 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -59,6 +59,9 @@ /// from /obj/machinery/atmospherics/components/unary/cryo_cell/set_on(bool): (on) #define COMSIG_CRYO_SET_ON "cryo_set_on" +/// from /obj/proc/make_unfrozen() +#define COMSIG_OBJ_UNFREEZE "obj_unfreeze" + // /obj/machinery/atmospherics/components/binary/valve signals /// from /obj/machinery/atmospherics/components/binary/valve/toggle(): (on) diff --git a/code/__DEFINES/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm index 24cca713c65..79abb67d183 100644 --- a/code/__DEFINES/important_recursive_contents.dm +++ b/code/__DEFINES/important_recursive_contents.dm @@ -5,3 +5,5 @@ ///the client mobs channel of the important_recursive_contents list, everything in here will be a mob with an attached client ///this is given to both a clients mob, and a clients eye, both point to the clients mob #define RECURSIVE_CONTENTS_CLIENT_MOBS "recursive_contents_client_mobs" +///the parent of storage components currently shown to some client mob get this. gets removed when nothing is viewing the parent +#define RECURSIVE_CONTENTS_ACTIVE_STORAGE "recursive_contents_active_storage" diff --git a/code/__DEFINES/industrial_lift.dm b/code/__DEFINES/industrial_lift.dm new file mode 100644 index 00000000000..9d3052c01f0 --- /dev/null +++ b/code/__DEFINES/industrial_lift.dm @@ -0,0 +1,20 @@ +//Booleans in arguments are confusing, so I made them defines. +///the lift's controls are currently locked from user input +#define LIFT_PLATFORM_LOCKED 1 +///the lift's controls are currently unlocked so user's can direct it +#define LIFT_PLATFORM_UNLOCKED 0 + +//lift_id's +///basic lift_id, goes up and down +#define BASIC_LIFT_ID "base" +///tram lift_id, goes left and right or north and south. maybe one day be able to turn and go up/down as well +#define TRAM_LIFT_ID "tram" +///debug lift_id +#define DEBUG_LIFT_ID "debug" + + +//specific_lift_id's +///the specific_lift_id of the main station tram landmark for tramstation that spawns roundstart. +#define MAIN_STATION_TRAM "main station tram" +///the specific_lift_id of the tram on the hilbert research station +#define HILBERT_TRAM "tram_hilbert" diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm index 608f85660c2..e6ffcbae79d 100644 --- a/code/__DEFINES/obj_flags.dm +++ b/code/__DEFINES/obj_flags.dm @@ -9,7 +9,6 @@ #define ON_BLUEPRINTS (1<<5) //Are we visible on the station blueprints at roundstart? #define UNIQUE_RENAME (1<<6) // can you customize the description/name of the thing? #define USES_TGUI (1<<7) //put on things that use tgui on ui_interact instead of custom/old UI. -#define FROZEN (1<<8) #define BLOCK_Z_OUT_DOWN (1<<9) // Should this object block z falling from loc? #define BLOCK_Z_OUT_UP (1<<10) // Should this object block z uprise from loc? #define BLOCK_Z_IN_DOWN (1<<11) // Should this object block z falling from above? diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index ae00cf0ed4a..a6b2e3b85e6 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -488,6 +488,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_AREA_SENSITIVE "area-sensitive" ///every hearing sensitive atom has this trait #define TRAIT_HEARING_SENSITIVE "hearing_sensitive" +///every object that is currently the active storage of some client mob has this trait +#define TRAIT_ACTIVE_STORAGE "active_storage" /// Climbable trait, given and taken by the climbable element when added or removed. Exists to be easily checked via HAS_TRAIT(). #define TRAIT_CLIMBABLE "trait_climbable" @@ -895,5 +897,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Ignores body_parts_covered during the add_fingerprint() proc. Works both on the person and the item in the glove slot. #define TRAIT_FINGERPRINT_PASSTHROUGH "fingerprint_passthrough" +/// this object has been frozen +#define TRAIT_FROZEN "frozen" + /// Currently fishing #define TRAIT_GONE_FISHING "fishing" diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 8ef3ecb07f8..9b60641f864 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1041,23 +1041,9 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0,0,0))) -/obj/proc/make_frozen_visual() - // Used to make the frozen item visuals for Freon. - if(resistance_flags & FREEZE_PROOF) - return - if(!(obj_flags & FROZEN)) - name = "frozen [name]" - add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY) - alpha -= 25 - obj_flags |= FROZEN - //Assumes already frozed /obj/proc/make_unfrozen() - if(obj_flags & FROZEN) - name = replacetext(name, "frozen ", "") - remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix) - alpha += 25 - obj_flags &= ~FROZEN + SEND_SIGNAL(src, COMSIG_OBJ_UNFREEZE) /// generates a filename for a given asset. /// like generate_asset_name(), except returns the rsc reference and the rsc file hash as well as the asset name (sans extension) diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index 590962dc358..85b228e3178 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -274,7 +274,6 @@ DEFINE_BITFIELD(obj_flags, list( "CAN_BE_HIT" = CAN_BE_HIT, "DANGEROUS_POSSESSION" = DANGEROUS_POSSESSION, "EMAGGED" = EMAGGED, - "FROZEN" = FROZEN, "IN_USE" = IN_USE, "NO_BUILD" = NO_BUILD, "ON_BLUEPRINTS" = ON_BLUEPRINTS, diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 1f7de75b954..1ba022063ec 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -213,11 +213,13 @@ for(var/atom/target in checking) // will filter out nulls if(closed[target] || isarea(target)) // avoid infinity situations continue - closed[target] = TRUE + if(isturf(target) || isturf(target.loc) || (target in direct_access) || (ismovable(target) && target.flags_1 & IS_ONTOP_1)) //Directly accessible atoms if(Adjacent(target) || (tool && CheckToolReach(src, target, tool.reach))) //Adjacent or reaching attacks return TRUE + closed[target] = TRUE + if (!target.loc) continue diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm index 8c235bcefb0..9c03ef41b62 100644 --- a/code/controllers/subsystem/explosions.dm +++ b/code/controllers/subsystem/explosions.dm @@ -549,18 +549,18 @@ SUBSYSTEM_DEF(explosions) var/base_shake_amount = sqrt(near_distance / (distance + 1)) if(distance <= round(near_distance + world.view - 2, 1)) // If you are close enough to see the effects of the explosion first-hand (ignoring walls) - listener.playsound_local(epicenter, null, 100, TRUE, frequency, S = near_sound) + listener.playsound_local(epicenter, null, 100, TRUE, frequency, sound_to_use = near_sound) if(base_shake_amount > 0) shake_camera(listener, NEAR_SHAKE_DURATION, clamp(base_shake_amount, 0, NEAR_SHAKE_CAP)) else if(distance < far_distance) // You can hear a far explosion if you are outside the blast radius. Small explosions shouldn't be heard throughout the station. var/far_volume = clamp(far_distance / 2, FAR_LOWER, FAR_UPPER) if(creaking) - listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = creaking_sound, distance_multiplier = 0) + listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = creaking_sound, distance_multiplier = 0) else if(prob(FAR_SOUND_PROB)) // Sound variety during meteor storm/tesloose/other bad event - listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = far_sound, distance_multiplier = 0) + listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = far_sound, distance_multiplier = 0) else - listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = echo_sound, distance_multiplier = 0) + listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = echo_sound, distance_multiplier = 0) if(base_shake_amount || quake_factor) base_shake_amount = max(base_shake_amount, quake_factor * 3, 0) // Devastating explosions rock the station and ground @@ -573,7 +573,7 @@ SUBSYSTEM_DEF(explosions) shake_camera(listener, FAR_SHAKE_DURATION, clamp(quake_factor / 4, 0, FAR_SHAKE_CAP)) else echo_volume = 40 - listener.playsound_local(epicenter, null, echo_volume, TRUE, frequency, S = echo_sound, distance_multiplier = 0) + listener.playsound_local(epicenter, null, echo_volume, TRUE, frequency, sound_to_use = echo_sound, distance_multiplier = 0) if(creaking) // 5 seconds after the bang, the station begins to creak addtimer(CALLBACK(listener, /mob/proc/playsound_local, epicenter, null, rand(FREQ_LOWER, FREQ_UPPER), TRUE, frequency, null, null, FALSE, hull_creaking_sound, 0), CREAK_DELAY) diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 58fbf7ab952..7479845c2e9 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -40,14 +40,21 @@ SUBSYSTEM_DEF(mapping) // Z-manager stuff var/station_start // should only be used for maploading-related tasks var/space_levels_so_far = 0 - ///list of all the z level datums created representing the z levels in the world - var/list/z_list + ///list of all z level datums in the order of their z (z level 1 is at index 1, etc.) + var/list/datum/space_level/z_list + ///list of all z level indices that form multiz connections and whether theyre linked up or down + ///list of lists, inner lists are of the form: list("up or down link direction" = TRUE) + var/list/multiz_levels = list() var/datum/space_level/transit var/datum/space_level/empty_space var/num_of_res_levels = 1 /// True when in the process of adding a new Z-level, global locking var/adding_new_zlevel = FALSE + ///shows the default gravity value for each z level. recalculated when gravity generators change. + ///associative list of the form: list("[z level num]" = max generator gravity in that z level OR the gravity level trait) + var/list/gravity_by_z_level = list() + /datum/controller/subsystem/mapping/New() ..() #ifdef FORCE_MAP @@ -101,8 +108,48 @@ SUBSYSTEM_DEF(mapping) generate_station_area_list() initialize_reserved_level(transit.z_value) SSticker.OnRoundstart(CALLBACK(src, .proc/spawn_maintenance_loot)) + generate_z_level_linkages() + calculate_default_z_level_gravities() + return ..() +/datum/controller/subsystem/mapping/proc/calculate_default_z_level_gravities() + for(var/z_level in 1 to length(z_list)) + calculate_z_level_gravity(z_level) + +/datum/controller/subsystem/mapping/proc/generate_z_level_linkages() + for(var/z_level in 1 to length(z_list)) + generate_linkages_for_z_level(z_level) + +/datum/controller/subsystem/mapping/proc/generate_linkages_for_z_level(z_level) + if(!isnum(z_level) || z_level <= 0) + return FALSE + + if(multiz_levels.len < z_level) + multiz_levels.len = z_level + + var/linked_down = level_trait(z_level, ZTRAIT_DOWN) + var/linked_up = level_trait(z_level, ZTRAIT_UP) + multiz_levels[z_level] = list() + if(linked_down) + multiz_levels[z_level]["[DOWN]"] = TRUE + if(linked_up) + multiz_levels[z_level]["[UP]"] = TRUE + +/datum/controller/subsystem/mapping/proc/calculate_z_level_gravity(z_level_number) + if(!isnum(z_level_number) || z_level_number < 1) + return FALSE + + var/max_gravity = 0 + + for(var/obj/machinery/gravity_generator/main/grav_gen as anything in GLOB.gravity_generators["[z_level_number]"]) + max_gravity = max(grav_gen.setting, max_gravity) + + max_gravity = max_gravity || level_trait(z_level_number, ZTRAIT_GRAVITY) || 0//just to make sure no nulls + gravity_by_z_level["[z_level_number]"] = max_gravity + return max_gravity + + /** * ##setup_ruins * @@ -215,6 +262,7 @@ Used by the AI doomsday and the self-destruct nuke. clearing_reserved_turfs = SSmapping.clearing_reserved_turfs z_list = SSmapping.z_list + multiz_levels = SSmapping.multiz_levels #define INIT_ANNOUNCE(X) to_chat(world, span_boldannounce("[X]")); log_world(X) /datum/controller/subsystem/mapping/proc/LoadGroup(list/errorList, name, path, files, list/traits, list/default_traits, silent = FALSE) diff --git a/code/controllers/subsystem/processing/tramprocess.dm b/code/controllers/subsystem/processing/tramprocess.dm index f72ba30ddee..b497cce8b8c 100644 --- a/code/controllers/subsystem/processing/tramprocess.dm +++ b/code/controllers/subsystem/processing/tramprocess.dm @@ -1,5 +1,15 @@ PROCESSING_SUBSYSTEM_DEF(tramprocess) name = "Tram Process" - wait = 1 + wait = 0.5 /// only used on maps with trams, so only enabled by such. can_fire = FALSE + + ///how much time a tram can take per movement before we notify admins and slow down the tram. in milliseconds + var/max_time = 15 + + ///how many times the tram can move costing over max_time milliseconds before it gets slowed down + var/max_exceeding_moves = 5 + + ///how many times the tram can move costing less than half max_time milliseconds before we speed it back up again. + ///is only used if the tram has been slowed down for exceeding max_time + var/max_cheap_moves = 5 diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm index 0608140bb7c..b9dc0988840 100644 --- a/code/controllers/subsystem/spatial_gridmap.dm +++ b/code/controllers/subsystem/spatial_gridmap.dm @@ -171,53 +171,6 @@ SUBSYSTEM_DEF(spatial_grid) var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value) new_cell_grid[y] += cell -///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears. -///i really fucking hope this never gets called after init :clueless: -/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate) - for(var/new_ear in 1 to number_to_generate) - pregenerated_oranges_ears += new/mob/oranges_ear(null) - - number_of_oranges_ears = length(pregenerated_oranges_ears) - -///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf. -///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to) -/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears) - var/input_length = length(atoms_that_need_ears) - - if(input_length > number_of_oranges_ears) - stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen") - pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it - - . = list() - - ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf - var/mob/oranges_ear/current_ear - ///the next atom in atoms_that_need_ears an ear assigned to it - var/atom/assigned_atom - ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one - ///because allocating more than one oranges_ear to a given loc wastes view iterations - var/turf/turf_loc - - for(var/current_ear_index in 1 to input_length) - assigned_atom = atoms_that_need_ears[current_ear_index] - - turf_loc = get_turf(assigned_atom) - if(!turf_loc) - continue - - current_ear = pregenerated_oranges_ears[current_ear_index] - - if(turf_loc.assigned_oranges_ear) - turf_loc.assigned_oranges_ear.references += assigned_atom - continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves - - current_ear.references += assigned_atom - - current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us - turf_loc.assigned_oranges_ear = current_ear - - . += current_ear - ///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed. ///because i never tested this. /datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy) @@ -278,10 +231,6 @@ SUBSYSTEM_DEF(spatial_grid) . = list() - //cache for sanic speeds - var/cells_on_y_axis = src.cells_on_y_axis - var/cells_on_x_axis = src.cells_on_x_axis - //technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this var/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z] @@ -458,6 +407,8 @@ SUBSYSTEM_DEF(spatial_grid) GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + return intersecting_cell + /** * find the spatial map cell that target used to belong to, then remove the target (and sometimes it's important_recusive_contents) from it. * make sure to provide the turf old_target used to be "in" @@ -584,6 +535,53 @@ SUBSYSTEM_DEF(spatial_grid) message_admins(cell_coords) message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]") +///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears. +///i really fucking hope this never gets called after init :clueless: +/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate) + for(var/new_ear in 1 to number_to_generate) + pregenerated_oranges_ears += new/mob/oranges_ear(null) + + number_of_oranges_ears = length(pregenerated_oranges_ears) + +///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf. +///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to) +/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears) + var/input_length = length(atoms_that_need_ears) + + if(input_length > number_of_oranges_ears) + stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen") + pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it + + . = list() + + ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf + var/mob/oranges_ear/current_ear + ///the next atom in atoms_that_need_ears an ear assigned to it + var/atom/assigned_atom + ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one + ///because allocating more than one oranges_ear to a given loc wastes view iterations + var/turf/turf_loc + + for(var/current_ear_index in 1 to input_length) + assigned_atom = atoms_that_need_ears[current_ear_index] + + turf_loc = get_turf(assigned_atom) + if(!turf_loc) + continue + + current_ear = pregenerated_oranges_ears[current_ear_index] + + if(turf_loc.assigned_oranges_ear) + turf_loc.assigned_oranges_ear.references += assigned_atom + continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves + + current_ear.references += assigned_atom + + current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us + turf_loc.assigned_oranges_ear = current_ear + + . += current_ear + ///debug proc for finding how full the cells of src's z level are /atom/proc/find_grid_statistics_for_z_level(insert_clients = 0) var/raw_clients = 0 diff --git a/code/datums/components/bloodysoles.dm b/code/datums/components/bloodysoles.dm index 9268385775f..385f177505e 100644 --- a/code/datums/components/bloodysoles.dm +++ b/code/datums/components/bloodysoles.dm @@ -53,19 +53,42 @@ var/obj/item/parent_item = parent parent_item.update_slot_icon() + +/datum/component/bloodysoles/proc/reset_bloody_shoes() + bloody_shoes = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0) + on_changed_bloody_shoes(BLOOD_STATE_NOT_BLOODY) + +///lowers bloody_shoes[index] by adjust_by +/datum/component/bloodysoles/proc/adjust_bloody_shoes(index, adjust_by) + bloody_shoes[index] = max(bloody_shoes[index] - adjust_by, 0) + on_changed_bloody_shoes() + +/datum/component/bloodysoles/proc/set_bloody_shoes(index, new_value) + bloody_shoes[index] = new_value + on_changed_bloody_shoes(index) + +///called whenever the value of bloody_soles changes +/datum/component/bloodysoles/proc/on_changed_bloody_shoes(index) + if(index && index != last_blood_state) + last_blood_state = index + if(!wielder) + return + if(bloody_shoes[last_blood_state] <= BLOOD_FOOTPRINTS_MIN * 2)//need twice that amount to make footprints + UnregisterSignal(wielder, COMSIG_MOVABLE_MOVED) + else + RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved, override = TRUE) + /** * Run to equally share the blood between us and a decal */ /datum/component/bloodysoles/proc/share_blood(obj/effect/decal/cleanable/pool) - last_blood_state = pool.blood_state - // Share the blood between our boots and the blood pool - var/total_bloodiness = pool.bloodiness + bloody_shoes[last_blood_state] + var/total_bloodiness = pool.bloodiness + bloody_shoes[pool.blood_state] // We can however be limited by how much blood we can hold var/new_our_bloodiness = min(BLOOD_ITEM_MAX, total_bloodiness / 2) - bloody_shoes[last_blood_state] = new_our_bloodiness + set_bloody_shoes(pool.blood_state, new_our_bloodiness) pool.bloodiness = total_bloodiness - new_our_bloodiness // Give the pool the remaining blood incase we were limited if(HAS_TRAIT(parent_atom, TRAIT_LIGHT_STEP)) //the character is agile enough to don't mess their clothing and hands just from one blood splatter at floor @@ -105,7 +128,8 @@ equipped_slot = slot wielder = equipper - RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved) + if(bloody_shoes[last_blood_state] > BLOOD_FOOTPRINTS_MIN * 2) + RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved) RegisterSignal(wielder, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood) /** @@ -147,7 +171,7 @@ oldLocFP.update_appearance() else if(find_pool_by_blood_state(oldLocTurf)) // No footprints in the tile we left, but there was some other blood pool there. Add exit footprints on it - bloody_shoes[last_blood_state] -= half_our_blood + adjust_bloody_shoes(last_blood_state, half_our_blood) update_icon() oldLocFP = new(oldLocTurf) @@ -167,7 +191,7 @@ // Create new footprints if(half_our_blood >= BLOOD_FOOTPRINTS_MIN) - bloody_shoes[last_blood_state] -= half_our_blood + adjust_bloody_shoes(last_blood_state, half_our_blood) update_icon() var/obj/effect/decal/cleanable/blood/footprints/FP = new(get_turf(parent_atom)) @@ -213,8 +237,7 @@ if(!(clean_types & CLEAN_TYPE_BLOOD) || last_blood_state == BLOOD_STATE_NOT_BLOODY) return NONE - bloody_shoes = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0) - last_blood_state = BLOOD_STATE_NOT_BLOODY + reset_bloody_shoes() update_icon() return COMPONENT_CLEANED @@ -233,7 +256,6 @@ if(!bloody_feet) bloody_feet = mutable_appearance('icons/effects/blood.dmi', "shoeblood", SHOES_LAYER) RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/on_clean) - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved) RegisterSignal(parent, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood) RegisterSignal(parent, COMSIG_CARBON_UNEQUIP_SHOECOVER, .proc/unequip_shoecover) RegisterSignal(parent, COMSIG_CARBON_EQUIP_SHOECOVER, .proc/equip_shoecover) diff --git a/code/datums/components/drift.dm b/code/datums/components/drift.dm index 4d509026a44..d0e4bc6441f 100644 --- a/code/datums/components/drift.dm +++ b/code/datums/components/drift.dm @@ -114,7 +114,6 @@ return var/atom/movable/movable_parent = parent - movable_parent.inertia_moving = FALSE movable_parent.setDir(old_dir) if(movable_parent.Process_Spacemove(drifting_loop.direction, continuous_move = TRUE)) glide_to_halt(visual_delay) @@ -137,7 +136,7 @@ if(!isturf(movable_parent.loc)) qdel(src) return - if(movable_parent.inertia_moving) //This'll be handled elsewhere + if(movable_parent.inertia_moving) return if(!movable_parent.Process_Spacemove(drifting_loop.direction, continuous_move = TRUE)) return diff --git a/code/datums/components/overlay_lighting.dm b/code/datums/components/overlay_lighting.dm index 9fd0e0a4f5a..67290d4c58c 100644 --- a/code/datums/components/overlay_lighting.dm +++ b/code/datums/components/overlay_lighting.dm @@ -111,7 +111,6 @@ . = ..() if(directional) RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, .proc/on_parent_dir_change) - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_parent_moved) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_RANGE, .proc/set_range) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_POWER, .proc/set_power) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .proc/set_color) @@ -119,6 +118,7 @@ RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .proc/on_light_flags_change) RegisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT, .proc/on_parent_crafted) RegisterSignal(parent, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_parent_moved) var/atom/movable/movable_parent = parent if(movable_parent.light_flags & LIGHT_ATTACHED) overlay_lighting_flags |= LIGHTING_ATTACHED @@ -245,8 +245,9 @@ return if(new_holder != parent && new_holder != parent_attached_to) RegisterSignal(new_holder, COMSIG_PARENT_QDELETING, .proc/on_holder_qdel) - RegisterSignal(new_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved) RegisterSignal(new_holder, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater) + if(overlay_lighting_flags & LIGHTING_ON) + RegisterSignal(new_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved) if(directional) RegisterSignal(new_holder, COMSIG_ATOM_DIR_CHANGE, .proc/on_holder_dir_change) set_direction(new_holder.dir) @@ -423,6 +424,8 @@ cast_directional_light() add_dynamic_lumi() overlay_lighting_flags |= LIGHTING_ON + if(current_holder && current_holder != parent && current_holder != parent_attached_to) + RegisterSignal(current_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved) get_new_turfs() @@ -433,6 +436,8 @@ if(current_holder) remove_dynamic_lumi() overlay_lighting_flags &= ~LIGHTING_ON + if(current_holder) + UnregisterSignal(current_holder, COMSIG_MOVABLE_MOVED) clean_old_turfs() diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm index acc8c8d2a0c..0c40efa7dde 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -4,45 +4,66 @@ /datum/component/storage dupe_mode = COMPONENT_DUPE_UNIQUE - var/datum/component/storage/concrete/master //If not null, all actions act on master and this is just an access point. + ///If not null, all actions act on master and this is just an access point. + var/datum/component/storage/concrete/master - var/list/can_hold //if this is set, only items, and their children, will fit - var/list/cant_hold //if this is set, items, and their children, won't fit - var/list/exception_hold //if set, these items will be the exception to the max size of object that can fit. + ///if this is set, only items, and their children, will fit + var/list/can_hold + ///if this is set, items, and their children, won't fit + var/list/cant_hold + ///if set, these items will be the exception to the max size of object that can fit. + var/list/exception_hold /// If set can only contain stuff with this single trait present. var/list/can_hold_trait var/can_hold_description - var/list/mob/is_using //lazy list of mobs looking at the contents of this storage. + ///lazy list of mobs looking at the contents of this storage. + var/list/mob/is_using - var/locked = FALSE //when locked nothing can see inside or use it. + ///when locked nothing can see inside or use it. + var/locked = FALSE - var/max_w_class = WEIGHT_CLASS_SMALL //max size of objects that will fit. - var/max_combined_w_class = 14 //max combined sizes of objects that will fit. - var/max_items = 7 //max number of objects that will fit. + ///max size of objects that will fit. + var/max_w_class = WEIGHT_CLASS_SMALL + ///max combined sizes of objects that will fit. + var/max_combined_w_class = 14 + ///max number of objects that will fit. + var/max_items = 7 var/emp_shielded = FALSE - var/silent = FALSE //whether this makes a message when things are put in. - var/click_gather = FALSE //whether this can be clicked on items to pick it up rather than the other way around. - var/rustle_sound = TRUE //play rustle sound on interact. - var/allow_quick_empty = FALSE //allow empty verb which allows dumping on the floor of everything inside quickly. - var/allow_quick_gather = FALSE //allow toggle mob verb which toggles collecting all items from a tile. + ///whether this makes a message when things are put in. + var/silent = FALSE + ///whether this can be clicked on items to pick it up rather than the other way around. + var/click_gather = FALSE + ///play rustle sound on interact. + var/rustle_sound = TRUE + ///allow empty verb which allows dumping on the floor of everything inside quickly. + var/allow_quick_empty = FALSE + ///allow toggle mob verb which toggles collecting all items from a tile. + var/allow_quick_gather = FALSE var/collection_mode = COLLECT_EVERYTHING - var/insert_preposition = "in" //you put things "in" a bag, but "on" a tray. + ///you put things "in" a bag, but "on" a tray. + var/insert_preposition = "in" - var/display_numerical_stacking = FALSE //stack things of the same type and show as a single object with a number. + ///stack things of the same type and show as a single object with a number. + var/display_numerical_stacking = FALSE - var/atom/movable/screen/storage/boxes //storage display object - var/atom/movable/screen/close/closer //close button object + ///storage display object + var/atom/movable/screen/storage/boxes + ///close button object + var/atom/movable/screen/close/closer - var/allow_big_nesting = FALSE //allow storage objects of the same or greater size. + ///allow storage objects of the same or greater size. + var/allow_big_nesting = FALSE - var/attack_hand_interact = TRUE //interact on attack hand. - var/quickdraw = FALSE //altclick interact + ///interact on attack hand. + var/attack_hand_interact = TRUE + ///altclick interact + var/quickdraw = FALSE var/datum/action/item_action/storage_gather_mode/modeswitch_action @@ -420,6 +441,9 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches) M.client.screen |= closer M.client.screen |= real_location.contents M.set_active_storage(src) + if(ismovable(real_location)) + var/atom/movable/movable_loc = real_location + movable_loc.become_active_storage(src) LAZYOR(is_using, M) RegisterSignal(M, COMSIG_PARENT_QDELETING, .proc/mob_deleted) return TRUE @@ -437,6 +461,10 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches) if(!M.client) return TRUE var/atom/real_location = real_location() + if(!length(is_using) && ismovable(real_location)) + var/atom/movable/movable_loc = real_location + movable_loc.lose_active_storage(src) + M.client.screen -= boxes M.client.screen -= closer M.client.screen -= real_location.contents diff --git a/code/datums/components/wet_floor.dm b/code/datums/components/wet_floor.dm index 594ab849e03..7b84f1dabb7 100644 --- a/code/datums/components/wet_floor.dm +++ b/code/datums/components/wet_floor.dm @@ -127,8 +127,7 @@ decrease = max(0, decrease) if((is_wet() & TURF_WET_ICE) && t > T0C) //Ice melts into water! for(var/obj/O in T.contents) - if(O.obj_flags & FROZEN) - O.make_unfrozen() + O.make_unfrozen() add_wet(TURF_WET_WATER, max_time_left()) dry(null, TURF_WET_ICE) dry(null, ALL, FALSE, decrease) diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm index 30bd98a3268..204a2742011 100644 --- a/code/datums/elements/_element.dm +++ b/code/datums/elements/_element.dm @@ -32,9 +32,9 @@ /// Deactivates the functionality defines by the element on the given datum /datum/element/proc/Detach(datum/source, ...) SIGNAL_HANDLER + SHOULD_CALL_PARENT(TRUE) SEND_SIGNAL(source, COMSIG_ELEMENT_DETACH, src) - SHOULD_CALL_PARENT(TRUE) UnregisterSignal(source, COMSIG_PARENT_QDELETING) /datum/element/Destroy(force) diff --git a/code/datums/elements/atmos_sensitive.dm b/code/datums/elements/atmos_sensitive.dm index ca676327e26..c7dd4cf2f7a 100644 --- a/code/datums/elements/atmos_sensitive.dm +++ b/code/datums/elements/atmos_sensitive.dm @@ -22,6 +22,7 @@ /datum/element/atmos_sensitive/Detach(datum/source) var/atom/us = source us.RemoveElement(/datum/element/connect_loc, pass_on) + UnregisterSignal(source, COMSIG_MOVABLE_MOVED) if(us.flags_1 & ATMOS_IS_PROCESSING_1) us.atmos_end() SSair.atom_process -= us diff --git a/code/datums/elements/chewable.dm b/code/datums/elements/chewable.dm index 91e9a0ddac9..a5e10e26d8d 100644 --- a/code/datums/elements/chewable.dm +++ b/code/datums/elements/chewable.dm @@ -30,6 +30,7 @@ RegisterSignal(target, COMSIG_ITEM_EQUIPPED, .proc/on_equipped) /datum/element/chewable/Detach(datum/source, force) + . = ..() processing -= source UnregisterSignal(source, list(COMSIG_ITEM_DROPPED, COMSIG_ITEM_EQUIPPED)) diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm index a529025c967..b9a1ef5df20 100644 --- a/code/datums/elements/footstep.dm +++ b/code/datums/elements/footstep.dm @@ -87,7 +87,7 @@ if(steps % 2) return - if(steps != 0 && !source.has_gravity(turf)) // don't need to step as often when you hop around + if(steps != 0 && !source.has_gravity()) // don't need to step as often when you hop around return return turf @@ -117,10 +117,10 @@ return playsound(source_loc, pick(footstep_sounds[turf_footstep][1]), footstep_sounds[turf_footstep][2] * volume, TRUE, footstep_sounds[turf_footstep][3] + e_range, falloff_distance = 1, vary = sound_vary) -/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction) +/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction, forced, list/old_locs, momentum_change) SIGNAL_HANDLER - if (SHOULD_DISABLE_FOOTSTEPS(source)) + if (SHOULD_DISABLE_FOOTSTEPS(source) || !momentum_change) return var/volume_multiplier = 1 @@ -134,21 +134,31 @@ if(!source_loc) return - play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE) + //cache for sanic speed (lists are references anyways) + var/static/list/footstep_sounds = GLOB.footstep + ///list returned by playsound() filled by client mobs who heard the footstep. given to play_fov_effect() + var/list/heard_clients + if ((source.wear_suit?.body_parts_covered | source.w_uniform?.body_parts_covered | source.shoes?.body_parts_covered) & FEET) // we are wearing shoes - playsound(source_loc, pick(GLOB.footstep[source_loc.footstep][1]), - GLOB.footstep[source_loc.footstep][2] * volume * volume_multiplier, + + heard_clients = playsound(source_loc, pick(footstep_sounds[source_loc.footstep][1]), + footstep_sounds[source_loc.footstep][2] * volume * volume_multiplier, TRUE, - GLOB.footstep[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary) + footstep_sounds[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary) else if(source.dna.species.special_step_sounds) - playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary) + heard_clients = playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary) else - playsound(source_loc, pick(GLOB.barefootstep[source_loc.barefootstep][1]), - GLOB.barefootstep[source_loc.barefootstep][2] * volume * volume_multiplier, + var/static/list/bare_footstep_sounds = GLOB.barefootstep + + heard_clients = playsound(source_loc, pick(bare_footstep_sounds[source_loc.barefootstep][1]), + bare_footstep_sounds[source_loc.barefootstep][2] * volume * volume_multiplier, TRUE, - GLOB.barefootstep[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary) + bare_footstep_sounds[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary) + + if(heard_clients) + play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE, override_list = heard_clients) ///Prepares a footstep for machine walking diff --git a/code/datums/elements/forced_gravity.dm b/code/datums/elements/forced_gravity.dm index ada16943b86..96889194f2d 100644 --- a/code/datums/elements/forced_gravity.dm +++ b/code/datums/elements/forced_gravity.dm @@ -1,16 +1,18 @@ /datum/element/forced_gravity element_flags = ELEMENT_BESPOKE id_arg_index = 2 + ///the level of gravity we force unto our target var/gravity - var/ignore_space + ///whether we will override the turf if it forces no gravity + var/ignore_turf_gravity -/datum/element/forced_gravity/Attach(datum/target, gravity=1, ignore_space=FALSE) +/datum/element/forced_gravity/Attach(datum/target, gravity=1, ignore_turf_gravity = FALSE) . = ..() if(!isatom(target)) return ELEMENT_INCOMPATIBLE src.gravity = gravity - src.ignore_space = ignore_space + src.ignore_turf_gravity = ignore_turf_gravity RegisterSignal(target, COMSIG_ATOM_HAS_GRAVITY, .proc/gravity_check) if(isturf(target)) @@ -24,10 +26,12 @@ /datum/element/forced_gravity/proc/gravity_check(datum/source, turf/location, list/gravs) SIGNAL_HANDLER - if(!ignore_space && isspaceturf(location)) - return + if(!ignore_turf_gravity && location.force_no_gravity) + return FALSE gravs += gravity + return TRUE + /datum/element/forced_gravity/proc/turf_gravity_check(datum/source, atom/checker, list/gravs) SIGNAL_HANDLER diff --git a/code/datums/elements/frozen.dm b/code/datums/elements/frozen.dm new file mode 100644 index 00000000000..e1cc273774a --- /dev/null +++ b/code/datums/elements/frozen.dm @@ -0,0 +1,47 @@ +///simple element to handle frozen obj's +/datum/element/frozen + element_flags = ELEMENT_DETACH + +/datum/element/frozen/Attach(datum/target) + . = ..() + if(!isobj(target)) + return ELEMENT_INCOMPATIBLE + + var/obj/target_obj = target + if(target_obj.obj_flags & FREEZE_PROOF) + return ELEMENT_INCOMPATIBLE + + target_obj.name = "frozen [target_obj.name]" + target_obj.add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY) + target_obj.alpha -= 25 + + RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/on_moved) + RegisterSignal(target, COMSIG_MOVABLE_POST_THROW, .proc/shatter_on_throw) + RegisterSignal(target, COMSIG_OBJ_UNFREEZE, .proc/on_unfrozen) + +/datum/element/frozen/Detach(datum/source, ...) + var/obj/obj_source = source + obj_source.name = replacetext(obj_source.name, "frozen ", "") + obj_source.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix) + obj_source.alpha += 25 + . = ..() + +/datum/element/frozen/proc/on_unfrozen(datum/source) + SIGNAL_HANDLER + Detach(source) + +/datum/element/frozen/proc/shatter_on_throw(datum/target) + SIGNAL_HANDLER + var/obj/obj_target = target + obj_target.visible_message(span_danger("[obj_target] shatters into a million pieces!")) + qdel(obj_target) + +/datum/element/frozen/proc/on_moved(datum/target) + SIGNAL_HANDLER + var/obj/obj_target = target + if(!isopenturf(obj_target.loc)) + return + + var/turf/open/turf_loc = obj_target.loc + if(turf_loc.air?.temperature >= T0C)//unfreezes target + Detach(target) diff --git a/code/datums/elements/light_eater.dm b/code/datums/elements/light_eater.dm index 026258122a7..0eb59bbc850 100644 --- a/code/datums/elements/light_eater.dm +++ b/code/datums/elements/light_eater.dm @@ -67,8 +67,7 @@ /datum/element/light_eater/proc/table_buffet(atom/commisary, datum/devourer) . = list() SEND_SIGNAL(commisary, COMSIG_LIGHT_EATER_QUEUE, ., devourer) - for(var/nom in commisary.light_sources) - var/datum/light_source/morsel = nom + for(var/datum/light_source/morsel as anything in commisary.light_sources) .[morsel.source_atom] = TRUE /** diff --git a/code/datums/elements/obj_regen.dm b/code/datums/elements/obj_regen.dm index ef55457c28c..4830d420043 100644 --- a/code/datums/elements/obj_regen.dm +++ b/code/datums/elements/obj_regen.dm @@ -28,6 +28,7 @@ processing |= target /datum/element/obj_regen/Detach(obj/target) + . = ..() UnregisterSignal(target, COMSIG_ATOM_TAKE_DAMAGE) processing -= target if(!length(processing)) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 5970fc4851c..67c6deb77a1 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -403,7 +403,8 @@ GLOBAL_LIST_EMPTY(teleportlocs) /area/Entered(atom/movable/arrived, area/old_area) set waitfor = FALSE SEND_SIGNAL(src, COMSIG_AREA_ENTERED, arrived, old_area) - if(!LAZYACCESS(arrived.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE)) + + if(!arrived.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) return for(var/atom/movable/recipient as anything in arrived.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) SEND_SIGNAL(recipient, COMSIG_ENTER_AREA, src) @@ -419,7 +420,18 @@ GLOBAL_LIST_EMPTY(teleportlocs) if(L.client?.prefs.toggles & SOUND_SHIP_AMBIENCE) SEND_SOUND(L, sound('sound/ambience/shipambience.ogg', repeat = 1, wait = 0, volume = 35, channel = CHANNEL_BUZZ)) +/** + * Called when an atom exits an area + * + * Sends signals COMSIG_AREA_EXITED and COMSIG_EXIT_AREA (to a list of atoms) + */ +/area/Exited(atom/movable/gone, direction) + SEND_SIGNAL(src, COMSIG_AREA_EXITED, gone, direction) + if(!gone.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + return + for(var/atom/movable/recipient as anything in gone.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + SEND_SIGNAL(recipient, COMSIG_EXIT_AREA, src) ///Divides total beauty in the room by roomsize to allow us to get an average beauty per tile. /area/proc/update_beauty() @@ -431,20 +443,6 @@ GLOBAL_LIST_EMPTY(teleportlocs) return FALSE //Too big beauty = totalbeauty / areasize - -/** - * Called when an atom exits an area - * - * Sends signals COMSIG_AREA_EXITED and COMSIG_EXIT_AREA (to a list of atoms) - */ -/area/Exited(atom/movable/gone, direction) - SEND_SIGNAL(src, COMSIG_AREA_EXITED, gone, direction) - if(!LAZYACCESS(gone.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE)) - return - for(var/atom/movable/recipient as anything in gone.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) - SEND_SIGNAL(recipient, COMSIG_EXIT_AREA, src) - - /** * Setup an area (with the given name) * diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 2e72afe785e..8dc93ba4217 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1793,7 +1793,9 @@ * Returns true if this atom has gravity for the passed in turf * * Sends signals [COMSIG_ATOM_HAS_GRAVITY] and [COMSIG_TURF_HAS_GRAVITY], both can force gravity with - * the forced gravity var + * the forced gravity var. + * + * micro-optimized to hell because this proc is very hot, being called several times per movement every movement. * * Gravity situations: * * No gravity if you're not in a turf @@ -1804,39 +1806,28 @@ * * otherwise no gravity */ /atom/proc/has_gravity(turf/gravity_turf) - if(!gravity_turf || !isturf(gravity_turf)) + if(!isturf(gravity_turf)) gravity_turf = get_turf(src) - if(!gravity_turf) - return 0 - - var/list/forced_gravity = list() - SEND_SIGNAL(src, COMSIG_ATOM_HAS_GRAVITY, gravity_turf, forced_gravity) - if(!forced_gravity.len) - SEND_SIGNAL(gravity_turf, COMSIG_TURF_HAS_GRAVITY, src, forced_gravity) - if(forced_gravity.len) - var/max_grav = forced_gravity[1] - for(var/i in forced_gravity) - max_grav = max(max_grav, i) - return max_grav - - if(isspaceturf(gravity_turf)) // Turf never has gravity - return 0 - if(istype(gravity_turf, /turf/open/openspace)) //openspace in a space area doesn't get gravity - if(istype(get_area(gravity_turf), /area/space)) + if(!gravity_turf)//no gravity in nullspace return 0 - var/area/turf_area = get_area(gravity_turf) - if(turf_area.has_gravity) // Areas which always has gravity - return turf_area.has_gravity - else - // There's a gravity generator on our z level - if(GLOB.gravity_generators["[gravity_turf.z]"]) - var/max_grav = 0 - for(var/obj/machinery/gravity_generator/main/main_grav_gen as anything in GLOB.gravity_generators["[gravity_turf.z]"]) - max_grav = max(main_grav_gen.setting,max_grav) - return max_grav - return SSmapping.level_trait(gravity_turf.z, ZTRAIT_GRAVITY) + //the list isnt created every time as this proc is very hot, its only accessed if anything is actually listening to the signal too + var/static/list/forced_gravity = list() + if(SEND_SIGNAL(src, COMSIG_ATOM_HAS_GRAVITY, gravity_turf, forced_gravity)) + if(!length(forced_gravity)) + SEND_SIGNAL(gravity_turf, COMSIG_TURF_HAS_GRAVITY, src, forced_gravity) + + var/max_grav = 0 + for(var/i in forced_gravity)//our gravity is the strongest return forced gravity we get + max_grav = max(max_grav, i) + forced_gravity.Cut() + //cut so we can reuse the list, this is ok since forced gravity movers are exceedingly rare compared to all other movement + return max_grav + + var/area/turf_area = gravity_turf.loc + + return !gravity_turf.force_no_gravity && (SSmapping.gravity_by_z_level["[gravity_turf.z]"] || turf_area.has_gravity) /** * Causes effects when the atom gets hit by a rust effect from heretics diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 9d72dfc07a6..927f6069ce8 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -3,8 +3,6 @@ glide_size = 8 appearance_flags = TILE_BOUND|PIXEL_SCALE|LONG_GLIDE - ///how many times a this movable had movement procs called on it since Moved() was last called - var/move_stacks = 0 var/last_move = null var/anchored = FALSE var/move_resist = MOVE_RESIST_DEFAULT @@ -36,8 +34,10 @@ var/pass_flags = NONE /// If false makes [CanPass][/atom/proc/CanPass] call [CanPassThrough][/atom/movable/proc/CanPassThrough] on this type instead of using default behaviour var/generic_canpass = TRUE - var/moving_diagonally = 0 //0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move - var/atom/movable/moving_from_pull //attempt to resume grab after moving instead of before. + ///0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move + var/moving_diagonally = 0 + ///attempt to resume grab after moving instead of before. + var/atom/movable/moving_from_pull ///Holds information about any movement loops currently running/waiting to run on the movable. Lazy, will be null if nothing's going on var/datum/movement_packet/move_packet var/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm @@ -445,8 +445,7 @@ SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, target) glide_size = target - for(var/m in buckled_mobs) - var/mob/buckled_mob = m + for(var/mob/buckled_mob as anything in buckled_mobs) buckled_mob.set_glide_size(target) /** @@ -456,9 +455,9 @@ */ /atom/movable/proc/abstract_move(atom/new_loc) var/atom/old_loc = loc - move_stacks++ + var/direction = get_dir(old_loc, new_loc) loc = new_loc - Moved(old_loc) + Moved(old_loc, direction) //////////////////////////////////////// // Here's where we rewrite how byond handles movement except slightly different @@ -472,7 +471,7 @@ if(!direction) direction = get_dir(src, newloc) - if(set_dir_on_move && !face_mouse) + if(set_dir_on_move && dir != direction && !face_mouse) // SKYRAT EDIT CHANGE setDir(direction) var/is_multi_tile_object = bound_width > 32 || bound_height > 32 @@ -512,7 +511,6 @@ var/atom/oldloc = loc var/area/oldarea = get_area(oldloc) var/area/newarea = get_area(newloc) - move_stacks++ loc = newloc @@ -547,7 +545,7 @@ return FALSE var/atom/oldloc = loc //Early override for some cases like diagonal movement - if(glide_size_override) + if(glide_size_override && glide_size != glide_size_override) set_glide_size(glide_size_override) if(loc != newloc) @@ -599,12 +597,8 @@ moving_diagonally = SECOND_DIAG_STEP . = step(src, SOUTH) if(moving_diagonally == SECOND_DIAG_STEP) - if(!. && set_dir_on_move && !face_mouse) + if(!. && set_dir_on_move && !face_mouse) // SKYRAT EDIT CHANGE setDir(first_step_dir) - else if (!inertia_moving) - newtonian_move(direct) - if(client_mobs_in_contents) // We're done moving, update our parallax now - update_parallax_contents() moving_diagonally = 0 return @@ -635,12 +629,12 @@ //glide_size strangely enough can change mid movement animation and update correctly while the animation is playing //This means that if you don't override it late like this, it will just be set back by the movement update that's called when you move turfs. - if(glide_size_override) + if(glide_size_override && glide_size != glide_size_override) set_glide_size(glide_size_override) last_move = direct - if(set_dir_on_move && !face_mouse) + if(set_dir_on_move && dir != direct && !face_mouse) // SKYRAT EDIT CHANGE setDir(direct) if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob(s) . = FALSE @@ -665,11 +659,12 @@ * * movement_dir is the direction the movement took place. Can be NONE if it was some sort of teleport. * * The forced flag indicates whether this was a forced move, which skips many checks of regular movement. * * The old_locs is an optional argument, in case the moved movable was present in multiple locations before the movement. + * * momentum_change represents whether this movement is due to a "new" force if TRUE or an already "existing" force if FALSE **/ -/atom/movable/proc/Moved(atom/old_loc, movement_dir, forced = FALSE, list/old_locs) +/atom/movable/proc/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) SHOULD_CALL_PARENT(TRUE) - if (!inertia_moving) + if (!inertia_moving && momentum_change) newtonian_move(movement_dir) // If we ain't moving diagonally right now, update our parallax // We don't do this all the time because diag movements should trigger one call to this, not two @@ -677,14 +672,12 @@ if (!moving_diagonally && client_mobs_in_contents) update_parallax_contents() - move_stacks-- - if(move_stacks > 0) //we want only the first Moved() call in the stack to send this signal, all the other ones have an incorrect old_loc - return - if(move_stacks < 0) - stack_trace("move_stacks is negative in Moved()!") - move_stacks = 0 //setting it to 0 so that we dont get every movable with negative move_stacks runtiming on every movement + SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED, old_loc, movement_dir, forced, old_locs, momentum_change) - SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED, old_loc, movement_dir, forced, old_locs) + if(old_loc) + SEND_SIGNAL(old_loc, COMSIG_ATOM_ABSTRACT_EXITED, src, movement_dir) + if(loc) + SEND_SIGNAL(loc, COMSIG_ATOM_ABSTRACT_ENTERED, src, old_loc, old_locs) var/turf/old_turf = get_turf(old_loc) var/turf/new_turf = get_turf(src) @@ -847,7 +840,6 @@ ///allows this movable to know when it has "entered" another area no matter how many movable atoms its stuffed into, uses important_recursive_contents /atom/movable/proc/become_area_sensitive(trait_source = TRAIT_GENERIC) if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) - //RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AREA_SENSITIVE), .proc/on_area_sensitive_trait_loss) for(var/atom/movable/location as anything in get_nested_locs(src) + src) LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) @@ -891,6 +883,24 @@ ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS) UNSETEMPTY(movable_loc.important_recursive_contents) +///called when this movable becomes the parent of a storage component that is currently being viewed by a player. uses important_recursive_contents +/atom/movable/proc/become_active_storage(datum/component/storage/component_source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + ADD_TRAIT(src, TRAIT_ACTIVE_STORAGE, component_source) + +///called when this movable's storage component is no longer viewed by any players, unsets important_recursive_contents +/atom/movable/proc/lose_active_storage(datum/component/storage/component_source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + REMOVE_TRAIT(src, TRAIT_ACTIVE_STORAGE, component_source) + if(HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + ///Sets the anchored var and returns if it was sucessfully changed or not. /atom/movable/proc/set_anchored(anchorvalue) SHOULD_CALL_PARENT(TRUE) @@ -921,12 +931,13 @@ /atom/movable/proc/doMove(atom/destination) . = FALSE - move_stacks++ var/atom/oldloc = loc + var/is_multi_tile = bound_width > world.icon_size || bound_height > world.icon_size if(destination) ///zMove already handles whether a pull from another movable should be broken. if(pulledby && !currently_z_moving) pulledby.stop_pulling() + var/same_loc = oldloc == destination var/area/old_area = get_area(oldloc) var/area/destarea = get_area(destination) @@ -937,23 +948,49 @@ loc = destination if(!same_loc) - if(oldloc) - oldloc.Exited(src, movement_dir) + if(is_multi_tile && isturf(destination)) + var/list/new_locs = block( + destination, + locate( + min(world.maxx, destination.x + ROUND_UP(bound_width / 32)), + min(world.maxy, destination.y + ROUND_UP(bound_height / 32)), + destination.z + ) + ) if(old_area && old_area != destarea) old_area.Exited(src, movement_dir) - destination.Entered(src, oldloc) - if(destarea && old_area != destarea) - destarea.Entered(src, old_area) + for(var/atom/left_loc as anything in locs - new_locs) + left_loc.Exited(src, movement_dir) + + for(var/atom/entering_loc as anything in new_locs - locs) + entering_loc.Entered(src, movement_dir) + + if(old_area && old_area != destarea) + destarea.Entered(src, movement_dir) + else + if(oldloc) + oldloc.Exited(src, movement_dir) + if(old_area && old_area != destarea) + old_area.Exited(src, movement_dir) + destination.Entered(src, oldloc) + if(destarea && old_area != destarea) + destarea.Entered(src, old_area) . = TRUE //If no destination, move the atom into nullspace (don't do this unless you know what you're doing) else . = TRUE - loc = null + if (oldloc) + loc = null var/area/old_area = get_area(oldloc) - oldloc.Exited(src, NONE) + if(is_multi_tile && isturf(oldloc)) + for(var/atom/old_loc as anything in locs) + old_loc.Exited(src, NONE) + else + oldloc.Exited(src, NONE) + if(old_area) old_area.Exited(src, NONE) @@ -979,7 +1016,7 @@ * Called whenever an object moves and by mobs when they attempt to move themselves through space * And when an object or action applies a force on src, see [newtonian_move][/atom/movable/proc/newtonian_move] * - * Return 0 to have src start/keep drifting in a no-grav area and 1 to stop/not start drifting + * Return FALSE to have src start/keep drifting in a no-grav area and TRUE to stop/not start drifting * * Mobs should return 1 if they should be able to move of their own volition, see [/client/proc/Move] * @@ -988,10 +1025,10 @@ * * continuous_move - If this check is coming from something in the context of already drifting */ /atom/movable/proc/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) - if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE) + if(has_gravity()) return TRUE - if(has_gravity(src)) + if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE) return TRUE if(pulledby && (pulledby.pulledby != src || moving_from_pull)) @@ -1400,7 +1437,6 @@ log_admin("[key_name(usr)] has added deadchat control to [src]") message_admins(span_notice("[key_name(usr)] has added deadchat control to [src]")) - /** * A wrapper for setDir that should only be able to fail by living mobs. * diff --git a/code/game/machinery/dance_machine.dm b/code/game/machinery/dance_machine.dm index f548e41d422..04a387f3e22 100644 --- a/code/game/machinery/dance_machine.dm +++ b/code/game/machinery/dance_machine.dm @@ -459,7 +459,7 @@ continue if(!(M in rangers)) rangers[M] = TRUE - M.playsound_local(get_turf(M), null, volume, channel = CHANNEL_JUKEBOX, S = song_played, use_reverb = FALSE) + M.playsound_local(get_turf(M), null, volume, channel = CHANNEL_JUKEBOX, sound_to_use = song_played, use_reverb = FALSE) for(var/mob/L in rangers) if(get_dist(src,L) > 10) rangers -= L diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index aa1a6b6fe86..2531db29302 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -104,13 +104,6 @@ span_danger("You [damage_verb] [src] with [attacking_item][damage ? "." : ", without leaving a mark!"]"), null, COMBAT_MESSAGE_RANGE) log_combat(user, src, "attacked", attacking_item) -/obj/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE) - . = ..() - if(obj_flags & FROZEN) - visible_message(span_danger("[src] shatters into a million pieces!")) - qdel(src) - - /obj/assume_air(datum/gas_mixture/giver) if(loc) return loc.assume_air(giver) diff --git a/code/game/objects/structures/industrial_lift.dm b/code/game/objects/structures/industrial_lift.dm deleted file mode 100644 index 2209e12dfe9..00000000000 --- a/code/game/objects/structures/industrial_lift.dm +++ /dev/null @@ -1,620 +0,0 @@ - -//Booleans in arguments are confusing, so I made them defines. -#define LOCKED 1 -#define UNLOCKED 0 - -///Collect and command -/datum/lift_master - var/list/lift_platforms - /// Typepath list of what to ignore smashing through, controls all lifts - var/list/ignored_smashthroughs = list( - /obj/machinery/power/supermatter_crystal, - /obj/structure/holosign - ) - -/datum/lift_master/New(obj/structure/industrial_lift/lift_platform) - Rebuild_lift_plaform(lift_platform) - ignored_smashthroughs = typecacheof(ignored_smashthroughs) - -/datum/lift_master/Destroy() - for(var/l in lift_platforms) - var/obj/structure/industrial_lift/lift_platform = l - lift_platform.lift_master_datum = null - lift_platforms = null - return ..() - - -/datum/lift_master/proc/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform) - if(new_lift_platform in lift_platforms) - return - new_lift_platform.lift_master_datum = src - LAZYADD(lift_platforms, new_lift_platform) - RegisterSignal(new_lift_platform, COMSIG_PARENT_QDELETING, .proc/remove_lift_platforms) - -/datum/lift_master/proc/remove_lift_platforms(obj/structure/industrial_lift/old_lift_platform) - SIGNAL_HANDLER - - if(!(old_lift_platform in lift_platforms)) - return - old_lift_platform.lift_master_datum = null - LAZYREMOVE(lift_platforms, old_lift_platform) - UnregisterSignal(old_lift_platform, COMSIG_PARENT_QDELETING) - -///Collect all bordered platforms -/datum/lift_master/proc/Rebuild_lift_plaform(obj/structure/industrial_lift/base_lift_platform) - add_lift_platforms(base_lift_platform) - var/list/possible_expansions = list(base_lift_platform) - while(possible_expansions.len) - for(var/b in possible_expansions) - var/obj/structure/industrial_lift/borderline = b - var/list/result = borderline.lift_platform_expansion(src) - if(length(result)) - for(var/p in result) - if(lift_platforms.Find(p)) - continue - var/obj/structure/industrial_lift/lift_platform = p - add_lift_platforms(lift_platform) - possible_expansions |= lift_platform - possible_expansions -= borderline - -/** - * 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(LOCKED) - for(var/p in lift_platforms) - var/obj/structure/industrial_lift/lift_platform = p - lift_platform.travel(going) - set_controls(UNLOCKED) - -/** - * 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, z, gliding_amount = 8) - var/max_x = 1 - var/max_y = 1 - var/min_x = world.maxx - var/min_y = world.maxy - - - set_controls(LOCKED) - for(var/p in lift_platforms) - var/obj/structure/industrial_lift/lift_platform = p - max_x = max(max_x, lift_platform.x) - max_y = max(max_y, lift_platform.y) - min_x = min(min_x, lift_platform.x) - min_y = min(min_y, lift_platform.y) - - //This must be safe way to border tile to tile move of bordered platforms, that excludes platform overlapping. - if( going & WEST ) - //Go along the X axis from min to max, from left to right - for(var/x in min_x to max_x) - if( going & NORTH ) - //Go along the Y axis from max to min, from up to down - for(var/y in max_y to min_y step -1) - var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) - lift_platform?.travel(going, gliding_amount) - else - //Go along the Y axis from min to max, from down to up - for(var/y in min_y to max_y) - var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) - lift_platform?.travel(going, gliding_amount) - else - //Go along the X axis from max to min, from right to left - for(var/x in max_x to min_x step -1) - if( going & NORTH ) - //Go along the Y axis from max to min, from up to down - for(var/y in max_y to min_y step -1) - var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) - lift_platform?.travel(going, gliding_amount) - else - //Go along the Y axis from min to max, from down to up - for(var/y in min_y to max_y) - var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) - lift_platform?.travel(going, gliding_amount) - set_controls(UNLOCKED) - -///Check destination turfs -/datum/lift_master/proc/Check_lift_move(check_dir) - for(var/l in lift_platforms) - var/obj/structure/industrial_lift/lift_platform = l - var/turf/T = get_step_multiz(lift_platform, check_dir) - if(!T)//the edges of multi-z maps - return FALSE - if(check_dir == UP && !istype(T, /turf/open/openspace)) // We don't want to go through the ceiling! - return FALSE - if(check_dir == DOWN && !istype(get_turf(lift_platform), /turf/open/openspace)) // No going through the floor! - return FALSE - return TRUE - -/** - * Sets all lift parts's controls_locked variable. Used to prevent moving mid movement, or cooldowns. - */ -/datum/lift_master/proc/set_controls(state) - for(var/l in lift_platforms) - var/obj/structure/industrial_lift/lift_platform = l - lift_platform.controls_locked = state - -GLOBAL_LIST_EMPTY(lifts) -/obj/structure/industrial_lift - name = "lift platform" - desc = "A lightweight lift platform. It moves up and down." - icon = 'icons/obj/smooth_structures/catwalk.dmi' - icon_state = "catwalk-0" - base_icon_state = "catwalk" - density = FALSE - anchored = TRUE - armor = list(MELEE = 50, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, FIRE = 80, ACID = 50) - max_integrity = 50 - layer = LATTICE_LAYER //under pipes - plane = FLOOR_PLANE - smoothing_flags = SMOOTH_BITMASK - smoothing_groups = list(SMOOTH_GROUP_INDUSTRIAL_LIFT) - canSmoothWith = list(SMOOTH_GROUP_INDUSTRIAL_LIFT) - obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN - - var/id = null //ONLY SET THIS TO ONE OF THE LIFT'S PARTS. THEY'RE CONNECTED! ONLY ONE NEEDS THE SIGNAL! - var/pass_through_floors = FALSE //if true, the elevator works through floors - var/controls_locked = FALSE //if true, the lift cannot be manually moved. - var/list/atom/movable/lift_load //things to move - var/datum/lift_master/lift_master_datum //control from - -/obj/structure/industrial_lift/Initialize(mapload) - . = ..() - GLOB.lifts.Add(src) - var/static/list/loc_connections = list( - COMSIG_ATOM_EXITED =.proc/UncrossedRemoveItemFromLift, - COMSIG_ATOM_ENTERED = .proc/AddItemOnLift, - COMSIG_ATOM_INITIALIZED_ON = .proc/AddItemOnLift, - ) - AddElement(/datum/element/connect_loc, loc_connections) - RegisterSignal(src, COMSIG_MOVABLE_BUMP, .proc/GracefullyBreak) - - if(!lift_master_datum) - lift_master_datum = new(src) - - -/obj/structure/industrial_lift/proc/UncrossedRemoveItemFromLift(datum/source, atom/movable/gone, direction) - SIGNAL_HANDLER - RemoveItemFromLift(gone) - -/obj/structure/industrial_lift/proc/RemoveItemFromLift(atom/movable/potential_rider) - SIGNAL_HANDLER - if(!(potential_rider in lift_load)) - return - if(isliving(potential_rider) && HAS_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED)) - REMOVE_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT) - LAZYREMOVE(lift_load, potential_rider) - UnregisterSignal(potential_rider, COMSIG_PARENT_QDELETING) - -/obj/structure/industrial_lift/proc/AddItemOnLift(datum/source, atom/movable/AM) - SIGNAL_HANDLER - if(istype(AM, /obj/structure/fluff/tram_rail) || AM.invisibility == INVISIBILITY_ABSTRACT) //prevents the tram from stealing things like landmarks - return - if(AM in lift_load) - return - if(isliving(AM) && !HAS_TRAIT(AM, TRAIT_CANNOT_BE_UNBUCKLED)) - ADD_TRAIT(AM, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT) - LAZYADD(lift_load, AM) - RegisterSignal(AM, COMSIG_PARENT_QDELETING, .proc/RemoveItemFromLift) - -/** - * Signal for when the tram runs into a field of which it cannot go through. - * Stops the train's travel fully, sends a message, and destroys the train. - * Arguments: - * bumped_atom - The atom this tram bumped into - */ -/obj/structure/industrial_lift/proc/GracefullyBreak(atom/bumped_atom) - SIGNAL_HANDLER - - if(istype(bumped_atom, /obj/machinery/field)) - return - - bumped_atom.visible_message(span_userdanger("[src] crashes into the field violently!")) - for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms) - tram_part.travel_distance = 0 - tram_part.set_travelling(FALSE) - if(prob(15) || locate(/mob/living) in tram_part.lift_load) //always go boom on people on the track - explosion(tram_part, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib - qdel(tram_part) - -/obj/structure/industrial_lift/proc/lift_platform_expansion(datum/lift_master/lift_master_datum) - . = list() - for(var/direction in GLOB.cardinals) - var/obj/structure/industrial_lift/neighbor = locate() in get_step(src, direction) - if(!neighbor) - continue - . += neighbor - -/obj/structure/industrial_lift/proc/travel(going, gliding_amount = 8) - var/list/things_to_move = LAZYCOPY(lift_load) - var/turf/destination - if(!isturf(going)) - destination = get_step_multiz(src, going) - else - destination = going - ///handles any special interactions objects could have with the lift/tram, handled on the item itself - SEND_SIGNAL(destination, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move) - - if(istype(destination, /turf/closed/wall)) - var/turf/closed/wall/C = destination - 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(going == DOWN) - for(var/mob/living/crushed in destination.contents) - to_chat(crushed, span_userdanger("You are crushed by [src]!")) - crushed.gib(FALSE,FALSE,FALSE)//the nicest kind of gibbing, keeping everything intact. - - else if(going != UP) //can't really crush something upwards - var/atom/throw_target = get_edge_target_turf(src, turn(going, pick(45, -45))) //finds a spot to throw the victim at for daring to be hit by a tram - for(var/obj/structure/victim_structure in destination.contents) - if(QDELETED(victim_structure)) - continue - if(!is_type_in_typecache(victim_structure, lift_master_datum.ignored_smashthroughs) && 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 - visible_message(span_danger("[src] violently rams [victim_structure] out of the way!")) - victim_structure.anchored = FALSE - victim_structure.take_damage(rand(20, 25)) - victim_structure.throw_at(throw_target, 200, 4) - for(var/obj/machinery/victim_machine in destination.contents) - if(QDELETED(victim_machine)) - continue - if(is_type_in_typecache(victim_machine, lift_master_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/collided in destination.contents) - if(is_type_in_typecache(collided, lift_master_datum.ignored_smashthroughs)) - continue - to_chat(collided, span_userdanger("[src] collides into you!")) - playsound(src, 'sound/effects/splat.ogg', 50, TRUE) - var/damage = rand(5, 10) - collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD) - collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST) - collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG) - collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG) - collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM) - collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM) - - if(QDELETED(collided)) //in case it was a mob that dels on death - continue - var/turf/T = get_turf(src) - T.add_mob_blood(collided) - - collided.throw_at() - //if going EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away - var/datum/callback/land_slam = new(collided, /mob/living/.proc/tram_slam_land) - collided.throw_at(throw_target, 200, 4, callback = land_slam) - set_glide_size(gliding_amount) - forceMove(destination) - for(var/atom/movable/thing as anything in things_to_move) - thing.set_glide_size(gliding_amount) //matches the glide size of the moving platform to stop them from jittering on it. - thing.forceMove(destination) - -/obj/structure/industrial_lift/proc/use(mob/living/user) - if(!isliving(user) || !in_range(src, user) || user.combat_mode) - 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(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) - return //nice try - 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) - return - lift_master_datum.MoveLift(UP, user) - show_fluff_message(TRUE, user) - use(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) - return - lift_master_datum.MoveLift(DOWN, user) - show_fluff_message(FALSE, user) - use(user) - if("Cancel") - return - add_fingerprint(user) - -/** - * 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/industrial_lift/proc/check_menu(mob/user, starting_loc) - if(user.incapacitated() || !user.Adjacent(src) || starting_loc != src.loc) - return FALSE - return TRUE - -/obj/structure/industrial_lift/attack_hand(mob/user, list/modifiers) - . = ..() - if(.) - return - use(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) - -/obj/structure/industrial_lift/attack_paw(mob/user, list/modifiers) - return use(user) - -/obj/structure/industrial_lift/attackby(obj/item/W, mob/user, params) - return use(user) - -/obj/structure/industrial_lift/attack_robot(mob/living/silicon/robot/R) - if(R.Adjacent(src)) - return use(R) - -/** - * 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. - * * 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) - user.visible_message(span_notice("[user] moves the lift upwards."), span_notice("You move the lift upwards.")) - else - user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards.")) - -/obj/structure/industrial_lift/Destroy() - GLOB.lifts.Remove(src) - QDEL_NULL(lift_master_datum) - var/list/border_lift_platforms = lift_platform_expansion() - moveToNullspace() - for(var/border_lift in border_lift_platforms) - lift_master_datum = new(border_lift) - return ..() - -/obj/structure/industrial_lift/debug - name = "transport platform" - desc = "A lightweight platform. It moves in any direction, except up and down." - color = "#5286b9ff" - -/obj/structure/industrial_lift/debug/use(mob/user) - if (!in_range(src, user)) - 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/check_menu, user, loc), require_near = TRUE, tooltips = FALSE) - if (!in_range(src, user)) - return // nice try - - switch(result) - if("NORTH") - lift_master_datum.MoveLiftHorizontal(NORTH, z) - use(user) - if("NORTHEAST") - lift_master_datum.MoveLiftHorizontal(NORTHEAST, z) - use(user) - if("EAST") - lift_master_datum.MoveLiftHorizontal(EAST, z) - use(user) - if("SOUTHEAST") - lift_master_datum.MoveLiftHorizontal(SOUTHEAST, z) - use(user) - if("SOUTH") - lift_master_datum.MoveLiftHorizontal(SOUTH, z) - use(user) - if("SOUTHWEST") - lift_master_datum.MoveLiftHorizontal(SOUTHWEST, z) - use(user) - if("WEST") - lift_master_datum.MoveLiftHorizontal(WEST, z) - use(user) - if("NORTHWEST") - lift_master_datum.MoveLiftHorizontal(NORTHWEST, z) - use(user) - if("Cancel") - return - - add_fingerprint(user) - -/obj/structure/industrial_lift/tram - name = "tram" - desc = "A tram for traversing the station." - icon = 'icons/turf/floors.dmi' - icon_state = "titanium_yellow" - base_icon_state = null - smoothing_flags = NONE - smoothing_groups = null - canSmoothWith = null - //kind of a centerpiece of the station, so pretty tough to destroy - resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - /// Set by the tram control console in late initialize - var/travelling = FALSE - var/travel_distance = 0 - /// For finding the landmark initially - should be the exact same as the landmark's destination id. - var/initial_id = "middle_part" - var/obj/effect/landmark/tram/from_where - var/travel_direction - -GLOBAL_LIST_EMPTY_TYPED(central_trams, /obj/structure/industrial_lift/tram/central) - -/obj/structure/industrial_lift/tram/Initialize(mapload) - . = ..() - return INITIALIZE_HINT_LATELOAD - -/obj/structure/industrial_lift/tram/central - var/tram_id = "tram_station" - -/obj/structure/industrial_lift/tram/central/Initialize(mapload) - . = ..() - if(!SStramprocess.can_fire) - SStramprocess.can_fire = TRUE - GLOB.central_trams += src - -/obj/structure/industrial_lift/tram/central/Destroy() - GLOB.central_trams -= src - return ..() - -/obj/structure/industrial_lift/tram/LateInitialize() - . = ..() - find_our_location() - - -/** - * Finds the location of the tram - * - * The initial_id is assumed to the be the landmark the tram is built on in the map - * and where the tram will set itself to be on roundstart. - * The central tram piece goes further into this by actually checking the contents of the turf its on - * for a tram landmark when it docks anywhere. This assures the tram actually knows where it is after docking, - * even in the worst cast scenario. - */ -/obj/structure/industrial_lift/tram/proc/find_our_location() - for(var/obj/effect/landmark/tram/our_location in GLOB.landmarks_list) - if(our_location.destination_id == initial_id) - from_where = our_location - break - -/obj/structure/industrial_lift/tram/proc/set_travelling(travelling) - if (src.travelling == travelling) - return - - 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/process(delta_time) - if(!travel_distance) - addtimer(CALLBACK(src, .proc/unlock_controls), 3 SECONDS) - return PROCESS_KILL - else - travel_distance-- - lift_master_datum.MoveLiftHorizontal(travel_direction, z, DELAY_TO_GLIDE_SIZE(SStramprocess.wait)) - -/** - * Handles moving the tram - * - * Tells the individual tram parts where to actually go and has an extra safety check - * incase multiple inputs get through, preventing conflicting directions and the tram - * literally ripping itself apart. The proc handles the first move before the subsystem - * takes over to keep moving it in process() - */ -/obj/structure/industrial_lift/tram/proc/tram_travel(obj/effect/landmark/tram/to_where) - if(to_where == from_where) - return - - visible_message(span_notice("[src] has been called to the [to_where]!")) - - lift_master_datum.set_controls(LOCKED) - travel_direction = get_dir(from_where, to_where) - travel_distance = get_dist(from_where, to_where) - //first movement is immediate - for(var/obj/structure/industrial_lift/tram/other_tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location. - if(other_tram_part.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs - return //we don't care to undo locked controls, though, as that will resolve itself - SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, from_where, to_where) - other_tram_part.set_travelling(TRUE) - other_tram_part.from_where = to_where - lift_master_datum.MoveLiftHorizontal(travel_direction, z, DELAY_TO_GLIDE_SIZE(SStramprocess.wait)) - travel_distance-- - - START_PROCESSING(SStramprocess, src) - -/** - * 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/industrial_lift/tram/proc/unlock_controls() - visible_message(span_notice("[src]'s controls are now unlocked.")) - for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location. - tram_part.set_travelling(FALSE) - lift_master_datum.set_controls(UNLOCKED) - -GLOBAL_LIST_EMPTY(tram_landmarks) - -/obj/effect/landmark/tram - name = "tram destination" //the tram buttons will mention this. - icon_state = "tram" - /// The ID of that particular destination. - var/destination_id - /// The ID of the tram that can travel to use - var/tram_id = "tram_station" - /// Icons for the tgui console to list out for what is at this location - var/list/tgui_icons = list() - -/obj/effect/landmark/tram/Initialize(mapload) - . = ..() - GLOB.tram_landmarks += src - -/obj/effect/landmark/tram/Destroy() - GLOB.tram_landmarks -= src - return ..() - - -/obj/effect/landmark/tram/left_part - name = "West Wing" - destination_id = "left_part" - tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel") - -/obj/effect/landmark/tram/middle_part - name = "Central Wing" - destination_id = "middle_part" - tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench") - -/obj/effect/landmark/tram/right_part - name = "East Wing" - destination_id = "right_part" - tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask") diff --git a/code/game/sound.dm b/code/game/sound.dm index 8580eaa14da..2e88ae160fe 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -23,120 +23,101 @@ ) environment = SOUND_ENVIRONMENT_NONE //Default to none so sounds without overrides dont get reverb -/*! playsound - -playsound is a proc used to play a 3D sound in a specific range. This uses SOUND_RANGE + extra_range to determine that. - -source - Origin of sound -soundin - Either a file, or a string that can be used to get an SFX -vol - The volume of the sound, excluding falloff and pressure affection. -vary - bool that determines if the sound changes pitch every time it plays -extrarange - modifier for sound range. This gets added on top of SOUND_RANGE -falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. -frequency - playback speed of audio -channel - The channel the sound is played at -pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space) -ignore_walls - Whether or not the sound can pass through walls. -falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. - -*/ - +/** + * playsound is a proc used to play a 3D sound in a specific range. This uses SOUND_RANGE + extra_range to determine that. + * + * source - Origin of sound. + * soundin - Either a file, or a string that can be used to get an SFX. + * vol - The volume of the sound, excluding falloff and pressure affection. + * vary - bool that determines if the sound changes pitch every time it plays. + * extrarange - modifier for sound range. This gets added on top of SOUND_RANGE. + * falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. + * frequency - playback speed of audio. + * channel - The channel the sound is played at. + * pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space). + * ignore_walls - Whether or not the sound can pass through walls. + * falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. + */ /proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE) if(isarea(source)) CRASH("playsound(): source is an area") var/turf/turf_source = get_turf(source) - if (!turf_source) + if (!turf_source || !soundin || !vol) return //allocate a channel if necessary now so its the same for everyone channel = channel || SSsounds.random_available_channel() - // Looping through the player list has the added bonus of working for mobs inside containers var/sound/S = sound(get_sfx(soundin)) var/maxdistance = SOUND_RANGE + extrarange var/source_z = turf_source.z var/list/listeners = SSmobs.clients_by_zlevel[source_z].Copy() + . = list()//output everything that successfully heard the sound + var/turf/above_turf = SSmapping.get_turf_above(turf_source) var/turf/below_turf = SSmapping.get_turf_below(turf_source) - if(!ignore_walls) //these sounds don't carry through walls - listeners = listeners & hearers(maxdistance,turf_source) + if(ignore_walls) - if(above_turf && istransparentturf(above_turf)) - listeners += hearers(maxdistance,above_turf) - - if(below_turf && istransparentturf(turf_source)) - listeners += hearers(maxdistance,below_turf) - - else if(above_turf && istransparentturf(above_turf)) listeners += SSmobs.clients_by_zlevel[above_turf.z] if(below_turf && istransparentturf(turf_source)) listeners += SSmobs.clients_by_zlevel[below_turf.z] - for(var/mob/listening_mob as anything in listeners) - if(get_dist(listening_mob, turf_source) <= maxdistance) - listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) - for(var/mob/listening_mob as anything in SSmobs.dead_players_by_zlevel[source_z]) - if(get_dist(listening_mob, turf_source) <= maxdistance) - listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) + for(var/mob/listening_mob as anything in listeners) + if(get_dist(listening_mob, turf_source) <= maxdistance) + listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) + . += listening_mob -/*! playsound + else //these sounds don't carry through walls + listeners = get_hearers_in_view(maxdistance, turf_source) -playsound_local is a proc used to play a sound directly on a mob from a specific turf. -This is called by playsound to send sounds to players, in which case it also gets the max_distance of that sound. + if(above_turf && istransparentturf(above_turf)) + listeners += get_hearers_in_view(maxdistance, above_turf) -turf_source - Origin of sound -soundin - Either a file, or a string that can be used to get an SFX -vol - The volume of the sound, excluding falloff -vary - bool that determines if the sound changes pitch every time it plays -frequency - playback speed of audio -falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. -channel - The channel the sound is played at -pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space) -max_distance - The peak distance of the sound, if this is a 3D sound -falloff_distance - Distance at which falloff begins, if this is a 3D sound -distance_multiplier - Can be used to multiply the distance at which the sound is heard + if(below_turf && istransparentturf(turf_source)) + listeners += get_hearers_in_view(maxdistance, below_turf) -*/ + for(var/mob/listening_mob in listeners | SSmobs.dead_players_by_zlevel[source_z])//observers always hear through walls + if(get_dist(listening_mob, turf_source) <= maxdistance) + listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) + . += listening_mob -/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/S, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE) +/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE) if(!client || !can_hear()) return - if(!S) - S = sound(get_sfx(soundin)) + if(!sound_to_use) + sound_to_use = sound(get_sfx(soundin)) - S.wait = 0 //No queue - S.channel = channel || SSsounds.random_available_channel() - S.volume = vol + sound_to_use.wait = 0 //No queue + sound_to_use.channel = channel || SSsounds.random_available_channel() + sound_to_use.volume = vol if(vary) if(frequency) - S.frequency = frequency + sound_to_use.frequency = frequency else - S.frequency = get_rand_frequency() + sound_to_use.frequency = get_rand_frequency() if(isturf(turf_source)) - var/turf/T = get_turf(src) + var/turf/turf_loc = get_turf(src) //sound volume falloff with distance - var/distance = get_dist(T, turf_source) - - distance *= distance_multiplier + var/distance = get_dist(turf_loc, turf_source) * distance_multiplier if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff. - S.volume -= (max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * S.volume + sound_to_use.volume -= (max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * sound_to_use.volume //https://www.desmos.com/calculator/sqdfl8ipgf if(pressure_affected) //Atmosphere affects sound var/pressure_factor = 1 - var/datum/gas_mixture/hearer_env = T.return_air() + var/datum/gas_mixture/hearer_env = turf_loc.return_air() var/datum/gas_mixture/source_env = turf_source.return_air() if(hearer_env && source_env) @@ -149,37 +130,35 @@ distance_multiplier - Can be used to multiply the distance at which the sound is if(distance <= 1) pressure_factor = max(pressure_factor, 0.15) //touching the source of the sound - S.volume *= pressure_factor + sound_to_use.volume *= pressure_factor //End Atmosphere affecting sound - if(S.volume <= 0) + if(sound_to_use.volume <= 0) return //No sound - var/dx = turf_source.x - T.x // Hearing from the right/left - S.x = dx * distance_multiplier - var/dz = turf_source.y - T.y // Hearing from infront/behind - S.z = dz * distance_multiplier - var/dy = (turf_source.z - T.z) * 5 * distance_multiplier // Hearing from above / below, multiplied by 5 because we assume height is further along coords. - S.y = dy + var/dx = turf_source.x - turf_loc.x // Hearing from the right/left + sound_to_use.x = dx * distance_multiplier + var/dz = turf_source.y - turf_loc.y // Hearing from infront/behind + sound_to_use.z = dz * distance_multiplier + var/dy = (turf_source.z - turf_loc.z) * 5 * distance_multiplier // Hearing from above / below, multiplied by 5 because we assume height is further along coords. + sound_to_use.y = dy - S.falloff = max_distance || 1 //use max_distance, else just use 1 as we are a direct sound so falloff isnt relevant. + sound_to_use.falloff = max_distance || 1 //use max_distance, else just use 1 as we are a direct sound so falloff isnt relevant. // Sounds can't have their own environment. A sound's environment will be: // 1. the mob's // 2. the area's (defaults to SOUND_ENVRIONMENT_NONE) if(sound_environment_override != SOUND_ENVIRONMENT_NONE) - S.environment = sound_environment_override + sound_to_use.environment = sound_environment_override else var/area/A = get_area(src) - S.environment = A.sound_environment + sound_to_use.environment = A.sound_environment - if(use_reverb && S.environment != SOUND_ENVIRONMENT_NONE) //We have reverb, reset our echo setting - //S.echo[3] = 0 //Room setting, 0 means normal reverb //ORIGINAL - //S.echo[4] = 0 //RoomHF setting, 0 means normal reverb. //ORIGINAL - S.echo[3] = -1300 //Room setting, 0 means normal reverb //SKYRAT EDIT CHANGE - S.echo[4] = -1300 //RoomHF setting, 0 means normal reverb. //SKYRAT EDIT CHANGE + if(use_reverb && sound_to_use.environment != SOUND_ENVIRONMENT_NONE) //We have reverb, reset our echo setting + sound_to_use.echo[3] = -1300 //Room setting, 0 means normal reverb //SKYRAT EDIT CHANGE + sound_to_use.echo[4] = -1300 //RoomHF setting, 0 means normal reverb. //SKYRAT EDIT CHANGE - SEND_SOUND(src, S) + SEND_SOUND(src, sound_to_use) /proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S) if(!S) diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm index 9763dcdf063..d0cd6021011 100644 --- a/code/game/turfs/open/_open.dm +++ b/code/game/turfs/open/_open.dm @@ -1,6 +1,7 @@ /turf/open plane = FLOOR_PLANE - var/slowdown = 0 //negative for faster, positive for slower + ///negative for faster, positive for slower + var/slowdown = 0 var/footstep = null var/barefootstep = null @@ -241,10 +242,9 @@ /turf/open/proc/freeze_turf() for(var/obj/I in contents) - if(I.resistance_flags & FREEZE_PROOF) - continue - if(!(I.obj_flags & FROZEN)) - I.make_frozen_visual() + if(!HAS_TRAIT(I, TRAIT_FROZEN) && !(I.obj_flags & FREEZE_PROOF)) + I.AddElement(/datum/element/frozen) + for(var/mob/living/L in contents) if(L.bodytemperature <= 50) L.apply_status_effect(/datum/status_effect/freon) @@ -258,8 +258,7 @@ M.apply_water() wash(CLEAN_WASH) - for(var/am in src) - var/atom/movable/movable_content = am + for(var/atom/movable/movable_content as anything in src) if(ismopable(movable_content)) // Will have already been washed by the wash call above at this point. continue movable_content.wash(CLEAN_WASH) diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm index 9e0e0805f09..66cd6f96a11 100644 --- a/code/game/turfs/open/openspace.dm +++ b/code/game/turfs/open/openspace.dm @@ -32,6 +32,9 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr . = ..() overlays += GLOB.openspace_backdrop_one_for_all //Special grey square for projecting backdrop darkness filter on it. RegisterSignal(src, COMSIG_ATOM_INITIALIZED_ON, .proc/on_atom_created) + var/area/our_area = loc + if(istype(our_area, /area/space)) + force_no_gravity = TRUE return INITIALIZE_HINT_LATELOAD /turf/open/openspace/LateInitialize() diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm index 74b22b59d46..e5f74afcf44 100644 --- a/code/game/turfs/open/space/space.dm +++ b/code/game/turfs/open/space/space.dm @@ -22,6 +22,8 @@ bullet_bounce_sound = null vis_flags = VIS_INHERIT_ID //when this be added to vis_contents of something it be associated with something on clicking, important for visualisation of turf in openspace and interraction with openspace that show you turf. + force_no_gravity = TRUE + /turf/open/space/basic/New() //Do not convert to Initialize //This is used to optimize the map loader return diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 9e036ee9530..51511c38754 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -74,6 +74,8 @@ GLOBAL_LIST_EMPTY(station_turfs) /// but is now destroyed, this will preserve the value. /// See __DEFINES/construction.dm for RCD_MEMORY_*. var/rcd_memory + ///whether or not this turf forces movables on it to have no gravity (unless they themselves have forced gravity) + var/force_no_gravity = FALSE /// How pathing algorithm will check if this turf is passable by itself (not including content checks). By default it's just density check. /// WARNING: Currently to use a density shortcircuiting this does not support dense turfs with special allow through function @@ -363,8 +365,6 @@ GLOBAL_LIST_EMPTY(station_turfs) var/canPassSelf = CanPass(mover, get_dir(src, mover)) if(canPassSelf || (mover.movement_type & PHASING)) for(var/atom/movable/thing as anything in contents) - if(QDELETED(mover)) - return FALSE //We were deleted, do not attempt to proceed with movement. if(thing == mover || thing == mover.loc) // Multi tile objects and moving out of other objects continue if(!thing.Cross(mover)) @@ -385,14 +385,6 @@ GLOBAL_LIST_EMPTY(station_turfs) return (mover.movement_type & PHASING) return TRUE -/turf/open/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) - . = ..() - //melting - if(isobj(arrived) && air && air.temperature > T0C) - var/obj/O = arrived - if(O.obj_flags & FROZEN) - O.make_unfrozen() - // A proc in case it needs to be recreated or badmins want to change the baseturfs /turf/proc/assemble_baseturfs(turf/fake_baseturf_type) var/static/list/created_baseturf_lists = list() diff --git a/code/modules/assembly/doorcontrol.dm b/code/modules/assembly/doorcontrol.dm index b32ffee4ad6..7b254d9e5a2 100644 --- a/code/modules/assembly/doorcontrol.dm +++ b/code/modules/assembly/doorcontrol.dm @@ -185,34 +185,46 @@ if(cooldown) return cooldown = TRUE - var/obj/structure/industrial_lift/lift - for(var/l in GLOB.lifts) - var/obj/structure/industrial_lift/possible_lift = l - if(possible_lift.id != id || possible_lift.z == z || possible_lift.controls_locked) + 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_lift + + lift = possible_match break + if(!lift) addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 2 SECONDS) return - lift.visible_message(span_notice("[src] clinks and whirrs into automated motion, locking controls.")) - lift.lift_master_datum.set_controls(LOCKED) + + 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 - lift.z) + var/difference = abs(targetZ - target_z) ///Direction (up/down) needed to go to reach targetZ - var/direction = lift.z < targetZ ? UP : DOWN + 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.lift_master_datum.MoveLift(direction, null) - lift.visible_message(span_notice("[src] clicks, ready to be manually operated again.")) - lift.lift_master_datum.set_controls(UNLOCKED) + 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 @@ -221,17 +233,19 @@ desc = "A small device used to bring trams to you." ///for finding the landmark initially - should be the exact same as the landmark's destination id. var/initial_id + ///ID to link to allow us to link to one specific tram in the world + var/specific_lift_id = MAIN_STATION_TRAM ///this is our destination's landmark, so we only have to find it the first time. var/datum/weakref/to_where /obj/item/assembly/control/tram/Initialize(mapload) - . = ..() + ..() return INITIALIZE_HINT_LATELOAD /obj/item/assembly/control/tram/LateInitialize() . = ..() //find where the tram needs to go to (our destination). only needs to happen the first time - for(var/obj/effect/landmark/tram/our_destination as anything in GLOB.tram_landmarks) + for(var/obj/effect/landmark/tram/our_destination as anything in GLOB.tram_landmarks[specific_lift_id]) if(our_destination.destination_id == initial_id) to_where = WEAKREF(our_destination) break @@ -245,29 +259,27 @@ return cooldown = TRUE addtimer(VARSET_CALLBACK(src, cooldown, FALSE), 2 SECONDS) - var/obj/structure/industrial_lift/tram/tram_part - var/turf/current_turf = get_turf(src) - if(!current_turf) - return - for(var/atom/tram as anything in GLOB.central_trams) - if(tram.z != current_turf.z) - continue - tram_part = tram - break - if(!tram_part) + + var/datum/lift_master/tram/tram + for(var/datum/lift_master/tram/possible_match as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID]) + if(possible_match.specific_lift_id == specific_lift_id) + tram = possible_match + break + + if(!tram) say("The tram is not responding to call signals. Please send a technician to repair the internals of the tram.") return - if(tram_part.travelling) //in use - say("The tram is already travelling to [tram_part.from_where].") + if(tram.travelling) //in use + say("The tram is already travelling to [tram.from_where].") return if(!to_where) return var/obj/effect/landmark/tram/current_location = to_where.resolve() if(!current_location) return - if(tram_part.from_where == current_location) //already here + if(tram.from_where == current_location) //already here say("The tram is already here. Please board the tram and select a destination.") return say("The tram has been called to [current_location.name]. Please wait for its arrival.") - tram_part.tram_travel(current_location) + tram.tram_travel(current_location) diff --git a/code/modules/atmospherics/environmental/LINDA_fire.dm b/code/modules/atmospherics/environmental/LINDA_fire.dm index c36f4bbb26c..99196512a50 100644 --- a/code/modules/atmospherics/environmental/LINDA_fire.dm +++ b/code/modules/atmospherics/environmental/LINDA_fire.dm @@ -112,6 +112,7 @@ air_update_turf(FALSE, FALSE) var/static/list/loc_connections = list( COMSIG_ATOM_ENTERED = .proc/on_entered, + COMSIG_ATOM_ABSTRACT_ENTERED = .proc/on_entered, ) AddElement(/datum/element/connect_loc, loc_connections) diff --git a/code/modules/atmospherics/gasmixtures/gas_mixture.dm b/code/modules/atmospherics/gasmixtures/gas_mixture.dm index 95ed18a5efe..20f25223287 100644 --- a/code/modules/atmospherics/gasmixtures/gas_mixture.dm +++ b/code/modules/atmospherics/gasmixtures/gas_mixture.dm @@ -111,8 +111,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache()) if(volume) // to prevent division by zero var/cached_gases = gases TOTAL_MOLES(cached_gases, .) - . *= R_IDEAL_GAS_EQUATION * temperature / volume - return + return . * R_IDEAL_GAS_EQUATION * temperature / volume return 0 /// Calculate temperature in kelvins @@ -661,7 +660,7 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache()) /datum/gas_mixture/proc/gas_pressure_quadratic(a, b, c, lower_limit, upper_limit) var/solution if(!IS_INF_OR_NAN(a) && !IS_INF_OR_NAN(b) && !IS_INF_OR_NAN(c)) - solution = max(SolveQuadratic(a, b, c)) + solution = max(SolveQuadratic(a, b, c)) if(solution > lower_limit && solution < upper_limit) //SolveQuadratic can return empty lists so be careful here return solution stack_trace("Failed to solve pressure quadratic equation. A: [a]. B: [b]. C:[c]. Current value = [solution]. Expected lower limit: [lower_limit]. Expected upper limit: [upper_limit].") diff --git a/code/game/machinery/crossing_signal.dm b/code/modules/industrial_lift/crossing_signal.dm similarity index 88% rename from code/game/machinery/crossing_signal.dm rename to code/modules/industrial_lift/crossing_signal.dm index 80907a59c1d..b7bdc00792f 100644 --- a/code/game/machinery/crossing_signal.dm +++ b/code/modules/industrial_lift/crossing_signal.dm @@ -26,7 +26,7 @@ /// green, amber, or red. var/signal_state = XING_STATE_GREEN /// The ID of the tram we control - var/tram_id = "tram_station" + var/tram_id = MAIN_STATION_TRAM /// Weakref to the tram piece we control var/datum/weakref/tram_ref /// Proximity threshold for amber warning (slow people may be in danger) @@ -42,14 +42,14 @@ . = ..() find_tram() - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram_part = tram_ref?.resolve() if(tram_part) RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_travelling) /obj/machinery/crossing_signal/Destroy() . = ..() - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram_part = tram_ref?.resolve() if(tram_part) UnregisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING) @@ -67,8 +67,8 @@ * Locates tram parts in the lift global list after everything is done. */ /obj/machinery/crossing_signal/proc/find_tram() - for(var/obj/structure/industrial_lift/tram/central/tram as anything in GLOB.central_trams) - if(tram.tram_id != tram_id) + for(var/datum/lift_master/tram/tram as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID]) + if(tram.specific_lift_id != tram_id) continue tram_ref = WEAKREF(tram) break @@ -105,10 +105,10 @@ end_processing() /obj/machinery/crossing_signal/process() - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram = tram_ref?.resolve() // Check for stopped states. - if(!tram_part || !is_operational) + if(!tram || !is_operational) // Tram missing, or we lost power. // Tram missing is always safe (green) set_signal_state(XING_STATE_GREEN, force = !is_operational) @@ -116,26 +116,32 @@ use_power(active_power_usage) + var/obj/structure/industrial_lift/tram/tram_part = tram.return_closest_platform_to(src) + + if(QDELETED(tram_part)) + set_signal_state(XING_STATE_GREEN, force = !is_operational) + return PROCESS_KILL + // Everything will be based on position and travel direction var/signal_pos var/tram_pos var/tram_velocity_sign // 1 for positive axis movement, -1 for negative // Try to be agnostic about N-S vs E-W movement - if(tram_part.travel_direction & (NORTH|SOUTH)) + if(tram.travel_direction & (NORTH|SOUTH)) signal_pos = y tram_pos = tram_part.y - tram_velocity_sign = tram_part.travel_direction & NORTH ? 1 : -1 + tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1 else signal_pos = x tram_pos = tram_part.x - tram_velocity_sign = tram_part.travel_direction & EAST ? 1 : -1 + tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1 // How far away are we? negative if already passed. var/approach_distance = tram_velocity_sign * (signal_pos - tram_pos) // Check for stopped state. // Will kill the process since tram starting up will restart process. - if(!tram_part.travelling) + if(!tram.travelling) // if super close, show red anyway since tram could suddenly start moving if(abs(approach_distance) < red_distance_threshold) set_signal_state(XING_STATE_RED) diff --git a/code/modules/industrial_lift/industrial_lift.dm b/code/modules/industrial_lift/industrial_lift.dm new file mode 100644 index 00000000000..d7b3d9cfbec --- /dev/null +++ b/code/modules/industrial_lift/industrial_lift.dm @@ -0,0 +1,752 @@ +GLOBAL_LIST_EMPTY(lifts) + +/obj/structure/industrial_lift + name = "lift platform" + desc = "A lightweight lift platform. It moves up and down." + icon = 'icons/obj/smooth_structures/catwalk.dmi' + icon_state = "catwalk-0" + base_icon_state = "catwalk" + density = FALSE + anchored = TRUE + armor = list(MELEE = 50, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, FIRE = 80, ACID = 50) + max_integrity = 50 + layer = LATTICE_LAYER //under pipes + plane = FLOOR_PLANE + smoothing_flags = SMOOTH_BITMASK + smoothing_groups = list(SMOOTH_GROUP_INDUSTRIAL_LIFT) + canSmoothWith = list(SMOOTH_GROUP_INDUSTRIAL_LIFT) + obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN + appearance_flags = PIXEL_SCALE|KEEP_TOGETHER //no TILE_BOUND since we're potentially multitile + + ///ID used to determine what lift types we can merge with + var/lift_id = BASIC_LIFT_ID + + ///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/lift_load + ///lazylist of 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 + + ///what glide_size we set our moving contents to. + var/glide_size_override = 8 + ///lazy list of movables inside lift_load 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 + + ///master datum that controls our movement. in general /industrial_lift subtypes control moving themselves, and + /// /datum/lift_master instances control moving the entire tram and any behavior associated with that. + var/datum/lift_master/lift_master_datum + ///what subtype of /datum/lift_master to create for itself if no other platform on this tram has created one yet. + ///very important for some behaviors since + var/lift_master_type = /datum/lift_master + + ///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_multitile_platform = FALSE + +/obj/structure/industrial_lift/Initialize(mapload) + . = ..() + GLOB.lifts.Add(src) + + set_movement_registrations() + + //since lift_master datums find all connected platforms when an industrial lift first creates it and then + //sets those platforms' lift_master_datum to itself, this check will only evaluate to true once per tram platform + if(!lift_master_datum && lift_master_type) + lift_master_datum = new lift_master_type(src) + return INITIALIZE_HINT_LATELOAD + +/obj/structure/industrial_lift/LateInitialize() + //after everything is initialized the lift master can order everything + lift_master_datum.order_platforms_by_z_level() + +/obj/structure/industrial_lift/Destroy() + GLOB.lifts.Remove(src) + lift_master_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/industrial_lift/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/UncrossedRemoveItemFromLift) + RegisterSignal(turf_loc, list(COMSIG_ATOM_ENTERED,COMSIG_ATOM_INITIALIZED_ON), .proc/AddItemOnLift) + +///unset our movement registrations from turfs that no longer contain us (or every loc if turfs_to_unset is unspecified) +/obj/structure/industrial_lift/proc/unset_movement_registrations(list/turfs_to_unset) + var/static/list/registrations = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED, COMSIG_ATOM_INITIALIZED_ON) + for(var/turf/turf_loc as anything in turfs_to_unset || locs) + UnregisterSignal(turf_loc, registrations) + + +/obj/structure/industrial_lift/proc/UncrossedRemoveItemFromLift(datum/source, atom/movable/gone, direction) + SIGNAL_HANDLER + if(!(gone.loc in locs)) + RemoveItemFromLift(gone) + +/obj/structure/industrial_lift/proc/RemoveItemFromLift(atom/movable/potential_rider) + SIGNAL_HANDLER + if(!(potential_rider in lift_load)) + return + if(isliving(potential_rider) && HAS_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED)) + REMOVE_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT) + LAZYREMOVE(lift_load, potential_rider) + LAZYREMOVE(changed_gliders, potential_rider) + UnregisterSignal(potential_rider, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)) + +/obj/structure/industrial_lift/proc/AddItemOnLift(datum/source, atom/movable/new_lift_contents) + SIGNAL_HANDLER + var/static/list/blacklisted_types = typecacheof(list(/obj/structure/fluff/tram_rail, /obj/effect/decal/cleanable, /obj/structure/industrial_lift, /mob/camera)) + if(is_type_in_typecache(new_lift_contents, blacklisted_types) || new_lift_contents.invisibility == INVISIBILITY_ABSTRACT) //prevents the tram from stealing things like landmarks + return FALSE + if(new_lift_contents in lift_load) + return FALSE + + if(isliving(new_lift_contents) && !HAS_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED)) + ADD_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT) + LAZYADD(lift_load, new_lift_contents) + RegisterSignal(new_lift_contents, COMSIG_PARENT_QDELETING, .proc/RemoveItemFromLift) + + return TRUE + +///adds everything on our tile that can be added to our lift_load and initial_contents lists when we're created +/obj/structure/industrial_lift/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(AddItemOnLift(src, movable_contents)) + + var/datum/weakref/new_initial_contents = WEAKREF(movable_contents) + if(!new_initial_contents) + continue + + LAZYADD(initial_contents, new_initial_contents) + +///signal handler for COMSIG_MOVABLE_UPDATE_GLIDE_SIZE: when a movable in lift_load 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/industrial_lift/proc/on_changed_glide_size(atom/movable/moving_contents, new_glide_size) + SIGNAL_HANDLER + if(new_glide_size != glide_size_override) + LAZYADD(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 bottom left corner since thats assumed to be the loc of multitile objects +/obj/structure/industrial_lift/proc/create_multitile_platform(min_x, min_y, max_x, max_y, z) + + if(!(min_x && min_y && max_x && max_y && z)) + for(var/obj/structure/industrial_lift/other_lift as anything in lift_master_datum.lift_platforms) + if(other_lift.z != z) + continue + + min_x = min(min_x, other_lift.x) + max_x = max(max_x, other_lift.x) + + min_y = min(min_y, other_lift.y) + max_y = max(max_y, other_lift.y) + + var/turf/bottom_left_loc = locate(min_x, min_y, z) + var/obj/structure/industrial_lift/loc_corner_lift = locate() in bottom_left_loc + + if(!loc_corner_lift) + stack_trace("no lift in the bottom left corner of a lift level!") + return FALSE + + if(loc_corner_lift != src) + //the loc of a multitile object must always be the lower left corner + return loc_corner_lift.create_multitile_platform() + + 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 = world.icon_size * y + + for(var/x in first_x to last_x) + + var/x_pixel_offset = world.icon_size * x + + var/turf/lift_turf = locate(x + min_x, y + min_y, z) + + if(!lift_turf) + continue + + if(lift_turf in locs_to_skip) + continue + + var/obj/structure/industrial_lift/other_lift = locate() in lift_turf + + if(!other_lift) + continue + + locs_to_skip += other_lift.locs.Copy()//make sure we never go over multitile platforms multiple times + + other_lift.pixel_x = x_pixel_offset + other_lift.pixel_y = y_pixel_offset + + overlays += other_lift + + //now we vore all the other lifts connected to us on our z level + for(var/obj/structure/industrial_lift/other_lift in lift_master_datum.lift_platforms) + if(other_lift == src || other_lift.z != z) + continue + + lift_master_datum.lift_platforms -= other_lift + if(other_lift.lift_load) + LAZYOR(lift_load, other_lift.lift_load) + if(other_lift.initial_contents) + LAZYOR(initial_contents, other_lift.initial_contents) + + qdel(other_lift) + + lift_master_datum.multitile_platform = 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) + return TRUE + +///returns an unordered list of all lift platforms adjacent to us. used so our lift_master_datum can control all connected platforms. +///includes platforms directly above or below us as well. only includes platforms with an identical lift_id to our own. +/obj/structure/industrial_lift/proc/lift_platform_expansion(datum/lift_master/lift_master_datum) + . = list() + for(var/direction in GLOB.cardinals_multiz) + var/obj/structure/industrial_lift/neighbor = locate() in get_step_multiz(src, direction) + if(!neighbor || neighbor.lift_id != lift_id) + continue + . += neighbor + +///main proc for moving the lift in the direction [going]. handles horizontal and/or vertical movement for multi platformed lifts and multitile lifts. +/obj/structure/industrial_lift/proc/travel(going) + var/list/things_to_move = lift_load + var/turf/destination + if(!isturf(going)) + destination = get_step_multiz(src, going) + else + destination = going + going = get_dir_multiz(loc, going) + + var/x_offset = ROUND_UP(bound_width / 32) - 1 //how many tiles our horizontally farthest edge is from us + var/y_offset = ROUND_UP(bound_height / 32) - 1 //how many tiles our vertically farthest edge is from us + + //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/top_edge_y = destination.y + y_offset + + var/turf/top_right_corner = locate(min(world.maxx, back_edge_x), min(world.maxy, top_edge_y), destination.z) + + var/list/dest_locs = block( + destination, + top_right_corner + ) + + var/list/entering_locs = dest_locs - locs + var/list/exited_locs = locs - dest_locs + + if(going == DOWN) + 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) + + 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. + + 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) + 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) + + 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 + var/datum/lift_master/tram/our_lift = lift_master_datum + var/collision_lethality = our_lift.collision_lethality + + 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/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)) + if(get_dist(dest_turf, client_mob) <= 8) + shake_camera(client_mob, 2, 3) + + playsound(collided_wall, 'sound/effects/meteorimpact.ogg', 100, TRUE) + + for(var/obj/structure/victim_structure in dest_turf.contents) + if(QDELING(victim_structure)) + continue + if(!is_type_in_typecache(victim_structure, lift_master_datum.ignored_smashthroughs) && 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(going, 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, lift_master_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/collided in dest_turf.contents) + if(lift_master_datum.ignored_smashthroughs[collided.type]) + continue + to_chat(collided, span_userdanger("[src] collides into you!")) + playsound(src, 'sound/effects/splat.ogg', 50, TRUE) + var/damage = rand(5, 10) * collision_lethality + collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD) + collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST) + collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG) + collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG) + collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM) + collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM) + + if(QDELETED(collided)) //in case it was a mob that dels on death + continue + if(!throw_target) + throw_target = get_edge_target_turf(src, turn(going, pick(45, -45))) + + var/turf/T = get_turf(collided) + T.add_mob_blood(collided) + + collided.throw_at() + //if going EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away + var/datum/callback/land_slam = new(collided, /mob/living/.proc/tram_slam_land) + collided.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality, callback = land_slam) + + unset_movement_registrations(exited_locs) + group_move(things_to_move, going) + set_movement_registrations(entering_locs) + +///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/industrial_lift/proc/group_move(list/atom/movable/movers, movement_direction) + if(movement_direction == NONE) + stack_trace("an industrial lift was told to move to somewhere it already is!") + return FALSE + + var/turf/our_dest = get_step(src, movement_direction) + + var/area/our_area = get_area(src) + var/area/their_area = get_area(our_dest) + var/different_areas = our_area != their_area + var/turf/mover_old_loc + + 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(mover.glide_size != glide_size_override) + mover.set_glide_size(glide_size_override) + + LAZYREMOVE(changed_gliders, mover) + + if(different_areas) + for(var/atom/movable/mover as anything in movers) + if(QDELETED(mover)) + movers -= mover + continue + + //we dont need to call Entered() and Exited() for origin and destination here for each mover because + //all of them are considered to be on top of us, so the turfs and anything on them can only perceive us, + //which is why the platform itself uses forceMove() + mover_old_loc = mover.loc + + our_area.Exited(mover, movement_direction) + mover.loc = get_step(mover, movement_direction) + their_area.Entered(mover, movement_direction) + + mover.Moved(mover_old_loc, movement_direction, TRUE, null, FALSE) + + else + for(var/atom/movable/mover as anything in movers) + if(QDELETED(mover)) + movers -= mover + continue + + mover_old_loc = mover.loc + mover.loc = get_step(mover, movement_direction) + + 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. + * used by an admin via calling reset_lift_contents() on our lift_master_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/industrial_lift/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(lift_load) <= consider_anything_past) + return FALSE + + ///list of resolve()'d initial_contents that are still in lift_load + 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 lift_load)) + 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)) + 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 + +/obj/structure/industrial_lift/proc/use(mob/living/user) + if(!isliving(user) || !in_range(src, user) || user.combat_mode) + 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) + return //nice try + 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) + return + lift_master_datum.MoveLift(UP, user) + show_fluff_message(TRUE, user) + use(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) + return + lift_master_datum.MoveLift(DOWN, user) + show_fluff_message(FALSE, user) + use(user) + if("Cancel") + return + add_fingerprint(user) + +/** + * 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/industrial_lift/proc/check_menu(mob/user, starting_loc) + if(user.incapacitated() || !user.Adjacent(src) || starting_loc != src.loc) + return FALSE + return TRUE + +/obj/structure/industrial_lift/attack_hand(mob/user, list/modifiers) + . = ..() + if(.) + return + use(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) + +/obj/structure/industrial_lift/attack_paw(mob/user, list/modifiers) + return use(user) + +/obj/structure/industrial_lift/attackby(obj/item/W, mob/user, params) + return use(user) + +/obj/structure/industrial_lift/attack_robot(mob/living/silicon/robot/R) + if(R.Adjacent(src)) + return use(R) + +/** + * 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. + * * 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) + user.visible_message(span_notice("[user] moves the lift upwards."), span_notice("You move the lift upwards.")) + else + user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards.")) + +/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 + +/obj/structure/industrial_lift/debug/use(mob/user) + if (!in_range(src, user)) + 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/check_menu, user), require_near = TRUE, tooltips = FALSE) + if (!in_range(src, user)) + return // nice try + + switch(result) + if("NORTH") + lift_master_datum.MoveLiftHorizontal(NORTH, z) + use(user) + if("NORTHEAST") + lift_master_datum.MoveLiftHorizontal(NORTHEAST, z) + use(user) + if("EAST") + lift_master_datum.MoveLiftHorizontal(EAST, z) + use(user) + if("SOUTHEAST") + lift_master_datum.MoveLiftHorizontal(SOUTHEAST, z) + use(user) + if("SOUTH") + lift_master_datum.MoveLiftHorizontal(SOUTH, z) + use(user) + if("SOUTHWEST") + lift_master_datum.MoveLiftHorizontal(SOUTHWEST, z) + use(user) + if("WEST") + lift_master_datum.MoveLiftHorizontal(WEST, z) + use(user) + if("NORTHWEST") + lift_master_datum.MoveLiftHorizontal(NORTHWEST, z) + use(user) + if("Cancel") + return + + add_fingerprint(user) + +/obj/structure/industrial_lift/tram + name = "tram" + desc = "A tram for tramversing the station." + icon = 'icons/turf/floors.dmi' + icon_state = "titanium_yellow" + base_icon_state = null + smoothing_flags = NONE + smoothing_groups = null + canSmoothWith = null + //kind of a centerpiece of the station, so pretty tough to destroy + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + + lift_id = TRAM_LIFT_ID + lift_master_type = /datum/lift_master/tram + + /// Set by the tram control console in late initialize + var/travelling = FALSE + + //the following are only used to give to the lift_master datum when it's first created + + ///decisecond delay between horizontal movements. cannot make the tram move faster than 1 movement per world.tick_lag. only used to give to the lift_master + var/horizontal_speed = 0.5 + + create_multitile_platform = TRUE + +/obj/structure/industrial_lift/tram/AddItemOnLift(datum/source, atom/movable/AM) + . = ..() + if(travelling) + on_changed_glide_size(AM, AM.glide_size) + +/obj/structure/industrial_lift/tram/proc/set_travelling(travelling) + if (src.travelling == travelling) + return + + for(var/atom/movable/glider as anything in lift_load) + if(travelling) + glider.set_glide_size(glide_size_override) + RegisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, .proc/on_changed_glide_size) + else + LAZYREMOVE(changed_gliders, glider) + UnregisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE) + + 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 + +/** + * 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/industrial_lift/tram/proc/unlock_controls() + visible_message(span_notice("[src]'s controls are now unlocked.")) + for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location. + tram_part.set_travelling(FALSE) + lift_master_datum.set_controls(LIFT_PLATFORM_UNLOCKED) + +///debug proc to highlight the locs of the tram platform +/obj/structure/industrial_lift/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/clear_turfs, turfs, iterations), 1) + +/obj/structure/industrial_lift/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/clear_turfs, turfs, iterations), 1) diff --git a/code/modules/industrial_lift/lift_master.dm b/code/modules/industrial_lift/lift_master.dm new file mode 100644 index 00000000000..45c2ae3b81a --- /dev/null +++ b/code/modules/industrial_lift/lift_master.dm @@ -0,0 +1,397 @@ +///associative list of the form: list(lift_id = list(all lift_master datums attached to lifts of that type)) +GLOBAL_LIST_EMPTY(active_lifts_by_type) + +///coordinate and control movement across linked industrial_lift's. allows moving large single multitile platforms and many 1 tile platforms. +///also is capable of linking platforms across linked z levels +/datum/lift_master + ///the lift platforms we consider as part of this lift. ordered in order of lowest z level to highest z level after init. + ///(the sorting algorithm sucks btw) + var/list/obj/structure/industrial_lift/lift_platforms + + /// Typepath list of what to ignore smashing through, controls all lifts + var/static/list/ignored_smashthroughs = list( + /obj/machinery/power/supermatter_crystal, + /obj/structure/holosign, + /obj/machinery/field, + ) + + ///whether the lift handled by this lift_master datum is multitile as opposed to nxm platforms per z level + var/multitile_platform = FALSE + + ///taken from our lift platforms. if true we go through each z level of platforms and attempt to make the lowest left corner platform + ///into one giant multitile object the size of all other platforms on that z level. + var/create_multitile_platform = FALSE + + ///lift platforms have already been sorted in order of z level. + var/z_sorted = FALSE + + ///lift_id taken from our base lift platform, used to put us into GLOB.active_lifts_by_type + var/lift_id = BASIC_LIFT_ID + + ///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 + +/datum/lift_master/New(obj/structure/industrial_lift/lift_platform) + lift_id = lift_platform.lift_id + create_multitile_platform = lift_platform.create_multitile_platform + + Rebuild_lift_plaform(lift_platform) + ignored_smashthroughs = typecacheof(ignored_smashthroughs) + + LAZYADDASSOCLIST(GLOB.active_lifts_by_type, lift_id, src) + + for(var/obj/structure/industrial_lift/lift as anything in lift_platforms) + lift.add_initial_contents() + +/datum/lift_master/Destroy() + for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms) + lift_platform.lift_master_datum = null + lift_platforms = null + + LAZYREMOVEASSOC(GLOB.active_lifts_by_type, lift_id, src) + if(isnull(GLOB.active_lifts_by_type)) + GLOB.active_lifts_by_type = list()//im lazy + + return ..() + +/datum/lift_master/proc/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform) + if(new_lift_platform in lift_platforms) + return + for(var/obj/structure/industrial_lift/other_platform in new_lift_platform.loc) + if(other_platform != new_lift_platform) + stack_trace("there is more than one lift platform on a tile when a lift_master adds it. this causes problems") + qdel(other_platform) + + new_lift_platform.lift_master_datum = src + LAZYADD(lift_platforms, new_lift_platform) + RegisterSignal(new_lift_platform, COMSIG_PARENT_QDELETING, .proc/remove_lift_platforms) + + check_for_landmarks(new_lift_platform) + + if(z_sorted)//make sure we dont lose z ordering if we get additional platforms after init + order_platforms_by_z_level() + +/datum/lift_master/proc/remove_lift_platforms(obj/structure/industrial_lift/old_lift_platform) + SIGNAL_HANDLER + + if(!(old_lift_platform in lift_platforms)) + return + + old_lift_platform.lift_master_datum = null + LAZYREMOVE(lift_platforms, old_lift_platform) + UnregisterSignal(old_lift_platform, COMSIG_PARENT_QDELETING) + if(!length(lift_platforms)) + qdel(src) + +///Collect all bordered platforms via a simple floodfill algorithm. allows multiz trams because its funny +/datum/lift_master/proc/Rebuild_lift_plaform(obj/structure/industrial_lift/base_lift_platform) + add_lift_platforms(base_lift_platform) + var/list/possible_expansions = list(base_lift_platform) + + while(possible_expansions.len) + for(var/obj/structure/industrial_lift/borderline as anything in possible_expansions) + var/list/result = borderline.lift_platform_expansion(src) + if(length(result)) + for(var/obj/structure/industrial_lift/lift_platform as anything in result) + if(lift_platforms.Find(lift_platform)) + continue + + add_lift_platforms(lift_platform) + possible_expansions |= lift_platform + + possible_expansions -= borderline + +///check for any landmarks placed inside the locs of the given lift_platform +/datum/lift_master/proc/check_for_landmarks(obj/structure/industrial_lift/new_lift_platform) + SHOULD_CALL_PARENT(TRUE) + + for(var/turf/platform_loc as anything in new_lift_platform.locs) + var/obj/effect/landmark/lift_id/id_giver = locate() in platform_loc + + if(id_giver) + set_info_from_id_landmark(id_giver) + +///set vars and such given an overriding lift_id landmark +/datum/lift_master/proc/set_info_from_id_landmark(obj/effect/landmark/lift_id/landmark) + SHOULD_CALL_PARENT(TRUE) + + if(!istype(landmark, /obj/effect/landmark/lift_id))//lift_master subtypes can want differnet id's than the base type wants + return + + if(landmark.specific_lift_id) + specific_lift_id = landmark.specific_lift_id + + qdel(landmark) + +///orders the lift platforms in order of lowest z level to highest z level. +/datum/lift_master/proc/order_platforms_by_z_level() + //contains nested lists for every z level in the world. why? because its really easy to sort + var/list/platforms_by_z = list() + platforms_by_z.len = world.maxz + + for(var/z in 1 to world.maxz) + platforms_by_z[z] = list() + + for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms) + if(QDELETED(lift_platform) || !lift_platform.z) + lift_platforms -= lift_platform + continue + + platforms_by_z[lift_platform.z] += lift_platform + + if(create_multitile_platform) + for(var/list/z_list as anything in platforms_by_z) + if(!length(z_list)) + continue + + create_multitile_platform_for_z_level(z_list)//this will subtract all but one platform from the list + + var/list/output = list() + + for(var/list/z_list as anything in platforms_by_z) + output += z_list + + lift_platforms = output + + z_sorted = TRUE + +///goes through all platforms in the given list and finds the one in the lower left corner +/datum/lift_master/proc/create_multitile_platform_for_z_level(list/obj/structure/industrial_lift/platforms_in_z) + var/min_x = INFINITY + var/max_x = 0 + + var/min_y = INFINITY + var/max_y = 0 + + var/z = 0 + + for(var/obj/structure/industrial_lift/lift_to_sort as anything in platforms_in_z) + if(!z) + if(!lift_to_sort.z) + stack_trace("create_multitile_platform_for_z_level() was given a platform in nullspace or not on a turf!") + platforms_in_z -= lift_to_sort + continue + + z = lift_to_sort.z + + if(z != lift_to_sort.z) + stack_trace("create_multitile_platform_for_z_level() was given lifts on different z levels!") + platforms_in_z -= lift_to_sort + continue + + min_x = min(min_x, lift_to_sort.x) + max_x = max(max_x, lift_to_sort.x) + + min_y = min(min_y, lift_to_sort.y) + max_y = max(max_y, lift_to_sort.y) + + var/turf/lower_left_corner_loc = locate(min_x, min_y, z) + if(!lower_left_corner_loc) + CRASH("was unable to find a turf at the lower left corner of this z") + + var/obj/structure/industrial_lift/lower_left_corner_lift = locate() in lower_left_corner_loc + + if(!lower_left_corner_lift) + CRASH("there was no lift in the lower left corner of the given lifts") + + platforms_in_z.Cut() + platforms_in_z += lower_left_corner_lift//we want to change the list given to us not create a new one. so we do this + + lower_left_corner_lift.create_multitile_platform(min_x, min_y, max_x, max_y, z) + +///returns the closest lift to the specified atom, prioritizing lifts on the same z level. used for comparing distance +/datum/lift_master/proc/return_closest_platform_to(atom/comparison, allow_multiple_answers = FALSE) + if(!istype(comparison) || !comparison.z) + return FALSE + + var/list/obj/structure/industrial_lift/candidate_platforms = list() + + for(var/obj/structure/industrial_lift/platform as anything in lift_platforms) + if(platform.z == comparison.z) + candidate_platforms += platform + + var/obj/structure/industrial_lift/winner = candidate_platforms[1] + var/winner_distance = get_dist(comparison, winner) + + var/list/tied_winners = list(winner) + + for(var/obj/structure/industrial_lift/platform_to_sort as anything in candidate_platforms) + var/platform_distance = get_dist(comparison, platform_to_sort) + + if(platform_distance < winner_distance) + winner = platform_to_sort + winner_distance = platform_distance + + if(allow_multiple_answers) + tied_winners = list(winner) + + else if(platform_distance == winner_distance && allow_multiple_answers) + tied_winners += platform_to_sort + + if(allow_multiple_answers) + return tied_winners + + return winner + +///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 + if(isatom(atom_reference_OR_z_level_number)) + z = atom_reference_OR_z_level_number.z + + if(!isnum(z) || z < 0 || z > world.maxz) + return null + + var/list/platforms_in_z = list() + + for(var/obj/structure/industrial_lift/lift_to_check as anything in lift_platforms) + if(lift_to_check.z) + platforms_in_z += lift_to_check + + return platforms_in_z + +/** + * 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) + //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 + var/current_index = length(lift_platforms) + + while(current_index > 0) + platform_to_move = lift_platforms[current_index] + current_index-- + + platform_to_move.travel(going) + + else if(going == DOWN) + for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms) + lift_platform.travel(going) + set_controls(LIFT_PLATFORM_UNLOCKED) + +/** + * 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) + set_controls(LIFT_PLATFORM_LOCKED) + + if(multitile_platform) + for(var/obj/structure/industrial_lift/platform_to_move as anything in lift_platforms) + platform_to_move.travel(going) + + set_controls(LIFT_PLATFORM_UNLOCKED) + return + + var/max_x = 0 + var/max_y = 0 + var/max_z = 0 + var/min_x = world.maxx + var/min_y = world.maxy + var/min_z = world.maxz + + for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms) + max_z = max(max_z, lift_platform.z) + min_z = min(min_z, lift_platform.z) + + min_x = min(min_x, lift_platform.x) + max_x = max(max_x, lift_platform.x) + //this assumes that all z levels have identical horizontal bounding boxes + //but if youre still using a non multitile tram platform at this point + //then its your own problem. it wont runtime it will jsut be slower than it needs to be if this assumption isnt + //the case + + min_y = min(min_y, lift_platform.y) + max_y = max(max_y, lift_platform.y) + + for(var/z in min_z to max_z) + //This must be safe way to border tile to tile move of bordered platforms, that excludes platform overlapping. + if(going & WEST) + //Go along the X axis from min to max, from left to right + for(var/x in min_x to max_x) + if(going & NORTH) + //Go along the Y axis from max to min, from up to down + for(var/y in max_y to min_y step -1) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + + else if(going & SOUTH) + //Go along the Y axis from min to max, from down to up + for(var/y in min_y to max_y) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + + else + for(var/y in min_y to max_y) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + else + //Go along the X axis from max to min, from right to left + for(var/x in max_x to min_x step -1) + if(going & NORTH) + //Go along the Y axis from max to min, from up to down + for(var/y in max_y to min_y step -1) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + + else if (going & SOUTH) + for(var/y in min_y to max_y) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + + else + //Go along the Y axis from min to max, from down to up + for(var/y in min_y to max_y) + var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z)) + lift_platform?.travel(going) + + set_controls(LIFT_PLATFORM_UNLOCKED) + +///Check destination turfs +/datum/lift_master/proc/Check_lift_move(check_dir) + for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms) + for(var/turf/bound_turf in lift_platform.locs) + var/turf/T = get_step_multiz(lift_platform, check_dir) + if(!T)//the edges of multi-z maps + return FALSE + if(check_dir == UP && !istype(T, /turf/open/openspace)) // We don't want to go through the ceiling! + return FALSE + if(check_dir == DOWN && !istype(get_turf(lift_platform), /turf/open/openspace)) // No going through the floor! + return FALSE + return TRUE + +/** + * Sets all lift parts's controls_locked variable. Used to prevent moving mid movement, or cooldowns. + */ +/datum/lift_master/proc/set_controls(state) + controls_locked = state + +/** + * resets the contents of all platforms to their original state in case someone put a bunch of shit onto the tram. + * intended to be called by admins. passes all arguments to reset_contents() for each of our platforms. + * + * Arguments: + * * consider_anything_past - number. if > 0 our platforms will only handle foreign contents that exceed this number in each of their locs + * * foreign_objects - bool. if true our platforms will consider /atom/movable's that arent mobs as part of foreign contents + * * foreign_non_player_mobs - bool. if true our platforms consider mobs that dont have a mind to be foreign + * * consider_player_mobs - bool. if true our platforms consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well + */ +/datum/lift_master/proc/reset_lift_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE) + for(var/obj/structure/industrial_lift/lift_to_reset in lift_platforms) + lift_to_reset.reset_contents(consider_anything_past, foreign_objects, foreign_non_player_mobs, consider_player_mobs) + + return TRUE diff --git a/code/game/machinery/computer/tram_controls.dm b/code/modules/industrial_lift/tram_controls.dm similarity index 79% rename from code/game/machinery/computer/tram_controls.dm rename to code/modules/industrial_lift/tram_controls.dm index 957547569be..a55ff5b18f4 100644 --- a/code/game/machinery/computer/tram_controls.dm +++ b/code/modules/industrial_lift/tram_controls.dm @@ -6,12 +6,14 @@ circuit = /obj/item/circuitboard/computer/tram_controls flags_1 = NODECONSTRUCT_1 | SUPERMATTER_IGNORES_1 resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF - light_color = LIGHT_COLOR_GREEN - ///The ID of the tram we control - var/tram_id = "tram_station" + + light_range = 0 //we dont want to spam SSlighting with source updates every movement + ///Weakref to the tram piece we control var/datum/weakref/tram_ref + var/specific_lift_id = MAIN_STATION_TRAM + /obj/machinery/computer/tram_controls/Initialize(mapload, obj/item/circuitboard/C) . = ..() AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/tram_controls)) @@ -19,7 +21,6 @@ /obj/machinery/computer/tram_controls/LateInitialize() . = ..() - //find the tram, late so the tram is all... set up so when this is called? i'm seriously stupid and 90% of what i do consists of barely educated guessing :) find_tram() /** @@ -28,19 +29,17 @@ * Locates tram parts in the lift global list after everything is done. */ /obj/machinery/computer/tram_controls/proc/find_tram() - for(var/obj/structure/industrial_lift/tram/central/tram as anything in GLOB.central_trams) - if(tram.tram_id != tram_id) - continue - tram_ref = WEAKREF(tram) - break + for(var/datum/lift_master/lift as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID]) + if(lift.specific_lift_id == specific_lift_id) + tram_ref = WEAKREF(lift) /obj/machinery/computer/tram_controls/ui_state(mob/user) return GLOB.not_incapacitated_state /obj/machinery/computer/tram_controls/ui_status(mob/user,/datum/tgui/ui) - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram = tram_ref?.resolve() - if(tram_part?.travelling) + if(tram?.travelling) return UI_CLOSE if(!in_range(user, src) && !isobserver(user)) return UI_CLOSE @@ -54,11 +53,11 @@ ui.open() /obj/machinery/computer/tram_controls/ui_data(mob/user) - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram_lift = tram_ref?.resolve() var/list/data = list() - data["moving"] = tram_part?.travelling - data["broken"] = tram_part ? FALSE : TRUE - var/obj/effect/landmark/tram/current_loc = tram_part?.from_where + data["moving"] = tram_lift?.travelling + data["broken"] = tram_lift ? FALSE : TRUE + var/obj/effect/landmark/tram/current_loc = tram_lift?.from_where if(current_loc) data["tram_location"] = current_loc.name return data @@ -77,9 +76,7 @@ */ /obj/machinery/computer/tram_controls/proc/get_destinations() . = list() - for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks) - if(destination.tram_id != tram_id) - continue + for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id]) var/list/this_destination = list() this_destination["name"] = destination.name this_destination["dest_icons"] = destination.tgui_icons @@ -94,9 +91,7 @@ switch (action) if ("send") var/obj/effect/landmark/tram/to_where - for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks) - if(destination.tram_id != tram_id) - continue + for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id]) if(destination.destination_id == params["destination"]) to_where = destination break @@ -108,14 +103,13 @@ /// Attempts to sends the tram to the given destination /obj/machinery/computer/tram_controls/proc/try_send_tram(obj/effect/landmark/tram/to_where) - var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve() + var/datum/lift_master/tram/tram_part = tram_ref?.resolve() if(!tram_part) return FALSE - if(tram_part.travelling) - return FALSE - if(tram_part.controls_locked) // someone else started + if(tram_part.controls_locked) // someone else started already return FALSE tram_part.tram_travel(to_where) + visible_message("The tram has been called to [to_where.name]") return TRUE /obj/item/circuit_component/tram_controls @@ -147,12 +141,12 @@ . = ..() if (istype(shell, /obj/machinery/computer/tram_controls)) computer = shell - var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve() + var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve() RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_set_travelling) RegisterSignal(tram_part, COMSIG_TRAM_TRAVEL, .proc/on_tram_travel) /obj/item/circuit_component/tram_controls/unregister_usb_parent(atom/movable/shell) - var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve() + var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve() computer = null UnregisterSignal(tram_part, list(COMSIG_TRAM_SET_TRAVELLING, COMSIG_TRAM_TRAVEL)) return ..() @@ -168,9 +162,7 @@ return var/destination - for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks) - if(possible_destination.tram_id != computer.tram_id) - continue + for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks[computer.specific_lift_id]) if(possible_destination.name == new_destination.value) destination = possible_destination break diff --git a/code/modules/industrial_lift/tram_landmark.dm b/code/modules/industrial_lift/tram_landmark.dm new file mode 100644 index 00000000000..db855b58a5c --- /dev/null +++ b/code/modules/industrial_lift/tram_landmark.dm @@ -0,0 +1,47 @@ +GLOBAL_LIST_EMPTY(tram_landmarks) + +/obj/effect/landmark/tram + name = "tram destination" //the tram buttons will mention this. + icon_state = "tram" + + ///the id of the tram we're linked to. + var/specific_lift_id = MAIN_STATION_TRAM + /// The ID of that particular destination. + var/destination_id + /// Icons for the tgui console to list out for what is at this location + var/list/tgui_icons = list() + +/obj/effect/landmark/tram/Initialize(mapload) + . = ..() + LAZYADDASSOCLIST(GLOB.tram_landmarks, specific_lift_id, src) + +/obj/effect/landmark/tram/Destroy() + LAZYREMOVEASSOC(GLOB.tram_landmarks, specific_lift_id, src) + return ..() + + +/obj/effect/landmark/tram/left_part + name = "West Wing" + destination_id = "left_part" + tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel") + +/obj/effect/landmark/tram/middle_part + name = "Central Wing" + destination_id = "middle_part" + tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench") + +/obj/effect/landmark/tram/right_part + name = "East Wing" + destination_id = "right_part" + tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask") + +/** + * lift_id landmarks. used to map in specific_lift_id to trams. when the trams lift_master encounters one on a trams tile + * it sets its specific_lift_id to that landmark. allows you to have multiple trams and multiple controls linking to their specific tram + */ +/obj/effect/landmark/lift_id + name = "lift id setter" + icon_state = "lift_id" + + ///what specific id we give to the tram we're placed on, should explicitely set this if its a subtype, or weird things might happen + var/specific_lift_id = MAIN_STATION_TRAM diff --git a/code/modules/industrial_lift/tram_lift_master.dm b/code/modules/industrial_lift/tram_lift_master.dm new file mode 100644 index 00000000000..573e961a71d --- /dev/null +++ b/code/modules/industrial_lift/tram_lift_master.dm @@ -0,0 +1,174 @@ +/datum/lift_master/tram + + ///whether this tram is traveling across vertical and/or horizontal axis for some distance. not all lifts use this + var/travelling = FALSE + ///if we're travelling, what direction are we going + var/travel_direction = NONE + ///if we're travelling, how far do we have to go + var/travel_distance = 0 + + ///multiplier on how much damage/force the tram imparts on things it hits + var/collision_lethality = 1 + + /// reference to the destination landmark we consider ourselves "at". since we potentially span multiple z levels we dont actually + /// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this + /// and the destination landmark. + var/obj/effect/landmark/tram/from_where + + ///decisecond delay between horizontal movement. cannot make the tram move faster than 1 movement per world.tick_lag. + ///this var is poorly named its actually horizontal movement delay but whatever. + var/horizontal_speed = 0.5 + + ///version of horizontal_speed that gets set in init and is considered our base speed if our lift gets slowed down + var/base_horizontal_speed = 0.5 + + ///the world.time we should next move at. in case our speed is set to less than 1 movement per tick + var/next_move = INFINITY + + ///whether we have been slowed down automatically + var/slowed_down = FALSE + + ///how many times we moved while costing more than SStramprocess.max_time milliseconds per movement. + ///if this exceeds SStramprocess.max_exceeding_moves + var/times_exceeded = 0 + + ///how many times we moved while costing less than 0.5 * SStramprocess.max_time milliseconds per movement + var/times_below = 0 + +/datum/lift_master/tram/New(obj/structure/industrial_lift/tram/lift_platform) + . = ..() + horizontal_speed = lift_platform.horizontal_speed + base_horizontal_speed = lift_platform.horizontal_speed + + check_starting_landmark() + +/datum/lift_master/tram/vv_edit_var(var_name, var_value) + . = ..() + if(var_name == "base_horizontal_speed") + horizontal_speed = max(horizontal_speed, base_horizontal_speed) + +/datum/lift_master/tram/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform) + . = ..() + RegisterSignal(new_lift_platform, COMSIG_MOVABLE_BUMP, .proc/gracefully_break) + +/datum/lift_master/tram/check_for_landmarks(obj/structure/industrial_lift/tram/new_lift_platform) + . = ..() + for(var/turf/platform_loc as anything in new_lift_platform.locs) + var/obj/effect/landmark/tram/initial_destination = locate() in platform_loc + + if(initial_destination) + from_where = initial_destination + +/datum/lift_master/tram/proc/check_starting_landmark() + if(!from_where) + CRASH("a tram lift_master was initialized without any tram landmark to give it direction!") + + SStramprocess.can_fire = TRUE + + return TRUE + +/** + * Signal for when the tram runs into a field of which it cannot go through. + * Stops the train's travel fully, sends a message, and destroys the train. + * Arguments: + * bumped_atom - The atom this tram bumped into + */ +/datum/lift_master/tram/proc/gracefully_break(atom/bumped_atom) + SIGNAL_HANDLER + + if(istype(bumped_atom, /obj/machinery/field)) + return + + travel_distance = 0 + + bumped_atom.visible_message(span_userdanger("[src] crashes into the field violently!")) + for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) + tram_part.set_travelling(FALSE) + if(prob(15) || locate(/mob/living) in tram_part.lift_load) //always go boom on people on the track + explosion(tram_part, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib + qdel(tram_part) + +/** + * Handles moving the tram + * + * Tells the individual tram parts where to actually go and has an extra safety check + * incase multiple inputs get through, preventing conflicting directions and the tram + * literally ripping itself apart. all of the actual movement is handled by SStramprocess + */ +/datum/lift_master/tram/proc/tram_travel(obj/effect/landmark/tram/to_where) + if(to_where == from_where) + return + + travel_direction = get_dir(from_where, to_where) + travel_distance = get_dist(from_where, to_where) + from_where = to_where + set_travelling(TRUE) + set_controls(LIFT_PLATFORM_LOCKED) + SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, from_where, to_where) + + for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location. + if(tram_part.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs + return //we don't care to undo locked controls, though, as that will resolve itself + + tram_part.glide_size_override = DELAY_TO_GLIDE_SIZE(horizontal_speed) + tram_part.set_travelling(TRUE) + + next_move = world.time + horizontal_speed + + START_PROCESSING(SStramprocess, src) + +/datum/lift_master/tram/process(delta_time) + if(!travel_distance) + addtimer(CALLBACK(src, .proc/unlock_controls), 3 SECONDS) + return PROCESS_KILL + else if(world.time >= next_move) + var/start_time = TICK_USAGE + travel_distance-- + + MoveLiftHorizontal(travel_direction) + + var/duration = TICK_USAGE_TO_MS(start_time) + if(slowed_down) + if(duration <= (SStramprocess.max_time / 2)) + times_below++ + else + times_below = 0 + + if(times_below >= SStramprocess.max_cheap_moves) + horizontal_speed = base_horizontal_speed + slowed_down = FALSE + times_below = 0 + + else if(duration > SStramprocess.max_time) + times_exceeded++ + + if(times_exceeded >= SStramprocess.max_exceeding_moves) + message_admins("The tram at [ADMIN_JMP(lift_platforms[1])] is taking more than [SStramprocess.max_time] milliseconds per movement, halving its movement speed. if this continues to be a problem you can call reset_lift_contents() on the trams lift_master_datum to reset it to its original state and clear added objects") + horizontal_speed = base_horizontal_speed * 2 //halves its speed + slowed_down = TRUE + times_exceeded = 0 + else + times_exceeded = max(times_exceeded - 1, 0) + + next_move = world.time + horizontal_speed + +/** + * 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. + */ +/datum/lift_master/tram/proc/unlock_controls() + set_travelling(FALSE) + set_controls(LIFT_PLATFORM_UNLOCKED) + for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location. + tram_part.set_travelling(FALSE) + + +/datum/lift_master/tram/proc/set_travelling(new_travelling) + if(travelling == new_travelling) + return + + travelling = new_travelling + SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling) diff --git a/code/modules/industrial_lift/tram_override_objects.dm b/code/modules/industrial_lift/tram_override_objects.dm new file mode 100644 index 00000000000..7a348295a2d --- /dev/null +++ b/code/modules/industrial_lift/tram_override_objects.dm @@ -0,0 +1,39 @@ +/** + * the tram has a few objects mapped onto it at roundstart, by default many of those objects have unwanted properties + * for example grilles and windows have the atmos_sensitive element applied to them, which makes them register to + * themselves moving to re register signals onto the turf via connect_loc. this is bad and dumb since it makes the tram + * more expensive to move. + * + * if you map something on to the tram, make SURE if possible that it doesnt have anythign reacting to its own movement + * it will make the tram more expensive to move and we dont want that because we dont want to return to the days where + * the tram took a third of the tick per movement when its just carrying its default mapped in objects + */ +/obj/structure/grille/tram/Initialize(mapload) + . = ..() + RemoveElement(/datum/element/atmos_sensitive, mapload) + //atmos_sensitive applies connect_loc which 1. reacts to movement in order to 2. unregister and register signals to + //the old and new locs. we dont want that, pretend these grilles and windows are plastic or something idk + +/obj/structure/window/reinforced/shuttle/tram/Initialize(mapload, direct) + . = ..() + RemoveElement(/datum/element/atmos_sensitive, mapload) + +/obj/structure/shuttle/engine/propulsion/in_wall/tram + //if this has opacity, then every movement of the tram causes lighting updates + //DO NOT put something on the tram roundstart that has opacity, it WILL overload SSlighting + opacity = FALSE + +/obj/machinery/door/window/left/tram +/obj/machinery/door/window/right/tram + +/obj/machinery/door/window/left/tram/Initialize(mapload, set_dir, unres_sides) + . = ..() + RemoveElement(/datum/element/atmos_sensitive, mapload) + +/obj/machinery/door/window/right/tram/Initialize(mapload, set_dir, unres_sides) + . = ..() + RemoveElement(/datum/element/atmos_sensitive, mapload) + +MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/left/tram, 0) +MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/right/tram, 0) + diff --git a/code/game/objects/structures/tram_walls.dm b/code/modules/industrial_lift/tram_walls.dm similarity index 99% rename from code/game/objects/structures/tram_walls.dm rename to code/modules/industrial_lift/tram_walls.dm index 092b1d5e2e5..17844dbfa5d 100644 --- a/code/game/objects/structures/tram_walls.dm +++ b/code/modules/industrial_lift/tram_walls.dm @@ -10,7 +10,7 @@ base_icon_state = "wall" layer = LOW_OBJ_LAYER density = TRUE - opacity = TRUE + opacity = FALSE max_integrity = 100 smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS) diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm index dbcdf2a7420..1b7efcad8a0 100644 --- a/code/modules/instruments/songs/play_legacy.dm +++ b/code/modules/instruments/songs/play_legacy.dm @@ -87,5 +87,5 @@ L.apply_status_effect(/datum/status_effect/good_music) if(!(M?.client?.prefs?.toggles & SOUND_INSTRUMENTS)) continue - M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played) + M.playsound_local(source, null, volume * using_instrument.volume_multiplier, sound_to_use = music_played) // Could do environment and echo later but not for now diff --git a/code/modules/lighting/lighting_atom.dm b/code/modules/lighting/lighting_atom.dm index 4beac5b1dd2..f0487b86b1a 100644 --- a/code/modules/lighting/lighting_atom.dm +++ b/code/modules/lighting/lighting_atom.dm @@ -25,8 +25,8 @@ #undef NONSENSICAL_VALUE -// Will update the light (duh). -// Creates or destroys it if needed, makes it update values, makes sure it's got the correct source turf... +/// Will update the light (duh). +/// Creates or destroys it if needed, makes it update values, makes sure it's got the correct source turf... /atom/proc/update_light() set waitfor = FALSE if (QDELETED(src)) @@ -80,13 +80,6 @@ return recalculate_directional_opacity() - -/atom/movable/Moved(atom/OldLoc, Dir) - . = ..() - for (var/datum/light_source/light as anything in light_sources) // Cycle through the light sources on this atom and tell them to update. - light.source_atom.update_light() - - /atom/proc/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION) return diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm index ca600b7d8f2..f9c48fb87ce 100644 --- a/code/modules/lighting/lighting_source.dm +++ b/code/modules/lighting/lighting_source.dm @@ -40,10 +40,10 @@ /datum/light_source/New(atom/owner, atom/top) source_atom = owner // Set our new owner. - LAZYADD(source_atom.light_sources, src) + add_to_light_sources(source_atom.light_sources) top_atom = top if (top_atom != source_atom) - LAZYADD(top_atom.light_sources, src) + add_to_light_sources(top_atom.light_sources) source_turf = top_atom pixel_turf = get_turf_pixel(top_atom) || source_turf @@ -59,10 +59,10 @@ /datum/light_source/Destroy(force) remove_lum() if (source_atom) - LAZYREMOVE(source_atom.light_sources, src) + remove_from_light_sources(source_atom.light_sources) if (top_atom) - LAZYREMOVE(top_atom.light_sources, src) + remove_from_light_sources(top_atom.light_sources) if (needs_update) SSlighting.sources_queue -= src @@ -74,6 +74,35 @@ return ..() +///add this light source to new_atom_host's light_sources list. updating movement registrations as needed +/datum/light_source/proc/add_to_light_sources(atom/new_atom_host) + if(QDELETED(new_atom_host)) + return FALSE + + LAZYADD(new_atom_host.light_sources, src) + if(ismovable(new_atom_host) && new_atom_host == source_atom) + RegisterSignal(new_atom_host, COMSIG_MOVABLE_MOVED, .proc/update_host_lights) + return TRUE + +///remove this light source from old_atom_host's light_sources list, unsetting movement registrations +/datum/light_source/proc/remove_from_light_sources(atom/old_atom_host) + if(QDELETED(old_atom_host)) + return FALSE + + LAZYREMOVE(old_atom_host.light_sources, src) + if(ismovable(old_atom_host) && old_atom_host == source_atom) + UnregisterSignal(old_atom_host, COMSIG_MOVABLE_MOVED) + return TRUE + +///signal handler for when our host atom moves and we need to update our effects +/datum/light_source/proc/update_host_lights(atom/movable/host) + SIGNAL_HANDLER + + if(QDELETED(host)) + return + + host.update_light() + // Yes this doesn't align correctly on anything other than 4 width tabs. // If you want it to go switch everybody to elastic tab stops. // Actually that'd be great if you could! @@ -84,17 +113,17 @@ needs_update = level; \ -// This proc will cause the light source to update the top atom, and add itself to the update queue. +/// This proc will cause the light source to update the top atom, and add itself to the update queue. /datum/light_source/proc/update(atom/new_top_atom) // This top atom is different. if (new_top_atom && new_top_atom != top_atom) if(top_atom != source_atom && top_atom.light_sources) // Remove ourselves from the light sources of that top atom. - LAZYREMOVE(top_atom.light_sources, src) + remove_from_light_sources(top_atom.light_sources) top_atom = new_top_atom if (top_atom != source_atom) - LAZYADD(top_atom.light_sources, src) // Add ourselves to the light sources of our new top atom. + add_to_light_sources(top_atom.light_sources) EFFECT_UPDATE(LIGHTING_CHECK_UPDATE) diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm index 98aa475b8c2..330d3671a50 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm @@ -41,8 +41,9 @@ if(hit_object.resistance_flags & FREEZE_PROOF) hit_object.visible_message(span_warning("[hit_object] is freeze-proof!")) return - if(!(hit_object.obj_flags & FROZEN)) - hit_object.make_frozen_visual() + if(HAS_TRAIT(hit_object, TRAIT_FROZEN)) + return + hit_object.AddElement(/datum/element/frozen) else if(isliving(hit_atom)) var/mob/living/hit_mob = hit_atom SSmove_manager.stop_looping(hit_mob) //stops them mid pathing even if they're stunimmune diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm index 7d12dd93c3c..1c6a8fb98e6 100644 --- a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm +++ b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm @@ -187,7 +187,7 @@ GLOBAL_DATUM(necropolis_gate, /obj/structure/necropolis_gate/legion_gate) for(var/mob/M in GLOB.player_list) if(M.z == z) to_chat(M, span_userdanger("Discordant whispers flood your mind in a thousand voices. Each one speaks your name, over and over. Something horrible has been released.")) - M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, S = legion_sound) + M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, sound_to_use = legion_sound) flash_color(M, flash_color = "#FF0000", flash_time = 50) var/mutable_appearance/release_overlay = mutable_appearance('icons/effects/effects.dmi', "legiondoor") notify_ghosts("Legion has been released in the [get_area(src)]!", source = src, alert_overlay = release_overlay, action = NOTIFY_JUMP, flashwindow = FALSE) diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm index 5d9b36ff0c0..b8ffceaa6e8 100644 --- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm +++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm @@ -495,19 +495,22 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999)) else to_chat(user, "No vacated rooms.") +/obj/effect/landmark/lift_id/hilbert + specific_lift_id = HILBERT_TRAM + /obj/effect/landmark/tram/left_part/hilbert + specific_lift_id = HILBERT_TRAM destination_id = "left_part_hilbert" - tram_id = "tram_hilbert" tgui_icons = list("Reception" = "briefcase", "Botany" = "leaf", "Chemistry" = "flask") /obj/effect/landmark/tram/middle_part/hilbert + specific_lift_id = HILBERT_TRAM destination_id = "middle_part_hilbert" - tram_id = "tram_hilbert" tgui_icons = list("Processing" = "cogs", "Xenobiology" = "paw") /obj/effect/landmark/tram/right_part/hilbert + specific_lift_id = HILBERT_TRAM destination_id = "right_part_hilbert" - tram_id = "tram_hilbert" tgui_icons = list("Ordnance" = "bullseye", "Office" = "user", "Dormitories" = "bed") /obj/item/keycard/hilbert diff --git a/code/modules/mapping/space_management/traits.dm b/code/modules/mapping/space_management/traits.dm index b53e2ee332a..4911d5316f1 100644 --- a/code/modules/mapping/space_management/traits.dm +++ b/code/modules/mapping/space_management/traits.dm @@ -57,18 +57,19 @@ /// Attempt to get the turf below the provided one according to Z traits /datum/controller/subsystem/mapping/proc/get_turf_below(turf/T) - if (!T) + if (!T || !initialized) return - var/offset = level_trait(T.z, ZTRAIT_DOWN) + var/offset = multiz_levels[T.z]["[DOWN]"] if (!offset) return - return locate(T.x, T.y, T.z + offset) + return locate(T.x, T.y, T.z - offset) /// Attempt to get the turf above the provided one according to Z traits /datum/controller/subsystem/mapping/proc/get_turf_above(turf/T) - if (!T) + if (!T || !initialized) return - var/offset = level_trait(T.z, ZTRAIT_UP) + + var/offset = multiz_levels[T.z]["[UP]"] if (!offset) return return locate(T.x, T.y, T.z + offset) diff --git a/code/modules/mapping/space_management/zlevel_manager.dm b/code/modules/mapping/space_management/zlevel_manager.dm index 2ad150ebc4b..1e5d2aafc88 100644 --- a/code/modules/mapping/space_management/zlevel_manager.dm +++ b/code/modules/mapping/space_management/zlevel_manager.dm @@ -26,6 +26,8 @@ // TODO: sleep here if the Z level needs to be cleared var/datum/space_level/S = new z_type(new_z, name, traits) z_list += S + generate_linkages_for_z_level(new_z) + calculate_z_level_gravity(new_z) adding_new_zlevel = FALSE SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_Z, S) return S diff --git a/code/modules/meteors/meteors.dm b/code/modules/meteors/meteors.dm index 2e843f2303b..ef7bf322613 100644 --- a/code/modules/meteors/meteors.dm +++ b/code/modules/meteors/meteors.dm @@ -221,7 +221,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust=1)) //for space dust eve continue var/dist = get_dist(M.loc, src.loc) shake_camera(M, dist > 20 ? 2 : 4, dist > 20 ? 1 : 3) - M.playsound_local(src.loc, null, 50, 1, random_frequency, 10, S = meteor_sound) + M.playsound_local(src.loc, null, 50, 1, random_frequency, 10, sound_to_use = meteor_sound) /////////////////////// //Meteor types diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index af1eae47790..f9d73ae1ac5 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -25,11 +25,11 @@ AddComponent(/datum/component/bloodysoles/feet) AddElement(/datum/element/ridable, /datum/component/riding/creature/human) AddElement(/datum/element/strippable, GLOB.strippable_human_items, /mob/living/carbon/human/.proc/should_strip) - GLOB.human_list += src var/static/list/loc_connections = list( COMSIG_ATOM_ENTERED = .proc/on_entered, ) AddElement(/datum/element/connect_loc, loc_connections) + GLOB.human_list += src SSopposing_force.give_opfor_button(src) //SKYRAT EDIT - OPFOR SYSTEM /mob/living/carbon/human/proc/setup_human_dna() @@ -98,11 +98,6 @@ . += "Chemical Storage: [changeling.chem_charges]/[changeling.total_chem_storage]" . += "Absorbed DNA: [changeling.absorbed_count]" -// called when something steps onto a human -/mob/living/carbon/human/proc/on_entered(datum/source, atom/movable/AM) - SIGNAL_HANDLER - spreadFire(AM) - /mob/living/carbon/human/reset_perspective(atom/new_eye, force_reset = FALSE) if(dna?.species?.prevent_perspective_change && !force_reset) // This is in case a species needs to prevent perspective changes in certain cases, like Dullahans preventing perspective changes when they're looking through their head. update_fullscreen() @@ -411,7 +406,10 @@ to_chat(usr, "Exploitable information: [target_general_records.fields["exploitable_records"]]") //SKYRAT EDIT END - ..() //end of this massive fucking chain. TODO: make the hud chain not spooky. - Yeah, great job doing that. +//called when something steps onto a human +/mob/living/carbon/human/proc/on_entered(datum/source, atom/movable/AM) + SIGNAL_HANDLER + spreadFire(AM) /mob/living/carbon/human/proc/canUseHUD() return (mobility_flags & MOBILITY_USE) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index d6ee7e4e1c1..2e4225cbd7f 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -926,7 +926,7 @@ var/mob/living/L = pulledby L.set_pull_offsets(src, pulledby.grab_state) - if(active_storage && !(CanReach(active_storage.parent,view_only = TRUE))) + if(active_storage && !((active_storage.parent in important_recursive_contents?[RECURSIVE_CONTENTS_ACTIVE_STORAGE]) || CanReach(active_storage.parent,view_only = TRUE))) active_storage.close(src) if(body_position == LYING_DOWN && !buckled && prob(getBruteLoss()*200/maxHealth)) @@ -1464,6 +1464,7 @@ GLOBAL_LIST_EMPTY(fire_appearances) var/datum/status_effect/fire_handler/fire_stacks/fire_status = has_status_effect(/datum/status_effect/fire_handler/fire_stacks) if(!fire_status || !fire_status.on_fire) return + remove_status_effect(/datum/status_effect/fire_handler/fire_stacks) /** diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 1388ffb6e59..6eac0915fad 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -210,3 +210,5 @@ var/native_fov = FOV_180_DEGREES //SKYRAT EDIT CHANGE /// Lazy list of FOV traits that will apply a FOV view when handled. var/list/fov_traits + ///what multiplicative slowdown we get from turfs currently. + var/current_turf_slowdown = 0 diff --git a/code/modules/mob/living/living_fov.dm b/code/modules/mob/living/living_fov.dm index 1bc54a66db7..4812c0bfea9 100644 --- a/code/modules/mob/living/living_fov.dm +++ b/code/modules/mob/living/living_fov.dm @@ -88,11 +88,20 @@ UNSETEMPTY(fov_traits) update_fov() +//did you know you can subtype /image and /mutable_appearance? +/image/fov_image + icon = 'icons/effects/fov/fov_effects.dmi' + layer = FOV_EFFECTS_LAYER + appearance_flags = RESET_COLOR | RESET_TRANSFORM + plane = FULLSCREEN_PLANE + /// Plays a visual effect representing a sound cue for people with vision obstructed by FOV or blindness -/proc/play_fov_effect(atom/center, range, icon_state, dir = SOUTH, ignore_self = FALSE, angle = 0) +/proc/play_fov_effect(atom/center, range, icon_state, dir = SOUTH, ignore_self = FALSE, angle = 0, list/override_list) var/turf/anchor_point = get_turf(center) - var/image/fov_image - for(var/mob/living/living_mob in get_hearers_in_view(range, center)) + var/image/fov_image/fov_image + var/list/clients_shown + + for(var/mob/living/living_mob in override_list || get_hearers_in_view(range, center)) var/client/mob_client = living_mob.client if(!mob_client) continue @@ -101,18 +110,22 @@ if(living_mob.in_fov(center, ignore_self)) continue if(!fov_image) //Make the image once we found one recipient to receive it - fov_image = image(icon = 'icons/effects/fov/fov_effects.dmi', icon_state = icon_state, loc = anchor_point) - fov_image.plane = FULLSCREEN_PLANE - fov_image.layer = FOV_EFFECTS_LAYER + fov_image = new() + fov_image.loc = anchor_point + fov_image.icon_state = icon_state fov_image.dir = dir - fov_image.appearance_flags = RESET_COLOR | RESET_TRANSFORM if(angle) var/matrix/matrix = new matrix.Turn(angle) fov_image.transform = matrix fov_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + LAZYADD(clients_shown, mob_client) + mob_client.images += fov_image - addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_image_from_client, fov_image, mob_client), 30) + //when added as an image mutable_appearances act identically. we just make it an MA becuase theyre faster to change appearance + + if(clients_shown) + addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_images_from_clients, fov_image, clients_shown), 30) /atom/movable/screen/fov_blocker icon = 'icons/effects/fov/field_of_view.dmi' diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm index 8b5ed28aec6..8ffb61b4d93 100644 --- a/code/modules/mob/living/living_movement.dm +++ b/code/modules/mob/living/living_movement.dm @@ -28,9 +28,12 @@ /mob/living/proc/update_turf_movespeed(turf/open/T) if(isopenturf(T)) - add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown, multiplicative_slowdown = T.slowdown) - else + if(T.slowdown != current_turf_slowdown) + add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown, multiplicative_slowdown = T.slowdown) + current_turf_slowdown = T.slowdown + else if(current_turf_slowdown) remove_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown) + current_turf_slowdown = 0 /mob/living/proc/update_pull_movespeed() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index cc683403ff6..f40050db76e 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -38,6 +38,7 @@ if(observers?.len) for(var/mob/dead/observe as anything in observers) observe.reset_perspective(null) + qdel(hud_used) QDEL_LIST(client_colours) ghostize() //False, since we're deleting it currently diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 0560fa3d77d..20238ddb4d1 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -241,3 +241,6 @@ var/thinking_indicator = FALSE /// User is thinking in character. Used to revert to thinking state after stop_typing var/thinking_IC = FALSE + + ///how much gravity is slowing us down + var/gravity_slowdown = 0 diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index d058604e464..0e92ca69035 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -70,9 +70,9 @@ next_move_dir_sub = 0 var/old_move_delay = move_delay move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick - if(!mob || !mob.loc) + if(!direct || !new_loc) return FALSE - if(!new_loc || !direct) + if(!mob?.loc) return FALSE if(mob.notransform) return FALSE //This is sota the goto stop mobs from moving var @@ -125,7 +125,9 @@ //We are now going to move var/add_delay = mob.cached_multiplicative_slowdown - mob.set_glide_size(DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? SQRT_2 : 1 ) )) // set it now in case of pulled objects + var/new_glide_size = DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? SQRT_2 : 1 ) ) + if(mob.glide_size != new_glide_size) + mob.set_glide_size(new_glide_size) // set it now in case of pulled objects //If the move was recent, count using old_move_delay //We want fractional behavior and all if(old_move_delay + world.tick_lag > world.time) @@ -144,10 +146,15 @@ if((direct & (direct - 1)) && mob.loc == new_loc) //moved diagonally successfully add_delay *= SQRT_2 + var/after_glide = 0 if(visual_delay) - mob.set_glide_size(visual_delay) + after_glide = visual_delay else - mob.set_glide_size(DELAY_TO_GLIDE_SIZE(add_delay)) + after_glide = DELAY_TO_GLIDE_SIZE(add_delay) + + if(after_glide != mob.glide_size) + mob.set_glide_size(after_glide) + move_delay += add_delay if(.) // If mob is null here, we deserve the runtime if(mob.throwing) @@ -365,10 +372,12 @@ /// Update the gravity status of this mob /mob/proc/update_gravity(has_gravity, override=FALSE) var/speed_change = max(0, has_gravity - STANDARD_GRAVITY) - if(!speed_change) + if(!speed_change && gravity_slowdown) remove_movespeed_modifier(/datum/movespeed_modifier/gravity) - else + gravity_slowdown = 0 + else if(gravity_slowdown != speed_change) add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/gravity, multiplicative_slowdown=speed_change) + gravity_slowdown = speed_change //bodypart selection verbs - Cyberboss //8: repeated presses toggles through head - eyes - mouth diff --git a/code/modules/mod/modules/modules_maint.dm b/code/modules/mod/modules/modules_maint.dm index da8d5e5578d..24e9beb8fc7 100644 --- a/code/modules/mod/modules/modules_maint.dm +++ b/code/modules/mod/modules/modules_maint.dm @@ -105,7 +105,7 @@ rave_screen = mod.wearer.add_client_colour(/datum/client_colour/rave) rave_screen.update_colour(rainbow_order[rave_number]) if(selection) - SEND_SOUND(mod.wearer, sound(selection.song_path, volume = 50, channel = CHANNEL_JUKEBOX)) + mod.wearer.playsound_local(get_turf(src), null, 50, channel = CHANNEL_JUKEBOX, sound_to_use = sound(selection.song_path), use_reverb = FALSE) /obj/item/mod/module/visor/rave/on_deactivation(display_message = TRUE, deleting = FALSE) . = ..() diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm index 41300a02847..ab4aec17ce3 100644 --- a/code/modules/power/gravitygenerator.dm +++ b/code/modules/power/gravitygenerator.dm @@ -3,7 +3,8 @@ // Gravity Generator // -GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding new gravity generators to the list, and keying it with the z level. +/// We will keep track of this by adding new gravity generators to the list, and keying it with the z level. +GLOBAL_LIST_EMPTY(gravity_generators) #define POWER_IDLE 0 #define POWER_UP 1 @@ -146,7 +147,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne var/charge_count = 100 var/current_overlay = null var/broken_state = 0 - var/setting = 1 //Gravity value when on + ///Gravity value when on + var/setting = 1 ///Station generator that spawns with gravity turned off. /obj/machinery/gravity_generator/main/station/off @@ -407,7 +409,7 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne M.update_gravity(M.has_gravity()) if(M.client) shake_camera(M, 15, 1) - M.playsound_local(T, null, 100, 1, 0.5, S = alert_sound) + M.playsound_local(T, null, 100, 1, 0.5, sound_to_use = alert_sound) //SKYRAT EDIT ADDITON BEGIN if(on) priority_announce("GRAVITATIONAL SYSTEMS OPERATIONAL", "Gravity Generator", ANNOUNCER_GRAVGENON) @@ -440,6 +442,7 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne GLOB.gravity_generators["[z]"] |= src else GLOB.gravity_generators["[z]"] -= src + SSmapping.calculate_z_level_gravity(z) /obj/machinery/gravity_generator/main/proc/change_setting(value) if(value != setting) diff --git a/code/modules/shuttle/on_move.dm b/code/modules/shuttle/on_move.dm index 5b2c22850c9..9a2866dfa1b 100644 --- a/code/modules/shuttle/on_move.dm +++ b/code/modules/shuttle/on_move.dm @@ -115,7 +115,7 @@ All ShuttleMove procs go here /atom/movable/proc/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) return move_mode -// Called on atoms to move the atom to the new location +/// Called on atoms to move the atom to the new location /atom/movable/proc/onShuttleMove(turf/newT, turf/oldT, list/movement_force, move_dir, obj/docking_port/stationary/old_dock, obj/docking_port/mobile/moving_dock) if(newT == oldT) // In case of in place shuttle rotation shenanigans. return diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm index 526fd875a68..96862680663 100644 --- a/code/modules/shuttle/shuttle.dm +++ b/code/modules/shuttle/shuttle.dm @@ -39,7 +39,7 @@ ///are we registered in SSshuttles? var/registered = FALSE - ///register to SSshuttles +///register to SSshuttles /obj/docking_port/proc/register() if(registered) WARNING("docking_port registered multiple times") @@ -47,7 +47,7 @@ registered = TRUE return - ///unregister from SSshuttles +///unregister from SSshuttles /obj/docking_port/proc/unregister() if(!registered) WARNING("docking_port unregistered multiple times") @@ -57,7 +57,7 @@ /obj/docking_port/proc/Check_id() return - //these objects are indestructible +//these objects are indestructible /obj/docking_port/Destroy(force) // unless you assert that you know what you're doing. Horrible things // may result. @@ -68,7 +68,7 @@ return QDEL_HINT_LETMELIVE /obj/docking_port/has_gravity(turf/T) - return FALSE + return TRUE /obj/docking_port/take_damage() return diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm index cee998c2836..07c7e57e026 100644 --- a/code/modules/unit_tests/create_and_destroy.dm +++ b/code/modules/unit_tests/create_and_destroy.dm @@ -103,6 +103,8 @@ ignore += typesof(/obj/item/toy/cards/cardhand) //Needs a holodeck area linked to it which is not guarenteed to exist and technically is supposed to have a 1:1 relationship with computer anyway. ignore += typesof(/obj/machinery/computer/holodeck) + //runtimes if not paired with a landmark + ignore += typesof(/obj/structure/industrial_lift) var/list/cached_contents = spawn_at.contents.Copy() var/baseturf_count = length(spawn_at.baseturfs) diff --git a/icons/effects/landmarks_static.dmi b/icons/effects/landmarks_static.dmi index 2082afc2fe025519f5a54842af83a258aba8640a..d6a26b050d59afefbf78017d4378b9deb0147215 100644 GIT binary patch literal 20455 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 literal 20347 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