diff --git a/.github/workflows/codeowner_reviews.yml b/.github/workflows/codeowner_reviews.yml
index f1cb174de37b..f81647e76eb6 100644
--- a/.github/workflows/codeowner_reviews.yml
+++ b/.github/workflows/codeowner_reviews.yml
@@ -14,16 +14,9 @@ jobs:
# Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it
- uses: actions/checkout@v6
- #Parse the Codeowner file on non draft PRs
- - name: CodeOwnersParser
- if: github.event.pull_request.draft == false
- id: CodeOwnersParser
- uses: tgstation/CodeOwnersParser@v1
-
- #Request reviews
+ #Request reviews
- name: Request reviews
- if: steps.CodeOwnersParser.outputs.owners != ''
- uses: tgstation/RequestReviewFromUser@v1
+ if: github.event.pull_request.draft == false
+ uses: tgstation/code-reviewers@main
with:
- separator: " "
- users: ${{ steps.CodeOwnersParser.outputs.owners }}
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/run_integration_tests.yml b/.github/workflows/run_integration_tests.yml
index 89f6cd236769..0f87ce2004d4 100644
--- a/.github/workflows/run_integration_tests.yml
+++ b/.github/workflows/run_integration_tests.yml
@@ -27,7 +27,7 @@ jobs:
# For example, `Run Tests (runtimestation; 515)`.
name: Run Tests (${{ inputs.major && format('{0}.{1}; ', inputs.major, inputs.minor) || '' }}${{ inputs.map }}; ${{ inputs.max_required_byond_client }})
runs-on: ubuntu-24.04
- timeout-minutes: 15
+ timeout-minutes: 55
steps:
- uses: actions/checkout@v6
- name: Restore BYOND from Cache
@@ -38,7 +38,7 @@ jobs:
- name: Download build outputs
uses: actions/download-artifact@v8
with:
- name: build-artifact-${{ inputs.major || env.BYOND_MAJOR }}-${{ inputs.minor || env.BYOND_MINOR}}
+ name: build-artifact-${{ inputs.major || env.BYOND_MAJOR }}-${{ inputs.minor || env.BYOND_MINOR }}
path: ./
- name: Setup database
env:
@@ -65,7 +65,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
- name: test_artifacts_${{ inputs.map }}_${{ inputs.major }}_${{ inputs.minor }}
+ name: test_artifacts_${{ inputs.map }}_${{ inputs.major || env.BYOND_MAJOR }}_${{ inputs.minor || env.BYOND_MINOR }}
path: data/screenshots_new/
retention-days: 1
- name: On test fail, write a step summary
diff --git a/_maps/map_files/debug/runtimestation.dmm b/_maps/map_files/debug/runtimestation.dmm
index 9779a6622eec..9196bf5d94cf 100644
--- a/_maps/map_files/debug/runtimestation.dmm
+++ b/_maps/map_files/debug/runtimestation.dmm
@@ -62,9 +62,9 @@
/turf/open/floor/iron,
/area/station/engineering/main)
"aq" = (
-/obj/machinery/camera/directional/north,
/obj/machinery/computer/monitor,
/obj/structure/cable,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/engineering/main)
"av" = (
@@ -286,13 +286,13 @@
/turf/closed/wall/r_wall,
/area/station/maintenance/aft)
"bM" = (
-/obj/machinery/camera/directional/north,
/obj/structure/table,
/obj/item/construction/rld,
/obj/item/construction/rcd/arcd,
/obj/effect/turf_decal/tile/blue/half/contrasted{
dir = 4
},
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/command/bridge)
"bO" = (
@@ -304,10 +304,10 @@
/turf/open/floor/iron/dark,
/area/station/medical/chemistry)
"bR" = (
-/obj/machinery/camera/directional/north,
/obj/machinery/power/apc/auto_name/directional/north,
/obj/structure/cable,
/obj/machinery/chem_heater/debug,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron/dark,
/area/station/medical/chemistry)
"bS" = (
@@ -323,8 +323,8 @@
/area/station/security/brig)
"bV" = (
/obj/effect/turf_decal/stripes/line,
-/obj/machinery/camera/directional/north,
/obj/machinery/status_display/evac/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/construction)
"bY" = (
@@ -642,11 +642,11 @@
/area/station/security/brig)
"dS" = (
/obj/machinery/atmospherics/components/tank/air,
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/plating,
/area/station/engineering/atmos)
"dT" = (
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron/dark,
/area/station/engineering/gravity_generator)
"dV" = (
@@ -1040,7 +1040,7 @@
/turf/closed/wall/r_wall,
/area/station/hallway/secondary/exit/departure_lounge)
"gn" = (
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/security/brig)
"gv" = (
@@ -1061,7 +1061,7 @@
/turf/open/floor/iron,
/area/station/hallway/secondary/exit/departure_lounge)
"gE" = (
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/hallway/secondary/entry)
"gF" = (
@@ -1081,7 +1081,7 @@
/area/station/cargo/storage)
"gI" = (
/obj/machinery/light/directional/north,
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/hallway/secondary/entry)
"gJ" = (
@@ -1181,7 +1181,7 @@
dir = 4
},
/obj/structure/extinguisher_cabinet/directional/north,
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron/white/corner{
dir = 1
},
@@ -1317,13 +1317,13 @@
/area/station/commons/storage/primary)
"pZ" = (
/obj/machinery/light/directional/north,
-/obj/machinery/camera/directional/north,
/obj/effect/turf_decal/plaque{
icon_state = "L11"
},
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/commons/storage/primary)
"qb" = (
@@ -2034,10 +2034,10 @@
/turf/open/floor/iron,
/area/station/construction)
"Hc" = (
-/obj/machinery/camera/directional/north,
/obj/effect/turf_decal/tile/blue/half/contrasted{
dir = 1
},
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/hallway/primary/central)
"Hg" = (
@@ -2057,7 +2057,7 @@
/turf/open/floor/iron,
/area/station/science)
"Ih" = (
-/obj/machinery/camera/directional/north,
+/obj/machinery/camera/autoname/directional/north,
/turf/open/floor/iron,
/area/station/hallway/secondary/exit/departure_lounge)
"Ir" = (
diff --git a/_maps/runtimestation_minimal.json b/_maps/runtimestation_minimal.json
index 983de67a36f9..455cac0139e9 100644
--- a/_maps/runtimestation_minimal.json
+++ b/_maps/runtimestation_minimal.json
@@ -15,7 +15,7 @@
"/datum/unit_test/traitor",
"/datum/unit_test/atmospherics_sanity",
"/datum/unit_test/maptest_mapload_space_verification",
- "/datum/unit_test/modify_fantasy_variable",
- "/datum/unit_test/create_and_destroy"
- ]
+ "/datum/unit_test/modify_fantasy_variable"
+ ],
+ "is_unit_test_map": true
}
diff --git a/_maps/shuttles/infiltrator_basic.dmm b/_maps/shuttles/infiltrator_basic.dmm
index 61f8ea231df2..468a10b865b7 100644
--- a/_maps/shuttles/infiltrator_basic.dmm
+++ b/_maps/shuttles/infiltrator_basic.dmm
@@ -19,9 +19,9 @@
/area/shuttle/syndicate/hallway)
"ae" = (
/obj/effect/spawner/structure/window/reinforced/plasma/plastitanium,
-/obj/machinery/door/poddoor/shutters{
- id = "syndieshutters";
- name = "Blast Shutters"
+/obj/machinery/door/poddoor/shutters/syndicate{
+ name = "Blast Shutters";
+ id = "syndieshutters"
},
/turf/open/floor/plating,
/area/shuttle/syndicate/bridge)
@@ -191,10 +191,6 @@
},
/area/shuttle/syndicate/airlock)
"aY" = (
-/obj/machinery/door/poddoor{
- id = "smindicate";
- name = "Outer Blast Door"
- },
/obj/docking_port/mobile/infiltrator,
/obj/structure/fans/tiny,
/obj/machinery/button/door/directional/north{
@@ -205,6 +201,11 @@
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 8
},
+/obj/machinery/door/poddoor/shutters/syndicate/indestructible{
+ id = "smindicate";
+ name = "Outer Blast Door";
+ dir = 4
+ },
/turf/open/floor/plating,
/area/shuttle/syndicate/airlock)
"aZ" = (
@@ -601,9 +602,7 @@
/obj/item/storage/box/zipties,
/obj/machinery/computer/security/telescreen/entertainment/directional/west,
/obj/structure/table/reinforced/plastitaniumglass,
-/turf/open/floor/iron/dark/smooth_corner{
- dir = 2
- },
+/turf/open/floor/iron/dark/smooth_corner,
/area/shuttle/syndicate/airlock)
"iE" = (
/obj/structure/closet/syndicate/nuclear,
diff --git a/_maps/templates/lazy_templates/nukie_base.dmm b/_maps/templates/lazy_templates/nukie_base.dmm
index f625537b0fe0..5175bac33bbe 100644
--- a/_maps/templates/lazy_templates/nukie_base.dmm
+++ b/_maps/templates/lazy_templates/nukie_base.dmm
@@ -7,14 +7,6 @@
"ae" = (
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"ah" = (
-/obj/effect/turf_decal/stripes/end{
- dir = 4
- },
-/obj/machinery/mech_bay_recharge_port,
-/obj/machinery/light/cold/directional/south,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"al" = (
/obj/effect/turf_decal/siding/wideplating/dark{
dir = 8
@@ -22,15 +14,6 @@
/obj/machinery/light/small/directional/north,
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
-"as" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark,
-/obj/structure/sign/poster/contraband/cybersun_six_hundred/directional/east,
-/obj/item/kirbyplants/random,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
-"au" = (
-/turf/open/floor/circuit/red,
-/area/centcom/syndicate_mothership/control)
"ay" = (
/obj/structure/table/reinforced,
/obj/item/papercutter,
@@ -54,6 +37,13 @@
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"aS" = (
+/obj/effect/turf_decal/siding/red/corner,
+/obj/item/folder/red,
+/obj/item/pen/red,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"aX" = (
/obj/machinery/shower/directional/south,
/turf/open/floor/iron/freezer,
@@ -77,10 +67,6 @@
/obj/structure/mirror/directional/east,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
-"bp" = (
-/obj/machinery/light/cold/directional/north,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"bB" = (
/obj/structure/railing{
dir = 4
@@ -123,6 +109,10 @@
},
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"bS" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark,
+/turf/closed/indestructible/syndicate,
+/area/centcom/syndicate_mothership/control)
"bT" = (
/obj/structure/flora/rock/pile/style_random,
/turf/open/misc/asteroid/snow/airless,
@@ -162,6 +152,15 @@
/obj/structure/cable,
/turf/open/floor/iron/smooth,
/area/centcom/syndicate_mothership/control)
+"cC" = (
+/obj/machinery/vending/cola,
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 4
+ },
+/turf/open/floor/iron/white/textured_large{
+ color = "#b51026"
+ },
+/area/centcom/syndicate_mothership/control)
"cF" = (
/obj/effect/baseturf_helper/asteroid/snow,
/turf/closed/indestructible/syndicate,
@@ -219,6 +218,20 @@
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"cW" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark,
+/obj/machinery/light/red/dim/directional/north,
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
+"cY" = (
+/obj/machinery/light/cold/directional/east,
+/obj/machinery/vending/snack/teal,
+/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/open/floor/catwalk_floor/iron_dark,
+/area/centcom/syndicate_mothership/control)
"cZ" = (
/obj/structure/mop_bucket/janitorialcart{
dir = 4
@@ -233,15 +246,6 @@
/obj/machinery/shower/directional/south,
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
-"dn" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 1
- },
-/obj/machinery/camera/autoname/directional/west{
- network = list("nukie")
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"dq" = (
/obj/machinery/light/small/directional/north,
/turf/open/floor/iron/smooth_half,
@@ -252,22 +256,16 @@
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"du" = (
-/obj/structure/chair/stool/directional/north,
-/obj/effect/landmark/start/nukeop_base,
-/obj/structure/sign/poster/contraband/donk_co/directional/south,
-/turf/open/floor/wood/tile,
-/area/centcom/syndicate_mothership/control)
-"dw" = (
-/obj/structure/table/reinforced,
-/obj/machinery/recharger,
-/turf/open/floor/carpet,
-/area/centcom/syndicate_mothership/control)
"dx" = (
/obj/machinery/shower/directional/east,
/obj/effect/turf_decal/stripes/box,
/turf/open/floor/mineral/titanium/tiled,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
+"dB" = (
+/obj/machinery/recharger,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/carpet,
+/area/centcom/syndicate_mothership/control)
"dF" = (
/obj/machinery/light/cold/directional/west,
/obj/structure/table/glass/plasmaglass,
@@ -467,6 +465,12 @@
/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
+"eM" = (
+/obj/effect/turf_decal/siding/red,
+/obj/machinery/recharger,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"fa" = (
/obj/effect/turf_decal/stripes/line,
/obj/effect/turf_decal/stripes/line{
@@ -478,15 +482,6 @@
},
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
-"fb" = (
-/obj/structure/chair/sofa/bench/left{
- dir = 4
- },
-/obj/machinery/camera/autoname/directional/north{
- network = list("nukie")
- },
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"fk" = (
/turf/open/floor/circuit/red/off,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
@@ -497,6 +492,15 @@
/obj/machinery/portable_atmospherics/canister/plasma,
/turf/open/floor/plating,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"fr" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 10
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/obj/item/folder/white,
+/obj/item/pen/red,
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
"fu" = (
/obj/machinery/shower/directional/south,
/obj/machinery/light/floor,
@@ -530,6 +534,14 @@
/obj/machinery/hydroponics/constructable,
/turf/open/floor/mineral/titanium/tiled,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"fP" = (
+/obj/effect/turf_decal/siding/red,
+/obj/effect/turf_decal/tile/red/half{
+ dir = 1
+ },
+/obj/machinery/light/red/dim/directional/north,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"fR" = (
/obj/structure/lattice/catwalk,
/obj/machinery/atmospherics/pipe/heat_exchanging/junction,
@@ -538,6 +550,11 @@
"fT" = (
/turf/open/floor/engine/bz,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"fU" = (
+/obj/structure/extinguisher_cabinet/directional/west,
+/obj/effect/turf_decal/tile/red/half,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"fV" = (
/obj/structure/frame/computer{
dir = 1
@@ -563,36 +580,24 @@
/obj/machinery/atmospherics/pipe/heat_exchanging/junction/layer2,
/turf/closed/indestructible/syndicate,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"go" = (
-/obj/machinery/light/cold/directional/east,
-/obj/machinery/vending/snack/teal,
+"gs" = (
+/obj/structure/extinguisher_cabinet/directional/west,
+/turf/open/floor/mineral/titanium,
+/area/centcom/syndicate_mothership/control)
+"gu" = (
+/obj/structure/chair/sofa/bench/right{
+ dir = 4
+ },
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/turf/open/floor/catwalk_floor/titanium,
-/area/centcom/syndicate_mothership/control)
-"gs" = (
-/obj/structure/extinguisher_cabinet/directional/west,
-/turf/open/floor/mineral/titanium,
+/turf/open/floor/catwalk_floor/iron_dark,
/area/centcom/syndicate_mothership/control)
"gw" = (
/obj/machinery/light/small/directional/west,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
-"gB" = (
-/obj/effect/turf_decal/siding/wideplating/dark{
- dir = 8
- },
-/obj/effect/turf_decal/siding/wideplating/dark{
- dir = 4
- },
-/obj/machinery/door/airlock/public/glass{
- name = "War Room"
- },
-/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
-/turf/open/floor/iron/textured_large,
-/area/centcom/syndicate_mothership/control)
"gD" = (
/obj/structure/flora/tree/dead/style_random,
/obj/structure/flora/grass/both/style_random,
@@ -773,21 +778,13 @@
dir = 4
},
/area/centcom/syndicate_mothership/expansion_fridgerummage)
-"ij" = (
-/obj/structure/chair/sofa/bench/left{
- dir = 8
+"ih" = (
+/obj/machinery/camera/autoname/directional/west{
+ network = list("nukie")
},
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/turf/open/floor/catwalk_floor/titanium,
-/area/centcom/syndicate_mothership/control)
-"il" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
+/obj/effect/turf_decal/tile/red/half{
dir = 1
},
-/obj/effect/turf_decal/siding/red/corner,
/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"is" = (
@@ -796,6 +793,13 @@
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"iz" = (
+/obj/effect/turf_decal/siding/red/corner{
+ dir = 1
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"iA" = (
/obj/effect/turf_decal/siding/thinplating{
dir = 1
@@ -826,9 +830,11 @@
},
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
-"iV" = (
-/obj/structure/chair/stool/directional/north,
-/turf/open/floor/iron/dark/textured_large,
+"iU" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 6
+ },
+/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/control)
"iX" = (
/obj/machinery/vending/dinnerware,
@@ -840,6 +846,16 @@
/obj/machinery/light/cold/directional/west,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"jd" = (
+/obj/structure/chair/stool/directional/south,
+/obj/structure/sign/map/left{
+ desc = "A framed picture of the station. Clockwise from security at the top (red), you see engineering (yellow), science (purple), escape (red and white), medbay (green), arrivals (blue and white), and finally cargo (brown).";
+ icon_state = "map-left-MS";
+ pixel_y = 32
+ },
+/obj/effect/landmark/start/nukeop_base,
+/turf/open/floor/wood/tile,
+/area/centcom/syndicate_mothership/control)
"je" = (
/obj/effect/turf_decal/siding/wideplating{
dir = 8
@@ -901,6 +917,15 @@
/obj/structure/noticeboard/directional/north,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"kf" = (
+/obj/effect/turf_decal/siding/red/corner{
+ dir = 8
+ },
+/obj/item/folder/red,
+/obj/item/pen/red,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"kg" = (
/turf/open/misc/ice/icemoon,
/area/centcom/syndicate_mothership/control)
@@ -1006,12 +1031,6 @@
},
/turf/open/lava/plasma/ice_moon,
/area/centcom/syndicate_mothership/control)
-"lc" = (
-/obj/effect/turf_decal/siding/red/corner{
- dir = 4
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"ld" = (
/turf/open/floor/iron/smooth_edge{
dir = 8
@@ -1031,13 +1050,6 @@
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"lo" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark,
-/obj/machinery/camera/autoname/directional/south{
- network = list("nukie")
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"lt" = (
/obj/effect/turf_decal/siding/thinplating_new/dark,
/turf/open/floor/mineral/plastitanium,
@@ -1109,18 +1121,6 @@
/obj/machinery/chem_dispenser/mutagensaltpeter,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
-"my" = (
-/obj/effect/landmark/start/nukeop_base/leader,
-/obj/effect/turf_decal/tile/bar/opposingcorners,
-/turf/open/floor/iron,
-/area/centcom/syndicate_mothership/control)
-"mz" = (
-/obj/machinery/vending/cola,
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 4
- },
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"mA" = (
/obj/effect/turf_decal/stripes/line{
dir = 10
@@ -1136,6 +1136,10 @@
},
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"mC" = (
+/obj/effect/turf_decal/tile/red/half,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"mJ" = (
/obj/structure/sign/poster/contraband/free_key,
/turf/closed/indestructible/syndicate,
@@ -1160,19 +1164,6 @@
},
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
-"ng" = (
-/obj/effect/turf_decal/delivery,
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 1
- },
-/obj/structure/fans/tiny,
-/obj/machinery/door/poddoor/shutters/syndicate{
- id = "FBBZ1";
- name = "Security Shutters";
- dir = 1
- },
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"nk" = (
/obj/structure/flora/tree/dead/style_random,
/obj/structure/flora/grass/both/style_random,
@@ -1201,14 +1192,14 @@
/obj/structure/flora/tree/pine/style_random,
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"nD" = (
-/obj/effect/turf_decal/stripes/full,
-/turf/open/floor/mineral/titanium/yellow,
-/area/centcom/syndicate_mothership/control)
"nF" = (
/obj/effect/turf_decal/syndicateemblem/top/middle,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/control)
+"nG" = (
+/obj/machinery/vending/cigarette/syndicate,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"nH" = (
/obj/effect/turf_decal/siding/thinplating_new/dark,
/obj/structure/closet/syndicate/personal,
@@ -1218,6 +1209,10 @@
dir = 8
},
/area/centcom/syndicate_mothership/control)
+"nI" = (
+/obj/machinery/minimap_table,
+/turf/open/floor/circuit/red/no_power,
+/area/centcom/syndicate_mothership/control)
"nL" = (
/obj/structure/chair/office/light{
dir = 1
@@ -1239,6 +1234,12 @@
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"nV" = (
+/obj/structure/chair/stool/directional/north,
+/obj/effect/landmark/start/nukeop_base,
+/obj/structure/sign/poster/contraband/donk_co/directional/south,
+/turf/open/floor/wood/tile,
+/area/centcom/syndicate_mothership/control)
"oc" = (
/obj/structure/fence{
dir = 4
@@ -1254,13 +1255,6 @@
},
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"oh" = (
-/obj/structure/table/reinforced,
-/obj/item/syndicatedetonator{
- desc = "This gaudy button can be used to instantly detonate syndicate bombs that have been activated on the station. It is also fun to press."
- },
-/turf/open/floor/carpet,
-/area/centcom/syndicate_mothership/control)
"oi" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 10
@@ -1272,16 +1266,12 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
/turf/open/floor/catwalk_floor/iron_dark,
/area/centcom/syndicate_mothership/control)
-"or" = (
-/obj/effect/turf_decal/siding/wideplating/dark{
- dir = 1
- },
-/obj/effect/turf_decal/siding/wideplating/dark,
-/obj/machinery/door/airlock/hatch{
- name = "Gangway"
+"om" = (
+/obj/structure/chair/sofa/bench/left{
+ dir = 4
},
-/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
-/turf/open/floor/iron,
+/obj/structure/sign/poster/contraband/smoke/directional/north,
+/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"os" = (
/obj/effect/turf_decal/stripes/box,
@@ -1298,10 +1288,19 @@
},
/turf/open/floor/engine/vacuum,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"oD" = (
+"oA" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 10
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"oH" = (
/obj/effect/turf_decal/siding/red{
dir = 1
},
+/obj/item/folder/red,
+/obj/item/pen/red,
+/obj/structure/table/reinforced/plastitaniumglass,
/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"oK" = (
@@ -1340,6 +1339,14 @@
},
/turf/open/floor/engine,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"oU" = (
+/obj/effect/turf_decal/siding/red{
+ dir = 1
+ },
+/obj/machinery/recharger,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"oW" = (
/obj/effect/turf_decal/siding/thinplating{
dir = 9
@@ -1386,11 +1393,11 @@
/obj/structure/flora/rock/icy/style_random,
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"pr" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
+"pp" = (
+/obj/effect/turf_decal/siding/red,
+/obj/effect/turf_decal/tile/red/half{
dir = 1
},
-/obj/effect/turf_decal/siding/red,
/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"ps" = (
@@ -1422,13 +1429,6 @@
/obj/machinery/vending/tool,
/turf/open/floor/mineral/titanium/yellow,
/area/centcom/syndicate_mothership/control)
-"pS" = (
-/obj/structure/chair/greyscale{
- dir = 4
- },
-/obj/effect/landmark/start/nukeop_base/overwatch,
-/turf/open/floor/mineral/plastitanium,
-/area/centcom/syndicate_mothership)
"pU" = (
/obj/structure/chair/office/light{
dir = 8
@@ -1499,10 +1499,6 @@
},
/turf/open/floor/plastic,
/area/centcom/syndicate_mothership/expansion_fridgerummage)
-"qv" = (
-/obj/item/kirbyplants/random,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"qw" = (
/turf/open/floor/plating,
/area/centcom/syndicate_mothership/control)
@@ -1567,25 +1563,28 @@
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"qU" = (
-/obj/structure/flora/rock/icy/style_random{
- pixel_x = -7
+"qR" = (
+/obj/effect/turf_decal/siding/wideplating{
+ dir = 1
+ },
+/obj/effect/turf_decal/siding/wideplating,
+/obj/machinery/door/poddoor/shutters/syndicate/indestructible{
+ name = "Base Lift";
+ dir = 1;
+ id_tag = "nukiespawnlift";
+ id = "nukiespawnlift"
+ },
+/turf/open/floor/iron/dark/textured_half,
+/area/centcom/syndicate_mothership/control)
+"qU" = (
+/obj/structure/flora/rock/icy/style_random{
+ pixel_x = -7
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
"qX" = (
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
-"rb" = (
-/obj/effect/turf_decal/siding/red,
-/obj/structure/table/reinforced,
-/obj/machinery/recharger,
-/obj/item/stack/spacecash/c10{
- pixel_x = -19;
- pixel_y = 10
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"rf" = (
/turf/open/floor/iron/stairs/old,
/area/centcom/syndicate_mothership/control)
@@ -1607,25 +1606,6 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/turf/open/floor/iron/smooth,
/area/centcom/syndicate_mothership/control)
-"ru" = (
-/obj/effect/turf_decal/siding/wideplating{
- dir = 1
- },
-/obj/effect/turf_decal/siding/wideplating,
-/obj/machinery/door/poddoor/shutters/syndicate/indestructible{
- name = "Base Lift";
- dir = 1;
- id_tag = "nukiespawnlift";
- id = "nukiespawnlift"
- },
-/turf/open/floor/iron/dark/textured_half,
-/area/centcom/syndicate_mothership/control)
-"rw" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 6
- },
-/turf/open/floor/mineral/plastitanium/red,
-/area/centcom/syndicate_mothership/control)
"rA" = (
/obj/effect/turf_decal/siding/purple{
dir = 1
@@ -1665,6 +1645,14 @@
/obj/structure/fence/door/opened,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"rO" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark/corner{
+ dir = 8
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/obj/item/folder/red,
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
"rS" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 5
@@ -1822,20 +1810,6 @@
},
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"tu" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red/corner{
- dir = 1
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
-"tv" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red/corner,
-/obj/item/folder/red,
-/obj/item/pen/red,
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"tz" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 8
@@ -1906,6 +1880,13 @@
/obj/structure/statue/uranium/nuke,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"ux" = (
+/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/open/floor/catwalk_floor/iron_dark,
+/area/centcom/syndicate_mothership/control)
"uK" = (
/obj/effect/turf_decal/siding/wideplating{
dir = 1
@@ -1938,6 +1919,14 @@
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"uY" = (
+/obj/effect/turf_decal/siding/red{
+ dir = 1
+ },
+/obj/effect/turf_decal/tile/red/half,
+/obj/machinery/light/red/dim/directional/south,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"ve" = (
/obj/effect/turf_decal/syndicateemblem/top/middle{
dir = 1
@@ -1958,26 +1947,17 @@
/obj/machinery/portable_atmospherics/canister/carbon_dioxide,
/turf/open/floor/plating,
/area/centcom/syndicate_mothership/expansion_bombthreat)
-"vv" = (
-/obj/structure/table/reinforced,
-/obj/item/paper/fluff/stations/centcom/disk_memo{
- pixel_x = -6;
- pixel_y = -7
- },
-/obj/item/taperecorder{
- pixel_y = 15
+"vn" = (
+/obj/effect/turf_decal/tile/red{
+ dir = 1
},
-/obj/item/stack/spacecash/c50,
-/turf/open/floor/carpet,
+/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
-"vx" = (
-/obj/machinery/door/airlock/external/ruin,
-/obj/effect/mapping_helpers/airlock/cyclelink_helper{
- dir = 8
+"vB" = (
+/obj/effect/turf_decal/tile/red/anticorner{
+ dir = 1
},
-/obj/structure/fans/tiny,
-/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
-/turf/open/floor/plating,
+/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"vI" = (
/obj/structure/railing,
@@ -2022,16 +2002,6 @@
/obj/structure/filingcabinet/medical,
/turf/open/floor/carpet,
/area/centcom/syndicate_mothership/control)
-"wc" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
-"we" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 5
- },
-/turf/open/floor/mineral/plastitanium/red,
-/area/centcom/syndicate_mothership/control)
"wg" = (
/obj/structure/fence/corner{
dir = 6
@@ -2061,6 +2031,16 @@
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
+"ww" = (
+/obj/structure/chair/sofa/bench/left{
+ dir = 8
+ },
+/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/open/floor/catwalk_floor/iron_dark,
+/area/centcom/syndicate_mothership/control)
"wC" = (
/obj/structure/chair/sofa/bench/right{
dir = 4
@@ -2068,6 +2048,15 @@
/obj/structure/sign/poster/contraband/rebels_unite/directional/south,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
+"wD" = (
+/obj/structure/chair/sofa/bench/left{
+ dir = 4
+ },
+/obj/machinery/camera/autoname/directional/north{
+ network = list("nukie")
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"wG" = (
/turf/open/floor/iron/smooth_half{
dir = 1
@@ -2096,10 +2085,6 @@
dir = 8
},
/area/centcom/syndicate_mothership/control)
-"wS" = (
-/obj/effect/landmark/nukeop_elevator/exterior,
-/turf/closed/indestructible/syndicate,
-/area/centcom/syndicate_mothership/control)
"wW" = (
/obj/structure/flora/rock/pile/style_random,
/obj/effect/light_emitter{
@@ -2108,6 +2093,10 @@
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
+"wX" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark/corner,
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
"wY" = (
/obj/machinery/atmospherics/components/unary/outlet_injector/monitored{
desc = "Has a valve and pump attached to it. Slightly more menacing than Nanotrasen's standard.";
@@ -2134,11 +2123,18 @@
/obj/effect/turf_decal/siding/thinplating_new/dark/corner,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
-"xj" = (
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/turf/open/floor/mineral/titanium,
+"xt" = (
+/obj/structure/table/reinforced/plastitaniumglass,
+/obj/effect/turf_decal/delivery,
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 1
+ },
+/obj/structure/fans/tiny,
+/obj/effect/turf_decal/tile/bar/opposingcorners,
+/obj/machinery/door/poddoor/shutters/syndicate{
+ dir = 1
+ },
+/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
"xu" = (
/turf/closed/indestructible/syndicate,
@@ -2161,6 +2157,12 @@
"ya" = (
/turf/open/floor/carpet,
/area/centcom/syndicate_mothership/control)
+"yb" = (
+/obj/effect/turf_decal/tile/red/half{
+ dir = 1
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"ye" = (
/obj/effect/turf_decal/weather/snow/corner{
dir = 10
@@ -2244,6 +2246,10 @@
/obj/structure/chair/office,
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
+"zl" = (
+/obj/machinery/light/small/red/directional/west,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"zo" = (
/obj/item/stack/sheet/mineral/sandbags,
/turf/open/floor/catwalk_floor/iron_dark,
@@ -2274,11 +2280,14 @@
},
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
-"zH" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark,
-/obj/item/kirbyplants/random,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
+"zI" = (
+/obj/effect/light_emitter{
+ set_cap = 1;
+ set_luminosity = 4
+ },
+/obj/structure/flora/tree/dead/style_random,
+/turf/closed/indestructible/rock/snow,
+/area/centcom/syndicate_mothership)
"zL" = (
/obj/structure/rack,
/obj/item/restraints/handcuffs/cable/pink,
@@ -2379,6 +2388,10 @@
},
/turf/open/floor/plating,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"AE" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark,
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
"AL" = (
/obj/machinery/light/cold/directional/south,
/turf/open/floor/iron/smooth_half{
@@ -2388,29 +2401,6 @@
"AM" = (
/turf/closed/indestructible/iron,
/area/centcom/syndicate_mothership/control)
-"AN" = (
-/obj/structure/table/reinforced/plastitaniumglass,
-/obj/effect/turf_decal/delivery,
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 1
- },
-/obj/structure/fans/tiny,
-/obj/effect/turf_decal/tile/bar/opposingcorners,
-/obj/machinery/door/poddoor/shutters/syndicate{
- dir = 1
- },
-/turf/open/floor/iron,
-/area/centcom/syndicate_mothership/control)
-"AO" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red/corner{
- dir = 4
- },
-/obj/item/stack/spacecash/c1{
- pixel_y = 12
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"AR" = (
/obj/effect/baseturf_helper/asteroid/snow,
/turf/closed/indestructible/syndicate,
@@ -2420,16 +2410,6 @@
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
-"AV" = (
-/obj/structure/chair/stool/directional/south,
-/obj/structure/sign/map/left{
- desc = "A framed picture of the station. Clockwise from security at the top (red), you see engineering (yellow), science (purple), escape (red and white), medbay (green), arrivals (blue and white), and finally cargo (brown).";
- icon_state = "map-left-MS";
- pixel_y = 32
- },
-/obj/effect/landmark/start/nukeop_base,
-/turf/open/floor/wood/tile,
-/area/centcom/syndicate_mothership/control)
"AW" = (
/obj/machinery/chem_master,
/turf/open/floor/mineral/titanium/tiled/yellow,
@@ -2561,6 +2541,17 @@
},
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
+"Cn" = (
+/obj/structure/chair/sofa/bench{
+ dir = 4
+ },
+/obj/machinery/light/cold/directional/west,
+/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/open/floor/catwalk_floor/iron_dark,
+/area/centcom/syndicate_mothership/control)
"Ct" = (
/obj/machinery/door/airlock/hatch{
name = "Closet"
@@ -2615,6 +2606,15 @@
},
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
+"CW" = (
+/obj/effect/turf_decal/siding/red{
+ dir = 4
+ },
+/obj/item/paper_bin,
+/obj/item/pen,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"CX" = (
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership)
@@ -2632,6 +2632,16 @@
/obj/structure/sign/poster/contraband/gorlex_recruitment/directional/west,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
+"Dt" = (
+/obj/effect/turf_decal/siding/red/corner{
+ dir = 4
+ },
+/obj/item/stack/spacecash/c1{
+ pixel_y = 12
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Du" = (
/obj/docking_port/stationary{
area_type = /area/centcom/syndicate_mothership;
@@ -2669,6 +2679,10 @@
},
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
+"DQ" = (
+/obj/effect/landmark/nukeop_elevator/exterior,
+/turf/closed/indestructible/syndicate,
+/area/centcom/syndicate_mothership/control)
"DV" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 8
@@ -2713,13 +2727,6 @@
"DZ" = (
/turf/closed/indestructible/syndicate,
/area/centcom/syndicate_mothership/control)
-"Ed" = (
-/obj/structure/chair/sofa/bench/left{
- dir = 4
- },
-/obj/structure/sign/poster/contraband/smoke/directional/north,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"Ef" = (
/obj/structure/fence,
/turf/open/misc/asteroid/snow/airless,
@@ -2783,23 +2790,16 @@
/obj/effect/turf_decal/siding/purple,
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
-"Fm" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red/corner{
- dir = 8
- },
-/obj/item/folder/red,
-/obj/item/pen/red,
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
-"Fp" = (
-/obj/machinery/vending/cigarette/syndicate,
-/turf/open/floor/mineral/titanium,
+"Fl" = (
+/obj/structure/chair/stool/directional/west,
+/obj/effect/landmark/start/nukeop_base,
+/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
-"Fq" = (
-/obj/structure/table/reinforced,
-/obj/item/flashlight/lamp,
-/turf/open/floor/carpet,
+"Fo" = (
+/obj/machinery/light/small/red/directional/north{
+ allow_break_on_init = 0
+ },
+/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership/control)
"FB" = (
/obj/machinery/atmospherics/components/unary/portables_connector/visible{
@@ -2810,6 +2810,25 @@
},
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"FC" = (
+/obj/item/paper/fluff/stations/centcom/disk_memo{
+ pixel_x = -6;
+ pixel_y = -7
+ },
+/obj/item/taperecorder{
+ pixel_y = 15
+ },
+/obj/item/stack/spacecash/c50,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/carpet,
+/area/centcom/syndicate_mothership/control)
+"FE" = (
+/obj/item/syndicatedetonator{
+ desc = "This gaudy button can be used to instantly detonate syndicate bombs that have been activated on the station. It is also fun to press."
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/carpet,
+/area/centcom/syndicate_mothership/control)
"FG" = (
/obj/effect/turf_decal/stripes/corner,
/obj/effect/turf_decal/stripes/corner{
@@ -2831,6 +2850,12 @@
/obj/machinery/atmospherics/pipe/heat_exchanging/simple,
/turf/closed/indestructible/syndicate,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"FQ" = (
+/obj/effect/turf_decal/tile/red{
+ dir = 4
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"FR" = (
/obj/machinery/light/floor,
/turf/open/floor/iron/dark/textured_half{
@@ -2975,17 +3000,6 @@
dir = 8
},
/area/centcom/syndicate_mothership/control)
-"GI" = (
-/obj/structure/chair/sofa/bench{
- dir = 4
- },
-/obj/machinery/light/cold/directional/west,
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/turf/open/floor/catwalk_floor/titanium,
-/area/centcom/syndicate_mothership/control)
"GL" = (
/obj/structure/lattice/catwalk,
/obj/effect/turf_decal/stripes/line{
@@ -3041,10 +3055,6 @@
},
/turf/open/misc/ice/icemoon,
/area/centcom/syndicate_mothership/control)
-"Hs" = (
-/obj/machinery/shuttle_manipulator,
-/turf/open/floor/circuit/red,
-/area/centcom/syndicate_mothership/control)
"Ht" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 8
@@ -3098,12 +3108,26 @@
/obj/item/stack/sheet/mineral/plasma,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"HE" = (
+/obj/effect/landmark/start/nukeop_base/leader,
+/obj/effect/turf_decal/tile/bar/opposingcorners,
+/turf/open/floor/iron,
+/area/centcom/syndicate_mothership/control)
"HJ" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
+"HN" = (
+/obj/machinery/vending/coffee,
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 4
+ },
+/turf/open/floor/iron/white/textured_large{
+ color = "#b51026"
+ },
+/area/centcom/syndicate_mothership/control)
"HW" = (
/obj/effect/light_emitter{
set_cap = 1;
@@ -3124,6 +3148,17 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
/turf/open/floor/catwalk_floor/iron_dark,
/area/centcom/syndicate_mothership/control)
+"Ic" = (
+/obj/effect/mapping_helpers/airlock/cyclelink_helper{
+ dir = 8
+ },
+/obj/structure/fans/tiny,
+/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
+/obj/machinery/door/airlock/hatch{
+ name = "Security Checkpoint"
+ },
+/turf/open/floor/plating,
+/area/centcom/syndicate_mothership/control)
"Id" = (
/obj/structure/closet/crate/freezer{
name = "meat freezer"
@@ -3188,6 +3223,11 @@
},
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"IA" = (
+/obj/item/flashlight/lamp,
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/carpet,
+/area/centcom/syndicate_mothership/control)
"IC" = (
/obj/structure/chair/stool/bar/directional/west,
/obj/effect/turf_decal/siding/thinplating/dark{
@@ -3196,41 +3236,12 @@
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
-"IL" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red,
-/obj/item/toy/nuke{
- pixel_x = -5;
- pixel_y = 1
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
-"IM" = (
-/obj/structure/chair/sofa/bench/right{
- dir = 4
- },
-/obj/effect/turf_decal/siding/thinplating_new/dark,
-/turf/open/floor/mineral/titanium,
-/area/centcom/syndicate_mothership/control)
"IQ" = (
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
-"IV" = (
-/obj/structure/chair/stool/directional/south,
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 1
- },
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
-"Ja" = (
-/obj/machinery/light/small/red/directional/north{
- allow_break_on_init = 0
- },
-/turf/open/floor/mineral/plastitanium,
-/area/centcom/syndicate_mothership/control)
"Jc" = (
/obj/structure/table/wood,
/obj/machinery/chem_dispenser/drinks/beer{
@@ -3266,6 +3277,10 @@
},
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"Jr" = (
+/obj/effect/turf_decal/stripes/full,
+/turf/open/floor/mineral/titanium,
+/area/centcom/syndicate_mothership/control)
"Js" = (
/obj/structure/table/wood,
/obj/item/rag,
@@ -3282,6 +3297,19 @@
},
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"JE" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 9
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"JG" = (
+/obj/structure/chair/comfy/black{
+ dir = 8
+ },
+/obj/effect/turf_decal/tile/red/full,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"JJ" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 5
@@ -3325,6 +3353,13 @@
/obj/effect/turf_decal/syndicateemblem/top/left,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/control)
+"JY" = (
+/obj/effect/turf_decal/siding/red/corner,
+/obj/effect/turf_decal/tile/red/half{
+ dir = 1
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Ka" = (
/obj/structure/chair/sofa/bench/right{
dir = 4
@@ -3345,6 +3380,12 @@
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"KB" = (
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/open/floor/iron/smooth_large,
+/area/centcom/syndicate_mothership/control)
"KD" = (
/obj/effect/turf_decal/trimline/red,
/obj/effect/turf_decal/trimline/red,
@@ -3394,6 +3435,14 @@
},
/turf/open/lava/plasma/ice_moon,
/area/centcom/syndicate_mothership/control)
+"KT" = (
+/obj/effect/turf_decal/stripes/end{
+ dir = 4
+ },
+/obj/machinery/mech_bay_recharge_port,
+/obj/machinery/light/red/dim/directional/south,
+/turf/open/floor/mineral/titanium,
+/area/centcom/syndicate_mothership/control)
"KU" = (
/obj/effect/light_emitter{
set_cap = 1;
@@ -3410,18 +3459,24 @@
},
/turf/open/floor/iron/smooth_half,
/area/centcom/syndicate_mothership/control)
+"KX" = (
+/obj/effect/turf_decal/siding/red{
+ dir = 8
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Ld" = (
/obj/effect/turf_decal/siding/thinplating_new/dark,
/obj/structure/closet/syndicate/personal,
/obj/effect/turf_decal/tile/red/full,
/turf/open/floor/iron/dark/textured_half,
/area/centcom/syndicate_mothership/control)
-"Lk" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red{
- dir = 8
+"Le" = (
+/obj/structure/chair/comfy/black{
+ dir = 4
},
-/obj/item/storage/fancy/donut_box,
+/obj/effect/turf_decal/tile/red/full,
/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
"Lu" = (
@@ -3457,15 +3512,6 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/turf/open/floor/stone,
/area/centcom/syndicate_mothership/control)
-"LB" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red{
- dir = 1
- },
-/obj/item/folder/red,
-/obj/item/pen/red,
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"LH" = (
/obj/effect/turf_decal/stripes/line,
/obj/machinery/atmospherics/components/binary/pump/on,
@@ -3514,13 +3560,6 @@
},
/turf/open/floor/plating/snowed/icemoon,
/area/centcom/syndicate_mothership/control)
-"Me" = (
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/turf/open/floor/catwalk_floor/titanium,
-/area/centcom/syndicate_mothership/control)
"Mh" = (
/obj/structure/sign/poster/contraband/lamarr/directional/south,
/turf/open/floor/iron/smooth_half{
@@ -3592,6 +3631,12 @@
/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
/turf/open/floor/catwalk_floor/iron,
/area/centcom/syndicate_mothership/control)
+"MF" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 4
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"MH" = (
/obj/machinery/hydroponics/constructable,
/obj/machinery/light/cold/directional/west,
@@ -3616,14 +3661,6 @@
dir = 8
},
/area/centcom/syndicate_mothership/control)
-"MU" = (
-/obj/effect/turf_decal/siding/wideplating/dark{
- dir = 8
- },
-/obj/structure/chair/stool/directional/east,
-/obj/effect/landmark/start/nukeop_base,
-/turf/open/floor/wood/tile,
-/area/centcom/syndicate_mothership/control)
"Nb" = (
/obj/structure/chair/sofa/right/brown{
dir = 4
@@ -3637,6 +3674,13 @@
name = "Tac-Com"
},
/area/centcom/syndicate_mothership/control)
+"Ne" = (
+/obj/effect/turf_decal/siding/red/corner{
+ dir = 4
+ },
+/obj/effect/turf_decal/tile/red/half,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Ni" = (
/obj/machinery/light/small/directional/north,
/obj/machinery/computer/slot_machine/syndicate,
@@ -3718,6 +3762,13 @@
/obj/item/melee/powerfist,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"Og" = (
+/obj/structure/chair/sofa/bench/right{
+ dir = 4
+ },
+/obj/effect/turf_decal/siding/thinplating_new/dark,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Oi" = (
/obj/structure/cable,
/turf/closed/indestructible/syndicate,
@@ -3858,6 +3909,24 @@
"Ph" = (
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"Pp" = (
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/turf/open/floor/iron/smooth_large,
+/area/centcom/syndicate_mothership/control)
+"Ps" = (
+/obj/effect/turf_decal/delivery,
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 1
+ },
+/obj/structure/fans/tiny,
+/obj/machinery/door/poddoor/shutters/syndicate{
+ id = "FBBZ1";
+ name = "Security Shutters";
+ dir = 1
+ },
+/turf/open/floor/mineral/titanium,
+/area/centcom/syndicate_mothership/control)
"Pu" = (
/obj/effect/turf_decal/stripes/line{
dir = 9
@@ -4057,6 +4126,13 @@
/obj/structure/cable,
/turf/open/floor/catwalk_floor/iron,
/area/centcom/syndicate_mothership/control)
+"Re" = (
+/obj/effect/turf_decal/siding/red{
+ dir = 1
+ },
+/obj/effect/turf_decal/tile/red/half,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Rn" = (
/obj/item/circuitboard/computer/syndicate_shuttle{
pixel_x = 2;
@@ -4132,6 +4208,14 @@
/obj/effect/baseturf_helper/asteroid/snow,
/turf/closed/indestructible/syndicate,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
+"RV" = (
+/obj/effect/turf_decal/siding/wideplating/dark{
+ dir = 8
+ },
+/obj/structure/chair/stool/directional/east,
+/obj/effect/landmark/start/nukeop_base,
+/turf/open/floor/wood/tile,
+/area/centcom/syndicate_mothership/control)
"Sc" = (
/obj/structure/railing,
/turf/open/floor/iron/stairs/old{
@@ -4139,18 +4223,6 @@
initial_gas_mix = "TEMP=2.7"
},
/area/centcom/syndicate_mothership)
-"Se" = (
-/obj/machinery/door/airlock/external/ruin,
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/obj/effect/mapping_helpers/airlock/cyclelink_helper{
- dir = 4
- },
-/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
-/turf/open/floor/plating,
-/area/centcom/syndicate_mothership/control)
"Sg" = (
/obj/machinery/camera/autoname/directional/east{
network = list("nukie")
@@ -4210,6 +4282,11 @@
/obj/item/mop,
/turf/open/floor/catwalk_floor/iron_smooth,
/area/centcom/syndicate_mothership/control)
+"Sz" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark,
+/obj/structure/sign/poster/contraband/cybersun_six_hundred/directional/east,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"SD" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 8
@@ -4217,6 +4294,12 @@
/obj/machinery/portable_atmospherics/pump,
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bombthreat)
+"SH" = (
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
+/turf/closed/indestructible/opsglass,
+/area/centcom/syndicate_mothership/control)
"SJ" = (
/obj/machinery/atmospherics/components/trinary/filter{
dir = 8
@@ -4242,6 +4325,12 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/turf/closed/indestructible/opsglass,
/area/centcom/syndicate_mothership/control)
+"SN" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 6
+ },
+/turf/open/floor/iron/smooth_half,
+/area/centcom/syndicate_mothership/control)
"SR" = (
/obj/structure/railing,
/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
@@ -4352,12 +4441,6 @@
},
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
-"TH" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 10
- },
-/turf/open/floor/mineral/plastitanium/red,
-/area/centcom/syndicate_mothership/control)
"TK" = (
/obj/structure/barricade/sandbags,
/obj/effect/light_emitter{
@@ -4405,6 +4488,12 @@
},
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/control)
+"Uc" = (
+/obj/effect/turf_decal/tile/red/anticorner{
+ dir = 4
+ },
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Um" = (
/obj/machinery/button/door/directional/south{
id = "syn_ordmix_vent";
@@ -4457,10 +4546,28 @@
},
/turf/open/floor/plating/icemoon,
/area/centcom/syndicate_mothership/control)
+"UC" = (
+/obj/structure/chair/greyscale{
+ dir = 4
+ },
+/obj/effect/landmark/start/nukeop_base/overwatch,
+/turf/open/floor/mineral/plastitanium,
+/area/centcom/syndicate_mothership)
"UE" = (
/obj/machinery/light/cold/directional/east,
/turf/open/floor/plating,
/area/centcom/syndicate_mothership/control)
+"UH" = (
+/obj/effect/turf_decal/siding/wideplating/dark{
+ dir = 1
+ },
+/obj/effect/turf_decal/siding/wideplating/dark,
+/obj/machinery/door/airlock/hatch{
+ name = "Gangway"
+ },
+/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
+/turf/open/floor/mineral/plastitanium,
+/area/centcom/syndicate_mothership/control)
"Va" = (
/obj/effect/turf_decal/syndicateemblem/middle/left{
dir = 8
@@ -4481,6 +4588,13 @@
/obj/structure/flora/rock/pile/style_random,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
+"Vq" = (
+/obj/structure/chair/sofa/bench/right{
+ dir = 8
+ },
+/obj/structure/sign/poster/contraband/syndicate_pistol/directional/north,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"Vr" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 5
@@ -4545,12 +4659,15 @@
/obj/item/stack/sheet/mineral/silver/fifty,
/turf/open/floor/mineral/titanium/tiled/yellow,
/area/centcom/syndicate_mothership/expansion_chemicalwarfare)
-"Wc" = (
-/obj/machinery/vending/coffee,
+"Wb" = (
+/obj/structure/cable,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
/obj/effect/turf_decal/siding/thinplating_new/dark{
dir = 4
},
-/turf/open/floor/mineral/titanium,
+/turf/open/floor/catwalk_floor/iron_dark,
/area/centcom/syndicate_mothership/control)
"Wo" = (
/obj/effect/light_emitter{
@@ -4562,16 +4679,6 @@
"Wp" = (
/turf/open/floor/iron/smooth,
/area/centcom/syndicate_mothership/control)
-"Wr" = (
-/obj/structure/chair/sofa/bench/right{
- dir = 4
- },
-/obj/structure/cable,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5,
-/turf/open/floor/catwalk_floor/titanium,
-/area/centcom/syndicate_mothership/control)
"Ws" = (
/obj/structure/flora/tree/pine/style_random,
/obj/effect/light_emitter{
@@ -4580,9 +4687,16 @@
},
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"Wu" = (
-/obj/structure/extinguisher_cabinet/directional/west,
-/turf/open/floor/iron/dark/textured_large,
+"Wt" = (
+/obj/structure/cable,
+/obj/effect/mapping_helpers/airlock/cyclelink_helper{
+ dir = 4
+ },
+/obj/effect/mapping_helpers/airlock/access/all/syndicate/general,
+/obj/machinery/door/airlock/hatch{
+ name = "Security Checkpoint"
+ },
+/turf/open/floor/plating,
/area/centcom/syndicate_mothership/control)
"Wz" = (
/obj/effect/turf_decal/siding/thinplating_new/dark{
@@ -4609,25 +4723,18 @@
},
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
-"WG" = (
-/obj/structure/chair/sofa/bench/right{
- dir = 8
+"WD" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 5
},
-/obj/structure/sign/poster/contraband/syndicate_pistol/directional/north,
-/turf/open/floor/mineral/titanium,
+/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/control)
"WI" = (
/obj/structure/sign/poster/contraband/revolver/directional/south,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
-"WR" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red{
- dir = 4
- },
-/obj/item/paper_bin,
-/obj/item/pen,
-/turf/open/floor/iron/dark/textured_large,
+"WQ" = (
+/turf/open/floor/circuit/red/no_power,
/area/centcom/syndicate_mothership/control)
"WS" = (
/obj/structure/sign/poster/contraband/energy_swords/directional/north,
@@ -4652,22 +4759,9 @@
/obj/structure/cable,
/turf/open/floor/catwalk_floor/iron_dark,
/area/centcom/syndicate_mothership/control)
-"Xj" = (
-/obj/structure/table/reinforced,
-/obj/effect/turf_decal/siding/red{
- dir = 1
- },
-/obj/machinery/recharger,
-/turf/open/floor/iron/dark/textured_large,
-/area/centcom/syndicate_mothership/control)
"Xk" = (
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership)
-"Xu" = (
-/obj/structure/chair/stool/directional/west,
-/obj/effect/landmark/start/nukeop_base,
-/turf/open/floor/wood/tile,
-/area/centcom/syndicate_mothership/control)
"Xv" = (
/obj/structure/table/reinforced/plastitaniumglass,
/obj/effect/turf_decal/siding/thinplating_new/dark{
@@ -4748,12 +4842,6 @@
dir = 1
},
/area/centcom/syndicate_mothership/control)
-"XZ" = (
-/obj/effect/turf_decal/siding/thinplating_new/dark{
- dir = 9
- },
-/turf/open/floor/mineral/plastitanium/red,
-/area/centcom/syndicate_mothership/control)
"Yd" = (
/obj/machinery/camera/autoname/directional/south{
network = list("nukie")
@@ -4795,6 +4883,15 @@
/obj/structure/cable,
/turf/open/floor/catwalk_floor/iron_smooth,
/area/centcom/syndicate_mothership/control)
+"YM" = (
+/obj/effect/turf_decal/siding/red,
+/obj/item/toy/nuke{
+ pixel_x = -5;
+ pixel_y = 1
+ },
+/obj/structure/table/reinforced/plastitaniumglass,
+/turf/open/floor/iron/dark/textured_large,
+/area/centcom/syndicate_mothership/control)
"YO" = (
/obj/effect/turf_decal/siding/wideplating/dark{
dir = 8
@@ -6512,7 +6609,7 @@ HW
wW
Kq
sv
-pS
+UC
fV
Kq
CX
@@ -6939,7 +7036,7 @@ Pc
ZZ
DZ
gE
-my
+HE
uX
Jc
ek
@@ -7552,7 +7649,7 @@ bF
DZ
al
Gd
-MU
+RV
YO
DZ
tL
@@ -7652,7 +7749,7 @@ VK
BD
sO
DZ
-AV
+jd
mK
er
TS
@@ -7757,7 +7854,7 @@ DZ
OS
TG
jT
-du
+nV
DZ
Dy
Wp
@@ -7857,7 +7954,7 @@ VK
GE
DZ
Ni
-Xu
+Fl
mL
BH
DZ
@@ -9382,16 +9479,16 @@ bW
qw
nw
MP
-ng
+Ps
DZ
VK
VK
VK
VK
DZ
-Ed
-GI
-IM
+om
+Cn
+Og
wG
wG
Te
@@ -9484,16 +9581,16 @@ bW
qw
qw
qw
-ng
+Ps
hb
TC
FR
TC
TC
hb
-ZZ
-Me
-wc
+qN
+ux
+ov
wG
AL
DZ
@@ -9586,20 +9683,20 @@ bW
qw
qw
qw
-ng
+Ps
qJ
TC
TC
FR
TC
qJ
-ZZ
-Me
-wc
+qN
+ux
+ov
wG
wG
DZ
-Ja
+Fo
Ov
DZ
DZ
@@ -9695,14 +9792,14 @@ Xv
KE
VK
DZ
-WG
-ij
-zH
+Vq
+ww
+OF
wG
wG
-ru
-XZ
-TH
+qR
+JE
+oA
Ov
DZ
Ox
@@ -9797,14 +9894,14 @@ so
uX
zL
DZ
-fb
-Wr
-zH
+wD
+gu
+OF
wG
wG
-ru
-we
-rw
+qR
+WD
+iU
Ov
DZ
Ox
@@ -9892,20 +9989,20 @@ bW
qw
qw
qw
-AN
+xt
uX
uX
uX
uX
uX
RA
-ZZ
-Me
-wc
+qN
+ux
+ov
wG
lE
DZ
-Ja
+Fo
Ov
DZ
DZ
@@ -9994,19 +10091,19 @@ bW
qw
qw
qw
-AN
+xt
LS
Mn
uX
ZH
LN
DZ
-Fp
-go
-as
+nG
+cY
+Sz
wG
wG
-wS
+DQ
DZ
DZ
DZ
@@ -10106,12 +10203,11 @@ DZ
DZ
qp
DZ
-gB
-gB
-DZ
-mz
-Wc
+NY
+NY
DZ
+cC
+HN
DZ
DZ
DZ
@@ -10119,6 +10215,7 @@ DZ
DZ
VK
VK
+VK
Ov
DZ
XC
@@ -10208,20 +10305,20 @@ DZ
pK
pg
VK
-ZZ
-ZZ
-dn
+wX
+SN
+ih
qN
qN
-qN
-Wu
-ov
-or
+zl
+fU
+UH
zE
sl
Ov
Ov
Ov
+Ov
DZ
Ox
Ox
@@ -10310,14 +10407,14 @@ DZ
CM
FN
VK
-ZZ
-ZZ
-IV
-tv
-WR
-AO
-iV
-ov
+AE
+vB
+vn
+Le
+Le
+Le
+mC
+DZ
DZ
DZ
DZ
@@ -10412,14 +10509,13 @@ DZ
DZ
eg
tL
-bp
-ZZ
-IV
-IL
-Hs
-Xj
-iV
-ov
+cW
+yb
+qN
+aS
+CW
+Dt
+mC
Cg
DZ
Ox
@@ -10428,6 +10524,7 @@ Ox
Ox
Ox
Ox
+Ox
sq
Ox
sU
@@ -10511,20 +10608,20 @@ Ld
ol
zo
VK
-ZZ
-ZZ
-Me
-ZZ
-ZZ
-IV
-rb
-au
-LB
-iV
-ov
-ah
+qN
+qN
+ux
+AE
+yb
+qN
+YM
+nI
+oU
+mC
+KT
DZ
Ox
+Ox
sq
sq
Ox
@@ -10613,18 +10710,17 @@ Tc
Ib
Ib
en
-xj
-xj
-Me
-ZZ
-ZZ
-IV
-Fm
-Lk
-tu
-iV
-ov
-nD
+Pp
+Pp
+KB
+AE
+yb
+qN
+eM
+WQ
+oH
+mC
+Jr
DZ
Ox
Ox
@@ -10633,6 +10729,7 @@ Ox
Ox
Ox
Ox
+Ox
sU
sU
sU
@@ -10715,21 +10812,21 @@ Ld
XL
RQ
VK
-ZZ
-ZZ
-Me
-ZZ
-ZZ
-il
-dV
-dV
-dV
-lc
-lo
+MF
+MF
+Wb
+AE
+yb
+qN
+kf
+KX
+iz
+mC
ec
DZ
DZ
Ox
+Ox
sU
sU
sU
@@ -10817,21 +10914,21 @@ DZ
DZ
DZ
DZ
-DZ
-VK
-Se
VK
-ZZ
-pr
-oh
-vv
-Fq
-oD
-ov
+Wt
+SH
+cW
+Uc
+FQ
+JG
+JG
+JG
+mC
Nd
gf
DZ
Ox
+Ox
sU
sU
sU
@@ -10919,21 +11016,21 @@ DZ
tJ
HW
HW
-Zt
VK
qw
VK
-qv
-pr
-ya
-PF
-ya
-oD
-OF
+rO
+fr
+JY
+dV
+dV
+dV
+Ne
DZ
DZ
DZ
Ox
+Ox
sU
sU
sU
@@ -11021,21 +11118,21 @@ QM
HW
HW
lj
-HW
VK
qw
VK
DZ
-VK
-wb
-wq
-dw
-VK
-DZ
+bS
+fP
+FE
+FC
+IA
+uY
DZ
Ox
Ox
Ox
+Ox
sU
sU
sU
@@ -11123,20 +11220,20 @@ HW
HW
HW
HW
-tJ
VK
Hv
VK
-Ho
-VK
-VK
-VK
-VK
-VK
-Zt
-KU
+uT
+bS
+pp
+ya
+PF
+ya
+Re
+DZ
Ye
uT
+Ox
Ye
sU
sU
@@ -11225,20 +11322,20 @@ nz
Zt
HW
HW
-HW
VK
qw
VK
-HW
-QM
-HW
-Zt
-HW
-HW
-lj
-KU
+uT
+DZ
+VK
+wb
+wq
+dB
+VK
+DZ
Ye
uT
+Ox
Ye
sU
sU
@@ -11327,18 +11424,18 @@ HW
HW
HW
QM
-Zt
VK
qw
VK
+uT
Zt
+VK
+VK
+VK
+VK
+VK
HW
-HW
-HW
-tJ
-HW
-HW
-KU
+zI
KU
Ye
Ye
@@ -11429,10 +11526,10 @@ uT
uT
uT
uT
-HW
VK
-vx
+Ic
VK
+uT
HW
tJ
HW
@@ -11532,11 +11629,11 @@ uT
uT
uT
uT
-uT
Du
uT
uT
uT
+uT
HW
QM
HW
diff --git a/biome.json b/biome.json
index 73718decc5b2..e00e95dc4c01 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"assist": {
"actions": {
"source": {
diff --git a/bun.lock b/bun.lock
index fde39f4c0a08..12a035251e19 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,30 +4,30 @@
"workspaces": {
"": {
"devDependencies": {
- "@biomejs/biome": "^2.3.10",
- "prettier": "^3.7.4",
- },
- },
+ "@biomejs/biome": "^2.4.16",
+ "prettier": "^3.8.3"
+ }
+ }
},
"packages": {
- "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
+ "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
- "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
+ "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="]
}
}
diff --git a/code/__DEFINES/construction/material.dm b/code/__DEFINES/construction/material.dm
index 59a7b1b74ec2..413de505765d 100644
--- a/code/__DEFINES/construction/material.dm
+++ b/code/__DEFINES/construction/material.dm
@@ -26,7 +26,7 @@
#define MATERIAL_SILO_STORED (1 << 0)
/// This material can be used in basic recipes, such as chairs/toilets/tiles
#define MATERIAL_BASIC_RECIPES (1 << 1)
-/// This material counts as a rigid enough solid to be used to craft tough objects like carving blocks or air tanks
+/// This material counts as a rigid enough solid to be used to craft tough objects like air tanks
#define MATERIAL_CLASS_RIGID (1 << 2)
/// The opposite of rigid, this means that the material cannot hold a solid form (like sand) and cannot be used in item crafting
#define MATERIAL_CLASS_AMORPHOUS (1 << 3)
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
index a8af2cc5c667..70642a4d6b47 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
@@ -167,7 +167,7 @@
/// Return to skip oxyloss and similar effects from blood level
#define HANDLE_BLOOD_NO_OXYLOSS (1<<2)
-/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced) iodk where it should go
+/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced): (var/obj/item/bodypart/limping_leg)
#define COMSIG_CARBON_LIMPING "mob_limp_check"
#define COMPONENT_CANCEL_LIMP (1<<0)
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index bed1a6b24ace..1b71b003872b 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -169,6 +169,19 @@
#define HUD_XENOBIO_CONSOLE "xenobio_console"
+#define HUD_TAC_MINIMAP "tac_minimap"
+#define HUD_TAC_MINIMAP_DIMMER "tac_minimap_dimmer"
+#define HUD_TAC_MINIMAP_Z_INDICATOR "tac_minimap_z_indicator"
+#define HUD_TAC_MINIMAP_Z_INDICATOR_UP "tac_minimap_z_up"
+#define HUD_TAC_MINIMAP_Z_INDICATOR_DOWN "tac_minimap_z_down"
+#define HUD_TAC_MINIMAP_TOOL_RED "tac_minimap_tool_red"
+#define HUD_TAC_MINIMAP_TOOL_YELLOW "tac_minimap_tool_yellow"
+#define HUD_TAC_MINIMAP_TOOL_PURPLE "tac_minimap_tool_purple"
+#define HUD_TAC_MINIMAP_TOOL_BLUE "tac_minimap_tool_blue"
+#define HUD_TAC_MINIMAP_TOOL_ERASE "tac_minimap_tool_erase"
+#define HUD_TAC_MINIMAP_TOOL_LABEL "tac_minimap_tool_label"
+#define HUD_TAC_MINIMAP_TOOL_CLEAR "tac_minimap_tool_clear"
+
/*
These defines specificy screen locations. For more information, see the byond documentation on the screen_loc var.
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 4d368ccc6705..4d7a26858a2b 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -13,6 +13,9 @@
#define PLANE_SPACE -21
#define PLANE_SPACE_PARALLAX -20
+#define WEATHER_MASK_PLANE -13
+#define WEATHER_MASK_RENDER_TARGET "*WEATHER_MASK_RENDER_TARGET"
+
#define DISPLACEMENT_PLANE -12
#define DISPLACEMENT_RENDER_TARGET "*DISPLACEMENT_RENDER_TARGET"
@@ -65,76 +68,79 @@
#define RENDER_PLANE_SPECULAR_MASK 17
#define SPECULAR_MASK_RENDER_TARGET "*RENDER_PLANE_SPECULAR_MASK"
+#define RENDER_PLANE_PARTICLE_WEATHER 18
+#define RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER 19
+
//-------------------- Lighting ---------------------
/// Main game plane to which everything renders, which then is multiplied by light
/// Should not be lit directly as it is sourced for emissive bloom
-#define RENDER_PLANE_UNLIT_GAME 18
+#define RENDER_PLANE_UNLIT_GAME 20
-#define RENDER_PLANE_O_LIGHTING 19
+#define RENDER_PLANE_O_LIGHTING 21
-#define RENDER_PLANE_LIGHTING 20
+#define RENDER_PLANE_LIGHTING 22
/// Masks the lighting plane with turfs, so we never light up the void
/// Failing that, masks emissives and the overlay lighting plane
-#define RENDER_PLANE_LIGHT_MASK 21
+#define RENDER_PLANE_LIGHT_MASK 23
#define LIGHT_MASK_RENDER_TARGET "*RENDER_PLANE_LIGHT_MASK"
/// We cannot render speculars to ABOVE_LIGHTING, as then they give it alpha and end up masking things in darkness
/// So we need to render it directly to RENDER_PLANE_GAME above RENDER_PLANE_LIGHTING
-#define RENDER_PLANE_SPECULAR 22
+#define RENDER_PLANE_SPECULAR 24
/// Things that should render ignoring lighting
-#define ABOVE_LIGHTING_PLANE 23
+#define ABOVE_LIGHTING_PLANE 25
-#define WEATHER_GLOW_PLANE 24
+#define WEATHER_GLOW_PLANE 26
///---------------- MISC -----------------------
///Pipecrawling images
-#define PIPECRAWL_IMAGES_PLANE 25
+#define PIPECRAWL_IMAGES_PLANE 30
///AI Camera Static
-#define CAMERA_STATIC_PLANE 26
+#define CAMERA_STATIC_PLANE 31
///Anything that wants to be part of the game plane, but also wants to draw above literally everything else
-#define HIGH_GAME_PLANE 27
+#define HIGH_GAME_PLANE 32
-#define FULLSCREEN_PLANE 28
+#define FULLSCREEN_PLANE 33
///--------------- FULLSCREEN RUNECHAT BUBBLES ------------
///Popup Chat Messages
-#define RUNECHAT_PLANE 30
+#define RUNECHAT_PLANE 34
/// Plane for balloon text (text that fades up)
-#define BALLOON_CHAT_PLANE 31
+#define BALLOON_CHAT_PLANE 35
//-------------------- HUD ---------------------
//HUD layer defines
-#define HUD_PLANE 35
-#define ABOVE_HUD_PLANE 36
+#define HUD_PLANE 40
+#define ABOVE_HUD_PLANE 41
///Plane of the "splash" icon used that shows on the lobby screen
-#define SPLASHSCREEN_PLANE 37
+#define SPLASHSCREEN_PLANE 42
// The largest plane here must still be less than RENDER_PLANE_GAME
//-------------------- Rendering ---------------------
-#define RENDER_PLANE_GAME 40
+#define RENDER_PLANE_GAME 50
/// If fov is enabled we'll draw game to this and do shit to it
-#define RENDER_PLANE_GAME_MASKED 41
+#define RENDER_PLANE_GAME_MASKED 51
/// The bit of the game plane that is let alone is sent here
-#define RENDER_PLANE_GAME_UNMASKED 42
+#define RENDER_PLANE_GAME_UNMASKED 52
-#define RENDER_PLANE_NON_GAME 45
+#define RENDER_PLANE_NON_GAME 55
// Only VERY special planes should be here, as they are above not just the game, but the UI planes as well.
/// Plane related to the menu when pressing Escape.
/// Needed so that we can apply a blur effect to EVERYTHING, and guarantee we are above all UI.
-#define ESCAPE_MENU_PLANE 46
+#define ESCAPE_MENU_PLANE 56
-#define RENDER_PLANE_MASTER 50
+#define RENDER_PLANE_MASTER 57
// Lummox I swear to god I will find you
// NOTE! You can only ever have planes greater then -10000, if you add too many with large offsets you will brick multiz
@@ -307,6 +313,10 @@
#define CURSE_LAYER 6
#define ECHO_LAYER 7
#define PARRY_LAYER 8
+#define MINIMAP_IMAGE_LAYER 9
+#define MINIMAP_BLIPS_LAYER 10
+#define MINIMAP_LOCATOR_LAYER 11
+#define MINIMAP_LABELS_LAYER 12
#define FOV_EFFECT_LAYER 100
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 7feed196c755..449a0f7f5f47 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -144,6 +144,7 @@
#define LOG_CATEGORY_GAME_ACCESS "game-access"
#define LOG_CATEGORY_GAME_BLOOD_WORM "game-blood-worm"
#define LOG_CATEGORY_GAME_EMOTE "game-emote"
+#define LOG_CATEGORY_GAME_MINIMAP_DRAWING "game-minimap-drawing"
#define LOG_CATEGORY_GAME_INTERNET_REQUEST "game-internet-request"
#define LOG_CATEGORY_GAME_OOC "game-ooc"
#define LOG_CATEGORY_GAME_PRAYER "game-prayer"
diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm
index 1e546cf96b17..0e48cec6ccb2 100644
--- a/code/__DEFINES/machines.dm
+++ b/code/__DEFINES/machines.dm
@@ -74,6 +74,19 @@
/// For wiremod/integrated circuits. Uses various minerals.
#define COMPONENT_PRINTER (1<<10)
+GLOBAL_LIST_INIT(build_types_to_string, list(
+ "[IMPRINTER]" = "Circuit Imprinter",
+ "[PROTOLATHE]" = "Protolathe",
+ "[AUTOLATHE]" = "Autolathe",
+ "[MECHFAB]" = "Exosuit Fabricator",
+ "[BIOGENERATOR]" = "Biogenerator",
+ "[LIMBGROWER]" = "Limb Grower",
+ "[SMELTER]" = "Smelter",
+ "[AWAY_LATHE]" = "Off-Grid Protolathe",
+ "[AWAY_IMPRINTER]" = "Off-Grid Circuit Imprinter",
+ "[COMPONENT_PRINTER]" = "Component Printer",
+))
+
#define HYPERTORUS_INACTIVE 0 // No or minimal energy
#define HYPERTORUS_NOMINAL 1 // Normal operation
#define HYPERTORUS_WARNING 2 // Integrity damaged
diff --git a/code/__DEFINES/minimap.dm b/code/__DEFINES/minimap.dm
new file mode 100644
index 000000000000..05629ef5d0b9
--- /dev/null
+++ b/code/__DEFINES/minimap.dm
@@ -0,0 +1,62 @@
+///Converts the overworld x and y to minimap x and y values
+#define MINIMAP_PIXEL_FROM_WORLD(val) (val*2-3)
+
+//actual size of a users screen in pixels
+#define SCREEN_PIXEL_SIZE 480
+
+//Drawing tool colors
+#define TACMAP_DRAWING_RED "#ff0000"
+#define TACMAP_DRAWING_YELLOW "#FFFF00"
+#define TACMAP_DRAWING_PURPLE "#A020F0"
+#define TACMAP_DRAWING_BLUE "#0000FF"
+
+
+//Turf colours
+#define TACMAP_BLACK "#111111d0"
+#define TACMAP_SOLID "#ebe5e5ee"
+#define TACMAP_DOOR "#451e5eee"
+#define TACMAP_WINDOW "#525252d0"
+#define TACMAP_FENCE "#8c2294ee"
+#define TACMAP_LAVA "#db4206d0"
+#define TACMAP_DIRT "#9c906dd0"
+#define TACMAP_SHALE "#706955d0"
+#define TACMAP_SNOW "#c4e3e9d0"
+#define TACMAP_MARS_DIRT "#aa5f44d0"
+#define TACMAP_ICE "#93cae0d0"
+#define TACMAP_WATER "#94b0d59c" //lower opacity as its really bright
+
+//Area colours
+//Departments
+#define TACMAP_AREA_COMMAND COLOR_COMMAND_BLUE
+#define TACMAP_AREA_CARGO COLOR_CARGO_BROWN
+#define TACMAP_AREA_ENGINEERING COLOR_ENGINEERING_ORANGE
+#define TACMAP_AREA_MEDICAL COLOR_MEDICAL_BLUE
+#define TACMAP_AREA_SCIENCE COLOR_SCIENCE_PINK
+#define TACMAP_AREA_SECURITY COLOR_SECURITY_RED
+#define TACMAP_AREA_SERVICE COLOR_SERVICE_LIME
+//General
+#define TACMAP_AREA_MAINTENANCE COLOR_WEBSAFE_DARK_GRAY
+
+/// How much we multiply the drawn image by, and as a result the pixel coordinates
+#define MINIMAP_PIXEL_MULTIPLIER 2
+/// Converts an icon pixel coordinate (from ICON_X/ICON_Y modifiers) to a world tile coordinate.
+#define MINIMAP_ICON_TO_WORLD(icon_coord, minimap_min) ((minimap_min) + floor(((icon_coord) - 1) / MINIMAP_PIXEL_MULTIPLIER))
+/// Converts a world tile coordinate to a pixel_w/pixel_z offset for placing a blip on the minimap display.
+#define MINIMAP_WORLD_TO_PIXEL(world_coord, minimap_min, half_size) (((world_coord) - (minimap_min)) * MINIMAP_PIXEL_MULTIPLIER + 1 - (half_size))
+#define COMSIG_MINIMAP_ADD(blip_tag) "minimap_add_" + blip_tag
+#define COMSIG_MINIMAP_REMOVE(blip_tag) "minimap_remove_" + blip_tag
+// sends a index of how much to change by
+#define COMSIG_MINIMAP_CHANGE_Z_LEVEL "minimap_z_change"
+#define COMSIG_MINIMAP_ACTION_TRIGGER "minimap_action_trigger"
+ #define COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL (1<<0)
+
+
+#define MINIMAP_BOMB_BLIP "nuke"
+#define MINIMAP_NUKEDISK_BLIP "nuke_disk"
+#define MINIMAP_NUKEOP_BLIP "nukeop"
+#define MINIMAP_NUKEOP_BORG_BLIP "nukeop_borg"
+#define MINIMAP_SYNDICATE_MECH_BLIP "syndicate_mech"
+#define MINIMAP_SYNDIE_TURRET_BLIP "syndie_turret"
+#define MINIMAP_LADDER_BLIP "ladder"
+#define MINIMAP_STAIR_BLIP "stair"
+#define MINIMAP_ANNOTATION_TAG_NUCLEAR "nuclear_ops"
diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm
index d75aeee503fb..a41dfde03071 100644
--- a/code/__DEFINES/sound.dm
+++ b/code/__DEFINES/sound.dm
@@ -21,33 +21,9 @@
#define MAX_INSTRUMENT_CHANNELS (128 * 6)
-/// This is the lowest volume that can be used by playsound otherwise it gets ignored
-/// Most sounds around 10 volume can barely be heard. Almost all sounds at 5 volume or below are inaudible
-/// This is to prevent sound being spammed at really low volumes due to distance calculations
-/// Recommend setting this to anywhere from 10-3 (or 0 to disable any sound minimum volume restrictions)
-/// Ex. For a 70 volume sound, 17 tile range, 3 exponent, 2 falloff_distance:
-/// Setting SOUND_AUDIBLE_VOLUME_MIN to 0 for the above will result in 17x17 radius (289 turfs)
-/// Setting SOUND_AUDIBLE_VOLUME_MIN to 5 for the above will result in 14x14 radius (196 turfs)
-/// Setting SOUND_AUDIBLE_VOLUME_MIN to 10 for the above will result in 11x11 radius (121 turfs)
-#define SOUND_AUDIBLE_VOLUME_MIN 3
-
-/* Calculates the max distance of a sound based on audible volume
- *
- * Note - you should NEVER pass in a volume that is lower than SOUND_AUDIBLE_VOLUME_MIN otherwise distance will be insanely large (like +250,000)
- *
- * Arguments:
- * * volume: The initial volume of the sound being played
- * * max_distance: The range of the sound in tiles (technically not real max distance since the furthest areas gets pruned due to SOUND_AUDIBLE_VOLUME_MIN)
- * * falloff_distance: Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this 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.
- * Returns: The max distance of a sound based on audible volume range
- */
-#define CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(volume, max_distance, falloff_distance, falloff_exponent)\
- floor(((((-(max(max_distance - falloff_distance, 0) ** (1 / falloff_exponent)) / volume) * (SOUND_AUDIBLE_VOLUME_MIN - volume)) ** falloff_exponent) + falloff_distance))
-
/* Calculates the volume of a sound based on distance
*
- * https://www.desmos.com/calculator/shjpmz3ck7
+ * https://www.desmos.com/calculator/ing6lxgd0m Update this when changed please
*
* Arguments:
* * volume: The initial volume of the sound being played
@@ -56,16 +32,16 @@
* * 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.
* Returns: The max distance of a sound based on audible volume range
*/
-#define CALCULATE_SOUND_VOLUME(volume, distance, max_distance, falloff_distance, falloff_exponent)\
- ((max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * volume)
+#define CALCULATE_SOUND_VOLUME_RATIO(volume, distance, max_distance, falloff_distance, falloff_exponent)\
+ ((max(distance - falloff_distance, 0) / (max(max_distance, distance) - falloff_distance)) ** (1 / falloff_exponent))
///Default range of a sound.
-#define SOUND_RANGE 17
-#define MEDIUM_RANGE_SOUND_EXTRARANGE -5
+#define SOUND_RANGE 15
+#define MEDIUM_RANGE_SOUND_EXTRARANGE -3
///default extra range for sounds considered to be quieter
-#define SHORT_RANGE_SOUND_EXTRARANGE -9
+#define SHORT_RANGE_SOUND_EXTRARANGE -7
///The range deducted from sound range for things that are considered silent / sneaky
-#define SILENCED_SOUND_EXTRARANGE -11
+#define SILENCED_SOUND_EXTRARANGE -10
///Percentage of sound's range where no falloff is applied
#define SOUND_DEFAULT_FALLOFF_DISTANCE 0 //Disabled because it doesn't actually have a nice effect, it just makes the jump to fall-off more shocking. maybe delete
///The default exponent of sound falloff
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index a9342860e213..ff73ad03c525 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -501,6 +501,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_CLIFF_WALKER "cliff_walker"
/// This means the user is currently holding/wearing a "tactical camouflage" item (like a potted plant).
#define TRAIT_TACTICALLY_CAMOUFLAGED "tactically_camouflaged"
+/// This means the user is allowed to draw on minimap holotables.
+#define TRAIT_MINIMAP_TABLE_DRAW "minimap_table_draw"
/// Gets double arcade prizes
#define TRAIT_GAMERGOD "gamer-god"
#define TRAIT_GIANT "giant"
diff --git a/code/__DEFINES/weather.dm b/code/__DEFINES/weather.dm
index 0bd041a497c7..ae2f50401e0a 100644
--- a/code/__DEFINES/weather.dm
+++ b/code/__DEFINES/weather.dm
@@ -67,3 +67,6 @@ GLOBAL_LIST_INIT(thunder_chance_options, list(
#define WEATHER_FORCED_TELEGRAPH "Telegraph Duration"
#define WEATHER_FORCED_END "End Duration"
#define WEATHER_FORCED_DURATION "Weather Duration"
+
+/// Maximum possible weather severity value
+#define MAXIMUM_WEATHER_SEVERITY 100
diff --git a/code/__HELPERS/logging/game.dm b/code/__HELPERS/logging/game.dm
index 0affd00228b6..52c1a2254f3c 100644
--- a/code/__HELPERS/logging/game.dm
+++ b/code/__HELPERS/logging/game.dm
@@ -32,3 +32,7 @@
/proc/log_ghost_poll(text, list/data)
logger.Log(LOG_CATEGORY_GAME_GHOST_POLLS, text, data)
+
+/// Logging for drawing on minimap
+/proc/log_minimap_drawing(text, list/data)
+ logger.Log(LOG_CATEGORY_GAME_MINIMAP_DRAWING, text, data)
diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm
index ccb1945a9248..fcd5753f6139 100644
--- a/code/__HELPERS/type2type.dm
+++ b/code/__HELPERS/type2type.dm
@@ -320,6 +320,15 @@ GLOBAL_LIST_INIT(modulo_angle_to_dir, list(NORTH,NORTHEAST,EAST,SOUTHEAST,SOUTH,
if(var_source.vars.Find(A))
. += A
+/// Converts a screen loc param to a x,y coordinate pixel on the screen.
+/proc/params2screenpixel(scr_loc)
+ var/list/x_and_y = splittext(scr_loc, ",")
+ var/list/x_dirty = splittext(x_and_y[1], ":")
+ var/list/y_dirty = splittext(x_and_y[2], ":")
+ var/x = (text2num(x_dirty[1]) - 1) * ICON_SIZE_X + text2num(x_dirty[2])
+ var/y = (text2num(y_dirty[1]) - 1) * ICON_SIZE_Y + text2num(y_dirty[2])
+ return list(x, y)
+
//word of warning: using a matrix like this as a color value will simplify it back to a string after being set
/proc/color_hex2color_matrix(string)
var/length = length(string)
diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm
index 548f15570536..926dde5c0d28 100644
--- a/code/_globalvars/lists/maintenance_loot.dm
+++ b/code/_globalvars/lists/maintenance_loot.dm
@@ -246,6 +246,7 @@ GLOBAL_LIST_INIT(uncommon_loot, list(//uncommon: useful items
/obj/item/food/canned/peaches/maint = 1,
/obj/item/storage/box/donkpockets = 1,
/obj/item/storage/box/gum/happiness = 1,
+ /obj/item/storage/box/ramen_beef = 1,
list(//Donk Varieties
/obj/item/storage/box/donkpockets/donkpocketberry = 1,
/obj/item/storage/box/donkpockets/donkpockethonk = 1,
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 1b36312f0129..498cf5b986e7 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -659,6 +659,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_XRAY_VISION" = TRAIT_XRAY_VISION,
"TRAIT_NOGRAV_ALWAYS_DRIFT" = TRAIT_NOGRAV_ALWAYS_DRIFT,
"TRAIT_SPEECH_BOOSTER" = TRAIT_SPEECH_BOOSTER,
+ "TRAIT_MINIMAP_TABLE_DRAW" = TRAIT_MINIMAP_TABLE_DRAW,
"TRAIT_MINING_PARRYING" = TRAIT_MINING_PARRYING,
"TRAIT_MINING_AOE_IMMUNE" = TRAIT_MINING_AOE_IMMUNE,
"TRAIT_IGNORE_FIRE_PROTECTION" = TRAIT_IGNORE_FIRE_PROTECTION,
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index ee1aa3633d11..d25020206dab 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -354,6 +354,13 @@
/mob/proc/RangedAttack(atom/A, modifiers)
if(SEND_SIGNAL(src, COMSIG_MOB_ATTACK_RANGED, A, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN)
return TRUE
+ A.RangedAttackOn(src, modifiers)
+
+/**
+ * Atom's version of RangedAttack, for when you want to do something when a mob clicks on this with more sanity than just Click()
+ */
+/atom/proc/RangedAttackOn(mob/attacker, list/modifiers)
+ return null
/**
* Ranged secondary attack
diff --git a/code/_onclick/click_ctrl.dm b/code/_onclick/click_ctrl.dm
index e80f8fa05cb9..37c8e3ec1193 100644
--- a/code/_onclick/click_ctrl.dm
+++ b/code/_onclick/click_ctrl.dm
@@ -34,7 +34,7 @@
SHOULD_NOT_OVERRIDE(TRUE)
. = ..()
- if(. || world.time < next_move || !can_perform_action(target, NOT_INSIDE_TARGET | SILENT_ADJACENCY | ALLOW_RESTING | FORBID_TELEKINESIS_REACH))
+ if(. || !ismovable(target) || world.time < next_move || !can_perform_action(target, NOT_INSIDE_TARGET | SILENT_ADJACENCY | ALLOW_RESTING | FORBID_TELEKINESIS_REACH))
return
. = TRUE
diff --git a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm
index 6cfafcd6797a..094371cc9e68 100644
--- a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm
+++ b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm
@@ -328,6 +328,22 @@
return
home.AddComponent(/datum/component/hide_weather_planes, src)
+/atom/movable/screen/plane_master/weather_mask
+ name = "Weather Mask"
+ documentation = "Used to mask particle weather effects to cut out areas unaffected by weather."
+ plane = WEATHER_MASK_PLANE
+ appearance_flags = PLANE_MASTER | NO_CLIENT_COLOR
+ render_target = WEATHER_MASK_RENDER_TARGET
+ render_relay_planes = list()
+ start_hidden = TRUE
+ critical = PLANE_CRITICAL_DISPLAY
+
+/atom/movable/screen/plane_master/weather_mask/set_home(datum/plane_master_group/home)
+ . = ..()
+ if(!.)
+ return
+ home.AddComponent(/datum/component/hide_weather_planes, src, TRUE)
+
/atom/movable/screen/plane_master/massive_obj
name = "Massive object"
documentation = "Huge objects need to render above everything else on the game plane, otherwise they'd well, get clipped and look not that huge. This does that."
diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm
index 9d42784bb2af..11979dde2ace 100644
--- a/code/_onclick/hud/rendering/render_plate.dm
+++ b/code/_onclick/hud/rendering/render_plate.dm
@@ -72,6 +72,45 @@
. = ..()
add_relay_to(GET_NEW_PLANE(RENDER_PLANE_EMISSIVE_BLOOM, offset), blend_override = BLEND_MULTIPLY)
+/atom/movable/screen/plane_master/rendering_plate/particle_weather
+ name = "Particle Weather"
+ documentation = "Plane used to render particle weather, masked by WEATHER_MASK_PLANE. \
+ Cannot be a single screen object as it needs to be a planemaster in order to be properly masked by the weather mask."
+ plane = RENDER_PLANE_PARTICLE_WEATHER
+ start_hidden = TRUE
+ critical = PLANE_CRITICAL_DISPLAY
+
+/atom/movable/screen/plane_master/rendering_plate/particle_weather/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset)
+ . = ..()
+ // We can actually do this just fine as we do not render anything onto ourselves but our particles
+ add_filter("weather_mask", 1, alpha_mask_filter(render_source = OFFSET_RENDER_TARGET(WEATHER_MASK_RENDER_TARGET, offset)))
+ SSweather.particle_planemasters += src
+ // And add all ongoing weather to ourselves
+ for (var/holder_offset, holder_list in SSweather.particle_holders)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ if (holder.plane == plane)
+ vis_contents += holder
+
+/atom/movable/screen/plane_master/rendering_plate/particle_weather/Destroy()
+ SSweather.particle_planemasters -= src
+ return ..()
+
+/atom/movable/screen/plane_master/rendering_plate/particle_weather/set_home(datum/plane_master_group/home)
+ . = ..()
+ if(!.)
+ return
+ home.AddComponent(/datum/component/hide_weather_planes, src, TRUE)
+
+/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive
+ name = "Emissive Particle Weather"
+ documentation = "Secondary particle weather plane for emissive parts of weather, which is additionally rendered onto the emissive plane after being masked."
+ plane = RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER
+
+/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset)
+ . = ..()
+ // Render a copy of ourselves onto the emissive plane encoded into the bloom channel
+ add_relay_to(GET_NEW_PLANE(EMISSIVE_PLANE, offset), relay_color = GLOB.emissive_color)
+
/atom/movable/screen/plane_master/rendering_plate/turf_lighting
name = "Turf lighting post-processing plate"
documentation = "Used by overlay lighting, and possibly over plates, to mask out turf lighting."
diff --git a/code/_onclick/hud/screen_objects/screen_objects.dm b/code/_onclick/hud/screen_objects/screen_objects.dm
index 11accd552f52..00341f671881 100644
--- a/code/_onclick/hud/screen_objects/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects/screen_objects.dm
@@ -94,7 +94,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen) // I hate this place
RegisterSignal(hud, COMSIG_QDELETING, PROC_REF(on_hud_delete))
/// Returns the mob this is being displayed to, if any
-/atom/movable/screen/proc/get_mob()
+/atom/movable/screen/proc/get_mob() as /mob
return hud?.mymob
/atom/movable/screen/proc/on_hud_delete(datum/source)
diff --git a/code/_onclick/overmind.dm b/code/_onclick/overmind.dm
index a9d8dba6e13f..b81777229c46 100644
--- a/code/_onclick/overmind.dm
+++ b/code/_onclick/overmind.dm
@@ -1,36 +1,36 @@
// Blob Overmind Controls
-/mob/eye/blob/ClickOn(atom/A, params) //Expand blob
+/mob/eye/blob/ClickOn(atom/clicked_on, params) //Expand blob
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, MIDDLE_CLICK))
- MiddleClickOn(A, params)
+ MiddleClickOn(clicked_on, params)
return
if(LAZYACCESS(modifiers, SHIFT_CLICK))
- ShiftClickOn(A)
+ ShiftClickOn(clicked_on)
return
if(LAZYACCESS(modifiers, ALT_CLICK))
- blob_click_alt(A)
+ blob_click_alt(clicked_on)
return
if(LAZYACCESS(modifiers, CTRL_CLICK))
- CtrlClickOn(A)
+ CtrlClickOn(clicked_on)
return
- var/turf/T = get_turf(A)
- if(T)
- expand_blob(T)
+ var/turf/target_turf = get_turf(clicked_on)
+ if(target_turf)
+ expand_blob(target_turf)
-/mob/eye/blob/MiddleClickOn(atom/A) //Rally spores
+/mob/eye/blob/MiddleClickOn(atom/clicked_on) //Rally spores
. = ..()
- var/turf/T = get_turf(A)
- if(T)
- rally_spores(T)
+ var/turf/target_turf = get_turf(clicked_on)
+ if(target_turf)
+ rally_spores(target_turf)
-/mob/eye/blob/CtrlClickOn(atom/A) //Create a shield
- var/turf/T = get_turf(A)
- if(T)
- create_shield(T)
+/mob/eye/blob/CtrlClickOn(atom/clicked_on) //Create a shield
+ var/turf/target_turf = get_turf(clicked_on)
+ if(target_turf)
+ create_shield(target_turf)
-/mob/eye/blob/proc/blob_click_alt(atom/A) //Remove a blob
- var/turf/T = get_turf(A)
- if(T)
- remove_blob(T)
+/mob/eye/blob/proc/blob_click_alt(atom/clicked_on) //Remove a blob
+ var/turf/target_turf = get_turf(clicked_on)
+ if(target_turf)
+ remove_blob(target_turf)
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index c48ff211499a..74ad7d26ddb7 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -101,6 +101,9 @@
/// log game events
/datum/config_entry/flag/log_game
+/// log minimap drawing events
+/datum/config_entry/flag/log_minimap_drawing
+
/// log mech data
/datum/config_entry/flag/log_mecha
diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm
index b4bf65a501c1..d30d3aab73e4 100644
--- a/code/controllers/subsystem/materials.dm
+++ b/code/controllers/subsystem/materials.dm
@@ -23,10 +23,11 @@ SUBSYSTEM_DEF(materials)
new /datum/stack_recipe("Material floor tile", /obj/item/stack/tile/material, 1, 4, 20, crafting_flags = CRAFT_SKIP_MATERIALS_PARITY, category = CAT_TILES),
new /datum/stack_recipe("Material airlock assembly", /obj/structure/door_assembly/door_assembly_material, 4, time = 5 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_DOORS),
new /datum/stack_recipe("Material platform", /obj/structure/platform/material, 2, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, trait_booster = TRAIT_QUICK_BUILD, trait_modifier = 0.75, category = CAT_STRUCTURE), \
+ new /datum/stack_recipe("Carving block", /obj/structure/carving_block, 5, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_STRUCTURE),
)
- /// List of stackcrafting recipes for materials using rigid recipes
+ /// List of stackcrafting recipes for materials using rigid recipes (none yet)
var/list/rigid_stack_recipes = list(
- new /datum/stack_recipe("Carving block", /obj/structure/carving_block, 5, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_STRUCTURE),
+
)
/// A list of dimensional themes used by the dimensional anomaly and other things, most of which require materials to function.
diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm
index 7a89236e08a2..d50013baceed 100644
--- a/code/controllers/subsystem/weather.dm
+++ b/code/controllers/subsystem/weather.dm
@@ -8,13 +8,26 @@ SUBSYSTEM_DEF(weather)
wait = 10
runlevels = RUNLEVEL_GAME
var/list/processing = list()
+ /// Z levels on which weather can occur -> weather that can occur -> probability of said weather occuring
var/list/eligible_zlevels = list()
- var/list/next_hit_by_zlevel = list() //Used by barometers to know when the next storm is coming
+ /// Used by barometers to know when the next storm is coming
+ var/list/next_hit_by_zlevel = list()
+ /// Alist of all particle holders per Z-stack offset for particle weather to be shown to clients
+ var/alist/particle_holders = alist()
+ /// List of all RENDER_PLANE_PARTICLE_WEATHER and RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER planes
+ var/list/particle_planemasters = list()
/datum/controller/subsystem/weather/fire(resumed = FALSE)
// process active weather
for(var/datum/weather/weather_event as anything in processing)
- if(!length(weather_event.subsystem_tasks) || weather_event.stage != MAIN_STAGE)
+ if(!length(weather_event.subsystem_tasks))
+ continue
+
+ if(istype(weather_event, /datum/weather/particle))
+ var/datum/weather/particle/particle_event = weather_event
+ particle_event.process_particles()
+
+ if(weather_event.stage != MAIN_STAGE)
continue
if(weather_event.subsystem_tasks[weather_event.task_index] == SSWEATHER_MOBS)
@@ -69,21 +82,42 @@ SUBSYSTEM_DEF(weather)
next_hit_by_zlevel["[z]"] = addtimer(CALLBACK(src, PROC_REF(make_eligible), z, possible_weather), randTime + initial(weather_event.weather_duration_upper), TIMER_UNIQUE|TIMER_STOPPABLE)
/datum/controller/subsystem/weather/Initialize()
- for(var/V in subtypesof(/datum/weather))
- var/datum/weather/W = V
- var/probability = initial(W.probability)
- var/target_trait = initial(W.target_trait)
+ for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
+ var/probability = initial(weather.probability)
+ var/target_trait = initial(weather.target_trait)
// any weather with a probability set may occur at random
if (probability)
for(var/z in SSmapping.levels_by_trait(target_trait))
LAZYINITLIST(eligible_zlevels["[z]"])
- eligible_zlevels["[z]"][W] = probability
+ eligible_zlevels["[z]"][weather] = probability
return SS_INIT_SUCCESS
+/datum/controller/subsystem/weather/proc/add_weather_objects(list/new_holders, z_level)
+ for (var/offset in 1 to length(new_holders))
+ var/list/holder_list = new_holders[offset]
+ if (isnull(particle_holders[offset]))
+ particle_holders[offset] = list()
+ particle_holders[offset] += holder_list
+
+ // We add it to vis_contents of planemasters rather than client screen as planemasters already
+ // manage their own visibility based on owner's z level
+ for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ if (holder.plane == plane_master.plane)
+ plane_master.vis_contents |= holder
+
+/datum/controller/subsystem/weather/proc/remove_weather_objects(list/old_holders)
+ for (var/offset in 1 to length(old_holders))
+ var/list/holder_list = old_holders[offset]
+ particle_holders[offset] -= holder_list
+
+ for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters)
+ plane_master.vis_contents -= holder_list
+
/datum/controller/subsystem/weather/proc/update_z_level(datum/space_level/level)
var/z = level.z_value
- for(var/datum/weather/weather as anything in subtypesof(/datum/weather))
+ for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
var/probability = initial(weather.probability)
var/target_trait = initial(weather.target_trait)
if(probability && level.traits[target_trait])
@@ -92,10 +126,9 @@ SUBSYSTEM_DEF(weather)
/datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type, z_levels, list/weather_data)
if (istext(weather_datum_type))
- for (var/V in subtypesof(/datum/weather))
- var/datum/weather/W = V
- if (initial(W.name) == weather_datum_type)
- weather_datum_type = V
+ for (var/datum/weather/weather as anything in valid_subtypesof(/datum/weather))
+ if (initial(weather.name) == weather_datum_type)
+ weather_datum_type = weather
break
if (!ispath(weather_datum_type, /datum/weather))
CRASH("run_weather called with invalid weather_datum_type: [weather_datum_type || "null"]")
@@ -107,10 +140,9 @@ SUBSYSTEM_DEF(weather)
else if (!islist(z_levels))
CRASH("run_weather called with invalid z_levels: [z_levels || "null"]")
-
- var/datum/weather/W = new weather_datum_type(z_levels, weather_data)
- W.telegraph(weather_data)
- return W
+ var/datum/weather/weather = new weather_datum_type(z_levels, weather_data)
+ weather.telegraph(weather_data)
+ return weather
/datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather)
eligible_zlevels[z] = possible_weather
diff --git a/code/datums/ai/telegraph_effects.dm b/code/datums/ai/telegraph_effects.dm
index b658c651988a..99a58a641a0a 100644
--- a/code/datums/ai/telegraph_effects.dm
+++ b/code/datums/ai/telegraph_effects.dm
@@ -34,7 +34,7 @@
/obj/effect/temp_visual/telegraphing/line
icon = 'icons/mob/telegraphing/telegraph.dmi'
icon_state = "line"
- duration = 0.8 SECONDS
+ duration = 1.2 SECONDS
/obj/effect/temp_visual/telegraphing/line/short
duration = 0.5 SECONDS
diff --git a/code/datums/components/ai_listen_to_weather.dm b/code/datums/components/ai_listen_to_weather.dm
index a7bb95ee8c13..29bcda3839b5 100644
--- a/code/datums/components/ai_listen_to_weather.dm
+++ b/code/datums/components/ai_listen_to_weather.dm
@@ -7,7 +7,7 @@
///what blackboard key are we setting
var/weather_key
-/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/ash_storm, weather_key = BB_STORM_APPROACHING)
+/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/particle/ash_storm, weather_key = BB_STORM_APPROACHING)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.weather_type = weather_type
diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm
index 5b7addb199e7..7a7cb3655b8f 100644
--- a/code/datums/components/hide_weather_planes.dm
+++ b/code/datums/components/hide_weather_planes.dm
@@ -8,17 +8,20 @@
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
var/list/datum/weather/active_weather = list()
var/list/atom/movable/screen/plane_master/plane_masters = list()
+ /// Do we care about all weather or only particle weather?
+ var/particle_only = FALSE
-/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about)
+/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about, particle_only = FALSE)
if(!istype(parent, /datum/plane_master_group))
return COMPONENT_INCOMPATIBLE
+ src.particle_only = particle_only
var/datum/plane_master_group/home = parent
plane_masters += care_about
RegisterSignal(care_about, COMSIG_QDELETING, PROC_REF(plane_master_deleted))
var/list/starting_signals = list()
var/list/ending_signals = list()
- for(var/datum/weather/weather_type as anything in typesof(/datum/weather))
+ for(var/datum/weather/weather_type as anything in valid_subtypesof(particle_only ? /datum/weather/particle : /datum/weather))
starting_signals += COMSIG_WEATHER_TELEGRAPH(weather_type)
ending_signals += COMSIG_WEATHER_END(weather_type)
@@ -103,6 +106,8 @@
var/list/connected_levels = SSmapping.get_connected_levels(new_z)
for(var/datum/weather/active as anything in SSweather.processing)
+ if(particle_only && !istype(active, /datum/weather/particle))
+ continue
if(length(connected_levels & active.impacted_z_levels))
active_weather += WEAKREF(active)
diff --git a/code/datums/components/lockable_storage.dm b/code/datums/components/lockable_storage.dm
index 51a0e48be6cd..a31888789b19 100644
--- a/code/datums/components/lockable_storage.dm
+++ b/code/datums/components/lockable_storage.dm
@@ -226,11 +226,14 @@
. = ..()
if(action != "keypad")
return TRUE
+
+ var/atom/source = parent
+ playsound(source, SFX_TERMINAL_TYPE, 50, FALSE)
+
var/digit = params["digit"]
switch(digit)
//locking it back up
if("C")
- var/atom/source = parent
numeric_input = ""
//you can't lock it if it's already locked or lacks a lock code.
if(source.atom_storage.locked || isnull(lock_code))
@@ -249,7 +252,6 @@
//unlocking the current code.
if(numeric_input != lock_code)
return TRUE
- var/atom/source = parent
source.atom_storage.set_locked(STORAGE_NOT_LOCKED)
numeric_input = ""
return TRUE
diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm
index 256f29526707..f13acee56c28 100644
--- a/code/datums/components/object_possession.dm
+++ b/code/datums/components/object_possession.dm
@@ -69,9 +69,9 @@
user.name = target.name
user.reset_perspective(target)
- target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
target.AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
- target.AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
target.AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(end_possession))
@@ -84,8 +84,8 @@
var/mob/poltergeist = parent
- possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
- possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
UnregisterSignal(possessed, COMSIG_QDELETING)
diff --git a/code/datums/components/storm_hating.dm b/code/datums/components/storm_hating.dm
index 62a59a0ad71a..0b4edbe84e78 100644
--- a/code/datums/components/storm_hating.dm
+++ b/code/datums/components/storm_hating.dm
@@ -4,7 +4,7 @@
/datum/component/storm_hating
/// Types of weather which trigger the effect
var/static/list/stormy_weather = list(
- /datum/weather/ash_storm,
+ /datum/weather/particle/ash_storm,
/datum/weather/snow_storm,
/datum/weather/void_storm,
)
diff --git a/code/datums/components/walking_aid.dm b/code/datums/components/walking_aid.dm
new file mode 100644
index 000000000000..7a869661f2d5
--- /dev/null
+++ b/code/datums/components/walking_aid.dm
@@ -0,0 +1,161 @@
+/**
+ * Walking Aid Component
+ *
+ * Add this to an item to allow it to act as a cane (or crutch) while held.
+ * Items with this component will help mobs avoid limping from broken
+ * leg bones, and lessen the slowdown caused by missing legs.
+ *
+ * Used by canes, crutches, and pole-like items such as spears and staffs.
+ */
+/datum/component/walking_aid
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ /// Causes a mob to waddle (wiggle) while walking when holding this object
+ var/waddling = FALSE
+ /// If set, the parent item must have this trait for the support to function
+ var/required_trait
+ /// Weakref to the mob currently being supported
+ var/datum/weakref/current_user_ref
+ /// The amount of slowdown to reduce for a limbless leg
+ var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40%
+
+/datum/component/walking_aid/Initialize(limbless_slowdown_modifier = 0.6, required_trait = null, waddling = FALSE)
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.waddling = waddling
+ src.required_trait = required_trait
+ src.limbless_slowdown_modifier = limbless_slowdown_modifier
+
+/datum/component/walking_aid/Destroy(force)
+ remove_support()
+ return ..()
+
+/datum/component/walking_aid/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip))
+ RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop))
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(get_examine_tags))
+ RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_WIELDED), PROC_REF(update_legs))
+ RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_WIELDED), PROC_REF(update_legs))
+ RegisterSignal(parent, SIGNAL_ADDTRAIT(required_trait), PROC_REF(update_legs))
+ RegisterSignal(parent, SIGNAL_REMOVETRAIT(required_trait), PROC_REF(update_legs))
+
+/datum/component/walking_aid/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_ITEM_DROPPED,
+ COMSIG_ATOM_EXAMINE_TAGS,
+ SIGNAL_ADDTRAIT(TRAIT_WIELDED),
+ SIGNAL_REMOVETRAIT(TRAIT_WIELDED),
+ SIGNAL_ADDTRAIT(required_trait),
+ SIGNAL_REMOVETRAIT(required_trait),
+ ))
+ remove_support()
+
+/datum/component/walking_aid/proc/on_equip(datum/source, mob/equipper, slot)
+ SIGNAL_HANDLER
+
+ remove_support()
+ if(!(slot & ITEM_SLOT_HANDS))
+ return
+ if(!isliving(equipper))
+ return
+
+ apply_support(equipper)
+
+/datum/component/walking_aid/proc/on_drop(datum/source, mob/user)
+ SIGNAL_HANDLER
+ remove_support()
+
+/datum/component/walking_aid/proc/get_examine_tags(atom/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ examine_list["walking-aid"] = "It can help lessen the slowdown caused from a missing or injured leg, when held on the same side as the injury."
+
+// Updates our leg status when wielded/unwielded a two handed walking aid like a spear
+/datum/component/walking_aid/proc/update_legs(atom/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/user = current_user_ref?.resolve()
+ user?.update_usable_leg_status()
+
+/datum/component/walking_aid/proc/apply_support(mob/living/user)
+ if(current_user_ref)
+ remove_support()
+
+ current_user_ref = WEAKREF(user)
+ RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping))
+ RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown))
+ user.update_usable_leg_status()
+
+ if(waddling)
+ user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
+
+/datum/component/walking_aid/proc/remove_support()
+ var/mob/living/user = current_user_ref?.resolve()
+ current_user_ref = null
+ if(isnull(user))
+ return
+ UnregisterSignal(user, list(COMSIG_CARBON_LIMPING, COMSIG_LIVING_LIMBLESS_SLOWDOWN))
+ user.update_usable_leg_status()
+
+ if(waddling)
+ REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src))
+
+/datum/component/walking_aid/proc/is_active()
+ // if both hands are holding it, then it is not being used for support
+ if(HAS_TRAIT(parent, TRAIT_WIELDED))
+ return FALSE
+
+ if(isnull(required_trait))
+ return TRUE
+
+ return HAS_TRAIT(parent, required_trait)
+
+/datum/component/walking_aid/proc/handle_limping(mob/living/user, obj/item/bodypart/limping_leg)
+ SIGNAL_HANDLER
+
+ if(!is_active())
+ return NONE
+ if(isnull(limping_leg))
+ return NONE
+
+ var/supported_zone = get_supported_leg_zone(user)
+ if(isnull(supported_zone))
+ return NONE
+ if(limping_leg.body_zone != supported_zone)
+ return NONE
+
+ return COMPONENT_CANCEL_LIMP
+
+/datum/component/walking_aid/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods)
+ SIGNAL_HANDLER
+
+ if(!is_active())
+ return
+ if(!iscarbon(user))
+ return
+ var/mob/living/carbon/carbon_user = user
+ var/leg_amount = carbon_user.usable_legs
+ if(leg_amount >= carbon_user.default_num_legs)
+ return
+ if(!leg_amount) // someday support dual-wielding crutches but for now they are destined to waddle
+ return
+
+ var/supported_zone = get_supported_leg_zone(user)
+ if(isnull(supported_zone))
+ return
+ if(carbon_user.get_bodypart(supported_zone)) // make sure their leg is actually missing
+ return
+
+ slowdown_mods += limbless_slowdown_modifier
+
+/datum/component/walking_aid/proc/get_supported_leg_zone(mob/living/user)
+ var/held_hand_zone = user.get_hand_zone_of_item(parent)
+
+ switch(held_hand_zone)
+ if(BODY_ZONE_R_ARM)
+ return BODY_ZONE_R_LEG
+ if(BODY_ZONE_L_ARM)
+ return BODY_ZONE_L_LEG
+ else
+ return null
diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm
index e8054369d88e..62f545e6ee33 100644
--- a/code/datums/diseases/_disease.dm
+++ b/code/datums/diseases/_disease.dm
@@ -77,8 +77,8 @@
SSdisease.active_diseases += D //Add it to the active diseases list, now that it's actually in a mob and being processed.
D.after_add()
+ D.register_disease_signals()
infectee.med_hud_set_status()
- register_disease_signals()
var/turf/source_turf = get_turf(infectee)
log_virus("[key_name(infectee)] was infected by virus: [src.admin_details()] at [loc_name(source_turf)]")
diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm
index d527d46b6d65..41d837993c4e 100644
--- a/code/datums/lazy_template.dm
+++ b/code/datums/lazy_template.dm
@@ -81,6 +81,17 @@
var/turf/bottom_left = reservation.bottom_left_turfs[z_idx]
var/turf/top_right = reservation.top_right_turfs[z_idx]
+ // Make our turfs dead to atmos
+ // Cache for sonic speed
+ var/list/to_rebuild = SSair.adjacent_rebuild
+ for(var/turf/contained_turf as anything in block(bottom_left, top_right))
+ SSair.remove_from_active(contained_turf)
+ to_rebuild -= contained_turf
+ for(var/turf/sub_turf as anything in contained_turf.atmos_adjacent_turfs)
+ sub_turf.atmos_adjacent_turfs?.Remove(contained_turf)
+ contained_turf.atmos_adjacent_turfs?.Cut()
+ CHECK_TICK
+
load_map(
file(load_path),
bottom_left.x,
@@ -104,6 +115,10 @@
loaded_atom_movables |= thing
SSatoms.InitializeAtoms(loaded_areas + loaded_atom_movables + loaded_turfs)
+ for(var/turf/turf as anything in loaded_turfs)
+ CALCULATE_ADJACENT_TURFS(turf, NORMAL_TURF)
+ CHECK_TICK
+
SSlighting.setup_static_lighting_if_needed(loaded_turfs)
SSmachines.setup_template_powernets(loaded_cables)
SSair.setup_template_machinery(loaded_atmospherics)
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index 9ab93ae378e8..a8549ab1418b 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -140,7 +140,7 @@
mid_length = 1.8 SECONDS
end_sound = 'sound/machines/computer/computer_end.ogg'
end_volume = 1 SECONDS
- volume = SOUND_AUDIBLE_VOLUME_MIN
+ volume = 3
falloff_exponent = 4 //Ultra quiet very fast
extra_range = -12
falloff_distance = 0 //Instant falloff after initial tile
diff --git a/code/datums/map_config.dm b/code/datums/map_config.dm
index 021c53b336d4..c3793f141e5c 100644
--- a/code/datums/map_config.dm
+++ b/code/datums/map_config.dm
@@ -60,8 +60,12 @@
/// Boolean - if TRUE, players spawn with grappling hooks in their bags
var/give_players_hooks = FALSE
+#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM)
/// List of unit tests that are skipped when running this map
var/list/skipped_tests
+ /// If TRUE, only unit tests with UNIT_TEST_DEBUG_MAP_ONLY will run on this map
+ var/is_unit_test_map = FALSE
+#endif
/// Boolean that tells SSmapping to load all away missions in the codebase.
var/load_all_away_missions = FALSE
@@ -266,6 +270,9 @@
stack_trace("Invalid path in mapping config for ignored unit tests: \[[path_as_text]\]")
continue
LAZYADD(skipped_tests, path_real)
+
+ if ("is_unit_test_map" in json)
+ is_unit_test_map = json["is_unit_test_map"]
#endif
defaulted = FALSE
diff --git a/code/datums/proximity_monitor/proximity_monitor.dm b/code/datums/proximity_monitor/proximity_monitor.dm
index ec77ce2145a1..f5e63733765f 100644
--- a/code/datums/proximity_monitor/proximity_monitor.dm
+++ b/code/datums/proximity_monitor/proximity_monitor.dm
@@ -74,9 +74,10 @@
//Update the ignore_if_not_on_turf
AddComponent(/datum/component/connect_range, host, loc_connections, current_range, ignore_if_not_on_turf)
-/datum/proximity_monitor/proc/on_uncrossed()
+/datum/proximity_monitor/proc/on_uncrossed(atom/source, atom/movable/gone, direction) //Used by the advanced subtype for effect fields.
SIGNAL_HANDLER
- return //Used by the advanced subtype for effect fields.
+ if(source != host)
+ hasprox_receiver?.OnProximityExit(gone)
/datum/proximity_monitor/proc/on_entered(atom/source, atom/movable/arrived, turf/old_loc)
SIGNAL_HANDLER
diff --git a/code/datums/quirks/negative_quirks/brain_problems.dm b/code/datums/quirks/negative_quirks/brain_problems.dm
index d9257a33c6b4..bd75265eff56 100644
--- a/code/datums/quirks/negative_quirks/brain_problems.dm
+++ b/code/datums/quirks/negative_quirks/brain_problems.dm
@@ -15,10 +15,14 @@
quirk_flags = QUIRK_HUMAN_ONLY|QUIRK_PROCESSES
mail_goodies = list(/obj/item/storage/pill_bottle/mannitol/braintumor)
no_process_traits = list(TRAIT_TUMOR_SUPPRESSED)
+ /// Speed at which our brain will get damaged, per second
+ var/degradation_speed = 0.2
+ /// Type of item we get in our pocket to help with the quirk after joining
+ var/obj/item/medicine_to_get = /obj/item/storage/pill_bottle/mannitol/braintumor
/datum/quirk/item_quirk/brainproblems/add_unique(client/client_source)
give_item_to_holder(
- /obj/item/storage/pill_bottle/mannitol/braintumor,
+ medicine_to_get,
list(
LOCATION_LPOCKET,
LOCATION_RPOCKET,
@@ -35,4 +39,4 @@
return ..()
/datum/quirk/item_quirk/brainproblems/process(seconds_per_tick)
- quirk_holder.adjust_organ_loss(ORGAN_SLOT_BRAIN, 0.2 * seconds_per_tick)
+ quirk_holder.adjust_organ_loss(ORGAN_SLOT_BRAIN, degradation_speed * seconds_per_tick)
diff --git a/code/datums/quirks/negative_quirks/chronic_illness.dm b/code/datums/quirks/negative_quirks/chronic_illness.dm
index 49438160c489..a96c787e37dc 100644
--- a/code/datums/quirks/negative_quirks/chronic_illness.dm
+++ b/code/datums/quirks/negative_quirks/chronic_illness.dm
@@ -10,8 +10,8 @@
mail_goodies = list(/obj/item/storage/pill_bottle/sansufentanyl)
/datum/quirk/item_quirk/chronic_illness/add(client/client_source)
- var/datum/disease/chronic_illness/hms = new /datum/disease/chronic_illness()
- quirk_holder.ForceContractDisease(hms)
+ var/datum/disease/chronic_illness/hms = new()
+ quirk_holder.ForceContractDisease(hms, make_copy = FALSE, del_on_fail = TRUE)
/datum/quirk/item_quirk/chronic_illness/add_unique(client/client_source)
give_item_to_holder(/obj/item/storage/pill_bottle/sansufentanyl, list(LOCATION_BACKPACK), flavour_text = "You've been provided with medication to help manage your condition. Take it regularly to avoid complications.", notify_player = TRUE)
diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm
index d9ad9903c283..93fdfb8213bb 100644
--- a/code/datums/sound_token.dm
+++ b/code/datums/sound_token.dm
@@ -123,7 +123,7 @@
if(source_turf.z != listener_turf.z)
should_be_muted = TRUE
- var/distance = get_dist(source_turf, listener_turf)
+ var/distance = get_dist_euclidean(source_turf, listener_turf)
if(distance > range)
should_be_muted = TRUE
if(should_be_muted && is_muted)
@@ -195,7 +195,7 @@
return
if(player_turf.z != source_turf.z)
return
- if(get_dist(source_turf, player_turf) > range)
+ if(get_dist_euclidean(source_turf, player_turf) > range)
return
add_or_update_listener(player)
diff --git a/code/datums/sprite_accessories/_sprite_accessory.dm b/code/datums/sprite_accessories/_sprite_accessory.dm
new file mode 100644
index 000000000000..4cdd8ab6e589
--- /dev/null
+++ b/code/datums/sprite_accessories/_sprite_accessory.dm
@@ -0,0 +1,66 @@
+/*
+ * Hello and welcome to sprite_accessories: For sprite accessories, such as hair,
+ * facial hair, and possibly tattoos and stuff somewhere along the line. This file is
+ * intended to be friendly for people with little to no actual coding experience.
+ * The process of adding in new hairstyles has been made pain-free and easy to do.
+ * Enjoy! - Doohl
+ *
+ *
+ * Notice: This all gets automatically compiled in a list in dna.dm, so you do not
+ * have to define any UI values for sprite accessories manually for hair and facial
+ * hair. Just add in new hair types and the game will naturally adapt.
+ *
+ * !!WARNING!!: changing existing hair information can be VERY hazardous to savefiles,
+ * to the point where you may completely corrupt a server's savefiles. Please refrain
+ * from doing this unless you absolutely know what you are doing, and have defined a
+ * conversion in savefile.dm
+ */
+
+/datum/sprite_accessory
+ /// The icon file the accessory is located in.
+ var/icon
+ /// The icon_state of the accessory.
+ var/icon_state
+ /// The preview name of the accessory.
+ var/name
+ /// Determines if the accessory will be skipped or included in random hair generations.
+ var/gender = NEUTER
+ /// Something that can be worn by either gender, but looks different on each.
+ var/gender_specific = FALSE
+ /// Determines if the accessory will be skipped by color preferences.
+ var/use_static
+ /**
+ * Currently only used by mutantparts so don't worry about hair and stuff.
+ * This is the source that this accessory will get its color from. Default is MUTCOLOR, but can also be HAIR, FACEHAIR, EYECOLOR and 0 if none.
+ */
+ var/color_src = MUTANT_COLOR
+ /// Is this part locked from roundstart selection? Used for parts that apply effects.
+ var/locked = FALSE
+ /// Should we center the sprite?
+ var/center = FALSE
+ /// The width of the sprite in pixels. Used to center it if necessary.
+ var/dimension_x = 32
+ /// The height of the sprite in pixels. Used to center it if necessary.
+ var/dimension_y = 32
+ /// Should this sprite block emissives?
+ var/em_block = FALSE
+ /// Determines if this is considered "sane" for the purpose of [/proc/randomize_human_normie]
+ /// Basically this is to blacklist the extremely wacky stuff from being picked in random human generation.
+ var/natural_spawn = TRUE
+
+/datum/sprite_accessory/blank
+ name = SPRITE_ACCESSORY_NONE
+ icon_state = SPRITE_ACCESSORY_NONE
+
+//////////.//////////////////
+// MutantParts Definitions //
+/////////////////////////////
+
+/datum/sprite_accessory/caps
+ icon = 'icons/mob/human/species/mush_cap.dmi'
+ color_src = HAIR_COLOR
+ em_block = TRUE
+
+/datum/sprite_accessory/caps/round
+ name = "Round"
+ icon_state = "round"
diff --git a/code/datums/sprite_accessories/clothing.dm b/code/datums/sprite_accessories/clothing.dm
new file mode 100644
index 000000000000..a2ab967b701b
--- /dev/null
+++ b/code/datums/sprite_accessories/clothing.dm
@@ -0,0 +1,718 @@
+/datum/sprite_accessory/clothing
+ abstract_type = /datum/sprite_accessory/clothing
+ /// Allows you to specify a greyscale config
+ var/greyscale_config
+ /// Icon state in the digitigrade template file to use if the wearer is digitigrade.
+ /// If null, no special digitigrade handling is done.
+ var/digi_icon_state
+ /// Color pallete for static colored underwear, like hearts.
+ /// Used so greyscale copies can have the same palette.
+ var/greyscale_colors = "#FFFFFF#FFFFFF#FFFFFF"
+ /// The layer this sprite accessory should render on
+ var/layer = BODY_LAYER
+ /// What kind of gender shaping this sprite accessory should use (in case your sprite gets a weird missing pixel in the center)
+ var/female_sprite_flags = FEMALE_UNIFORM_FULL
+
+/// Override to return a different icon state given a bodytype or physique
+/datum/sprite_accessory/clothing/proc/get_icon_state(physique, bodyshape)
+ return icon_state
+
+/**
+ * Generate an appearance from this clothing datum
+ *
+ * * color - if this is NOT a statically colored clothing article and NOT gags, uses this color.
+ * * physique - physique of the wearer (male or female)
+ * * bodyshape - bodyshape of the wearer (humanoid, digitigrade, etc)
+ */
+/datum/sprite_accessory/clothing/proc/make_appearance(color = COLOR_WHITE, physique = MALE, bodyshape = BODYSHAPE_HUMANOID)
+ var/static/list/cached_icons = list()
+ var/use_female = physique == FEMALE && female_sprite_flags
+ var/use_digi = digi_icon_state && (bodyshape & BODYSHAPE_DIGITIGRADE)
+ var/female_sprite_flags_to_use = female_sprite_flags
+ var/icon_state_to_use = get_icon_state(physique, bodyshape)
+ if(use_digi && female_sprite_flags_to_use)
+ female_sprite_flags_to_use = FEMALE_UNIFORM_TOP_ONLY // No bottom gender shaping for the digi legs
+
+ var/key = "[icon_state_to_use]-[greyscale_config || "ng"]-[use_female]-[use_digi]-[greyscale_colors]"
+ var/mutable_appearance/result
+ if(cached_icons[key]) // it's already cached
+ result = mutable_appearance(icon(cached_icons[key]))
+
+ else if(greyscale_config || use_female || use_digi) // icon ops ahead
+ var/icon/created = icon(greyscale_config ? SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) : icon, icon_state_to_use)
+ if(use_female)
+ created = wear_female_version(icon_state_to_use, icon, female_sprite_flags_to_use)
+ if(use_digi)
+ var/icon/replacement = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade_underwear, greyscale_colors), digi_icon_state)
+ created = replace_icon_legs(created, replacement)
+
+ cached_icons[key] = fcopy_rsc(created)
+ result = mutable_appearance(created)
+
+ else // no caching necessary
+ result = mutable_appearance(icon, icon_state)
+
+ result.layer = -layer
+ result.color = use_static ? null : color
+
+ return result
+
+
+///////////////////////////
+// Underwear Definitions //
+///////////////////////////
+
+/datum/sprite_accessory/clothing/underwear
+ icon = 'icons/mob/clothing/underwear.dmi'
+ use_static = FALSE
+ em_block = TRUE
+ abstract_type = /datum/sprite_accessory/clothing/underwear
+
+//MALE UNDERWEAR
+/datum/sprite_accessory/clothing/underwear/nude
+ name = "Nude"
+ icon_state = null
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/underwear/nude/make_appearance(mob/living/carbon/human/for_who)
+ return
+
+/datum/sprite_accessory/clothing/underwear/male_briefs
+ name = "Briefs"
+ icon_state = "male_briefs"
+ gender = MALE
+
+/datum/sprite_accessory/clothing/underwear/male_boxers
+ name = "Boxers"
+ icon_state = "male_boxers"
+ gender = MALE
+ digi_icon_state = "boxers"
+
+/datum/sprite_accessory/clothing/underwear/male_stripe
+ name = "Striped Boxers"
+ icon_state = "male_stripe"
+ gender = MALE
+ digi_icon_state = "boxers_stripe"
+
+/datum/sprite_accessory/clothing/underwear/male_midway
+ name = "Midway Boxers"
+ icon_state = "male_midway"
+ gender = MALE
+ digi_icon_state = "midway"
+
+/datum/sprite_accessory/clothing/underwear/male_longjohns
+ name = "Long Johns"
+ icon_state = "male_longjohns"
+ gender = MALE
+ digi_icon_state = "longjohns"
+
+/datum/sprite_accessory/clothing/underwear/male_kinky
+ name = "Jockstrap"
+ icon_state = "male_kinky"
+ gender = MALE
+
+/datum/sprite_accessory/clothing/underwear/male_mankini
+ name = "Mankini"
+ icon_state = "male_mankini"
+ gender = MALE
+
+/datum/sprite_accessory/clothing/underwear/male_hearts
+ name = "Hearts Boxers"
+ icon_state = "male_hearts"
+ gender = MALE
+ use_static = TRUE
+ digi_icon_state = "boxers_stripe_threecolor"
+ greyscale_colors = "#D62626#EEEEEE#D62626#"
+
+/datum/sprite_accessory/clothing/underwear/male_commie
+ name = "Commie Boxers"
+ icon_state = "male_commie"
+ gender = MALE
+ use_static = TRUE
+ digi_icon_state = "boxers_stripe_twocolor"
+ greyscale_colors = "#D62626#D1B62C#D62626"
+
+/datum/sprite_accessory/clothing/underwear/male_usastripe
+ name = "Freedom Boxers"
+ icon_state = "male_assblastusa"
+ gender = MALE
+ use_static = TRUE
+ digi_icon_state = "boxers_stripe_threecolor"
+ greyscale_colors = "#D62626#EEEEEE#2E26D6"
+
+/datum/sprite_accessory/clothing/underwear/male_uk
+ name = "UK Boxers"
+ icon_state = "male_uk"
+ gender = MALE
+ use_static = TRUE
+ digi_icon_state = "boxers_stripe_threecolor"
+ greyscale_colors = "#D62626#EEEEEE#2E26D6"
+
+//FEMALE UNDERWEAR
+/datum/sprite_accessory/clothing/underwear/female_bikini
+ name = "Bikini"
+ icon_state = "female_bikini"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/female_lace
+ name = "Lace Bikini"
+ icon_state = "female_lace"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/female_bralette
+ name = "Bralette w/ Boyshorts"
+ icon_state = "female_bralette"
+ gender = FEMALE
+ digi_icon_state = "short_short"
+
+/datum/sprite_accessory/clothing/underwear/female_sport
+ name = "Sports Bra w/ Boyshorts"
+ icon_state = "female_sport"
+ gender = FEMALE
+ digi_icon_state = "short"
+
+/datum/sprite_accessory/clothing/underwear/female_thong
+ name = "Thong"
+ icon_state = "female_thong"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/female_strapless
+ name = "Strapless Bikini"
+ icon_state = "female_strapless"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/female_babydoll
+ name = "Babydoll"
+ icon_state = "female_babydoll"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_onepiece
+ name = "One-Piece Swimsuit"
+ icon_state = "swim_onepiece"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_onepiece
+ name = "Strapless One-Piece Swimsuit"
+ icon_state = "swim_strapless_onepiece"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_twopiece
+ name = "Two-Piece Swimsuit"
+ icon_state = "swim_twopiece"
+ gender = FEMALE
+ digi_icon_state = "short_short"
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_twopiece
+ name = "Strapless Two-Piece Swimsuit"
+ icon_state = "swim_strapless_twopiece"
+ gender = FEMALE
+ digi_icon_state = "short_short"
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_stripe
+ name = "Strapless Striped Swimsuit"
+ icon_state = "swim_stripe"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/swimsuit_halter
+ name = "Halter Swimsuit"
+ icon_state = "swim_halter"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/underwear/female_white_neko
+ name = "Neko Bikini (White)"
+ icon_state = "female_neko_white"
+ gender = FEMALE
+ use_static = TRUE
+
+/datum/sprite_accessory/clothing/underwear/female_black_neko
+ name = "Neko Bikini (Black)"
+ icon_state = "female_neko_black"
+ gender = FEMALE
+ use_static = TRUE
+
+/datum/sprite_accessory/clothing/underwear/female_commie
+ name = "Commie Bikini"
+ icon_state = "female_commie"
+ gender = FEMALE
+ use_static = TRUE
+
+/datum/sprite_accessory/clothing/underwear/female_usastripe
+ name = "Freedom Bikini"
+ icon_state = "female_assblastusa"
+ gender = FEMALE
+ use_static = TRUE
+
+/datum/sprite_accessory/clothing/underwear/female_uk
+ name = "UK Bikini"
+ icon_state = "female_uk"
+ gender = FEMALE
+ use_static = TRUE
+
+/datum/sprite_accessory/clothing/underwear/female_kinky
+ name = "Lingerie"
+ icon_state = "female_kinky"
+ gender = FEMALE
+ use_static = TRUE
+
+////////////////////////////
+// Undershirt Definitions //
+////////////////////////////
+
+/datum/sprite_accessory/clothing/undershirt
+ icon = 'icons/mob/clothing/underwear.dmi'
+ em_block = TRUE
+ abstract_type = /datum/sprite_accessory/clothing/undershirt
+
+/datum/sprite_accessory/clothing/undershirt/nude
+ name = "Nude"
+ icon_state = null
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/nude/make_appearance(mob/living/carbon/human/for_who)
+ return
+
+// please make sure they're sorted alphabetically and categorized
+
+/datum/sprite_accessory/clothing/undershirt/bluejersey
+ name = "Jersey (Blue)"
+ icon_state = "shirt_bluejersey"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/redjersey
+ name = "Jersey (Red)"
+ icon_state = "shirt_redjersey"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/bluepolo
+ name = "Polo Shirt (Blue)"
+ icon_state = "bluepolo"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/grayyellowpolo
+ name = "Polo Shirt (Gray-Yellow)"
+ icon_state = "grayyellowpolo"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/redpolo
+ name = "Polo Shirt (Red)"
+ icon_state = "redpolo"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/whitepolo
+ name = "Polo Shirt (White)"
+ icon_state = "whitepolo"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/alienshirt
+ name = "Shirt (Alien)"
+ icon_state = "shirt_alien"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/mondmondjaja
+ name = "Shirt (Band)"
+ icon_state = "band"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/shirt_black
+ name = "Shirt (Black)"
+ icon_state = "shirt_black"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/blueshirt
+ name = "Shirt (Blue)"
+ icon_state = "shirt_blue"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/clownshirt
+ name = "Shirt (Clown)"
+ icon_state = "shirt_clown"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/commie
+ name = "Shirt (Commie)"
+ icon_state = "shirt_commie"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/greenshirt
+ name = "Shirt (Green)"
+ icon_state = "shirt_green"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/shirt_grey
+ name = "Shirt (Grey)"
+ icon_state = "shirt_grey"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/ian
+ name = "Shirt (Ian)"
+ icon_state = "ian"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/ilovent
+ name = "Shirt (I Love NT)"
+ icon_state = "ilovent"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/lover
+ name = "Shirt (Lover)"
+ icon_state = "lover"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/matroska
+ name = "Shirt (Matroska)"
+ icon_state = "matroska"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/meat
+ name = "Shirt (Meat)"
+ icon_state = "shirt_meat"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/nano
+ name = "Shirt (Nanotrasen)"
+ icon_state = "shirt_nano"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/peace
+ name = "Shirt (Peace)"
+ icon_state = "peace"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/pacman
+ name = "Shirt (Pogoman)"
+ icon_state = "pogoman"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/question
+ name = "Shirt (Question)"
+ icon_state = "shirt_question"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/redshirt
+ name = "Shirt (Red)"
+ icon_state = "shirt_red"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/skull
+ name = "Shirt (Skull)"
+ icon_state = "shirt_skull"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/ss13
+ name = "Shirt (SS13)"
+ icon_state = "shirt_ss13"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/stripe
+ name = "Shirt (Striped)"
+ icon_state = "shirt_stripes"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tiedye
+ name = "Shirt (Tie-dye)"
+ icon_state = "shirt_tiedye"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/uk
+ name = "Shirt (UK)"
+ icon_state = "uk"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/usa
+ name = "Shirt (USA)"
+ icon_state = "shirt_assblastusa"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/shirt_white
+ name = "Shirt (White)"
+ icon_state = "shirt_white"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/blackshortsleeve
+ name = "Short-sleeved Shirt (Black)"
+ icon_state = "blackshortsleeve"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/blueshortsleeve
+ name = "Short-sleeved Shirt (Blue)"
+ icon_state = "blueshortsleeve"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/greenshortsleeve
+ name = "Short-sleeved Shirt (Green)"
+ icon_state = "greenshortsleeve"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/purpleshortsleeve
+ name = "Short-sleeved Shirt (Purple)"
+ icon_state = "purpleshortsleeve"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/whiteshortsleeve
+ name = "Short-sleeved Shirt (White)"
+ icon_state = "whiteshortsleeve"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/sports_bra
+ name = "Sports Bra"
+ icon_state = "sports_bra"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/sports_bra2
+ name = "Sports Bra (Alt)"
+ icon_state = "sports_bra_alt"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/blueshirtsport
+ name = "Sports Shirt (Blue)"
+ icon_state = "blueshirtsport"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/greenshirtsport
+ name = "Sports Shirt (Green)"
+ icon_state = "greenshirtsport"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/redshirtsport
+ name = "Sports Shirt (Red)"
+ icon_state = "redshirtsport"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tank_black
+ name = "Tank Top (Black)"
+ icon_state = "tank_black"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tankfire
+ name = "Tank Top (Fire)"
+ icon_state = "tank_fire"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tank_grey
+ name = "Tank Top (Grey)"
+ icon_state = "tank_grey"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/female_midriff
+ name = "Tank Top (Midriff)"
+ icon_state = "tank_midriff"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/undershirt/tank_red
+ name = "Tank Top (Red)"
+ icon_state = "tank_red"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tankstripe
+ name = "Tank Top (Striped)"
+ icon_state = "tank_stripes"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tank_white
+ name = "Tank Top (White)"
+ icon_state = "tank_white"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/redtop
+ name = "Top (Red)"
+ icon_state = "redtop"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/undershirt/whitetop
+ name = "Top (White)"
+ icon_state = "whitetop"
+ gender = FEMALE
+
+/datum/sprite_accessory/clothing/undershirt/tshirt_blue
+ name = "T-Shirt (Blue)"
+ icon_state = "blueshirt"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tshirt_green
+ name = "T-Shirt (Green)"
+ icon_state = "greenshirt"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/tshirt_red
+ name = "T-Shirt (Red)"
+ icon_state = "redshirt"
+ gender = NEUTER
+
+/datum/sprite_accessory/clothing/undershirt/yellowshirt
+ name = "T-Shirt (Yellow)"
+ icon_state = "yellowshirt"
+ gender = NEUTER
+
+///////////////////////
+// Socks Definitions //
+///////////////////////
+
+/datum/sprite_accessory/clothing/socks
+ icon = 'icons/mob/clothing/underwear.dmi'
+ em_block = TRUE
+ abstract_type = /datum/sprite_accessory/clothing/socks
+
+/datum/sprite_accessory/clothing/socks/nude
+ name = "Nude"
+ icon_state = null
+
+/datum/sprite_accessory/clothing/socks/nude/make_appearance(mob/living/carbon/human/for_who)
+ return
+
+// please make sure they're sorted alphabetically and categorized
+
+/datum/sprite_accessory/clothing/socks/ace_knee
+ name = "Knee-high (Ace)"
+ icon_state = "ace_knee"
+
+/datum/sprite_accessory/clothing/socks/bee_knee
+ name = "Knee-high (Bee)"
+ icon_state = "bee_knee"
+
+/datum/sprite_accessory/clothing/socks/black_knee
+ name = "Knee-high (Black)"
+ icon_state = "black_knee"
+
+/datum/sprite_accessory/clothing/socks/commie_knee
+ name = "Knee-High (Commie)"
+ icon_state = "commie_knee"
+
+/datum/sprite_accessory/clothing/socks/usa_knee
+ name = "Knee-High (Freedom)"
+ icon_state = "assblastusa_knee"
+
+/datum/sprite_accessory/clothing/socks/rainbow_knee
+ name = "Knee-high (Rainbow)"
+ icon_state = "rainbow_knee"
+
+/datum/sprite_accessory/clothing/socks/striped_knee
+ name = "Knee-high (Striped)"
+ icon_state = "striped_knee"
+
+/datum/sprite_accessory/clothing/socks/thin_knee
+ name = "Knee-high (Thin)"
+ icon_state = "thin_knee"
+
+/datum/sprite_accessory/clothing/socks/trans_knee
+ name = "Knee-high (Trans)"
+ icon_state = "trans_knee"
+
+/datum/sprite_accessory/clothing/socks/uk_knee
+ name = "Knee-High (UK)"
+ icon_state = "uk_knee"
+
+/datum/sprite_accessory/clothing/socks/white_knee
+ name = "Knee-high (White)"
+ icon_state = "white_knee"
+
+/datum/sprite_accessory/clothing/socks/fishnet_knee
+ name = "Knee-high (Fishnet)"
+ icon_state = "fishnet_knee"
+
+/datum/sprite_accessory/clothing/socks/black_norm
+ name = "Normal (Black)"
+ icon_state = "black_norm"
+
+/datum/sprite_accessory/clothing/socks/white_norm
+ name = "Normal (White)"
+ icon_state = "white_norm"
+
+/datum/sprite_accessory/clothing/socks/pantyhose
+ name = "Pantyhose"
+ icon_state = "pantyhose"
+
+/datum/sprite_accessory/clothing/socks/black_short
+ name = "Short (Black)"
+ icon_state = "black_short"
+
+/datum/sprite_accessory/clothing/socks/white_short
+ name = "Short (White)"
+ icon_state = "white_short"
+
+/datum/sprite_accessory/clothing/socks/stockings_blue
+ name = "Stockings (Blue)"
+ icon_state = "stockings_blue"
+
+/datum/sprite_accessory/clothing/socks/stockings_cyan
+ name = "Stockings (Cyan)"
+ icon_state = "stockings_cyan"
+
+/datum/sprite_accessory/clothing/socks/stockings_dpink
+ name = "Stockings (Dark Pink)"
+ icon_state = "stockings_dpink"
+
+/datum/sprite_accessory/clothing/socks/stockings_green
+ name = "Stockings (Green)"
+ icon_state = "stockings_green"
+
+/datum/sprite_accessory/clothing/socks/stockings_orange
+ name = "Stockings (Orange)"
+ icon_state = "stockings_orange"
+
+/datum/sprite_accessory/clothing/socks/stockings_programmer
+ name = "Stockings (Programmer)"
+ icon_state = "stockings_lpink"
+
+/datum/sprite_accessory/clothing/socks/stockings_purple
+ name = "Stockings (Purple)"
+ icon_state = "stockings_purple"
+
+/datum/sprite_accessory/clothing/socks/stockings_yellow
+ name = "Stockings (Yellow)"
+ icon_state = "stockings_yellow"
+
+/datum/sprite_accessory/clothing/socks/stockings_fishnet
+ name = "Stockings (Fishnet)"
+ icon_state = "fishnet_full"
+
+/datum/sprite_accessory/clothing/socks/ace_thigh
+ name = "Thigh-high (Ace)"
+ icon_state = "ace_thigh"
+
+/datum/sprite_accessory/clothing/socks/bee_thigh
+ name = "Thigh-high (Bee)"
+ icon_state = "bee_thigh"
+
+/datum/sprite_accessory/clothing/socks/black_thigh
+ name = "Thigh-high (Black)"
+ icon_state = "black_thigh"
+
+/datum/sprite_accessory/clothing/socks/commie_thigh
+ name = "Thigh-high (Commie)"
+ icon_state = "commie_thigh"
+
+/datum/sprite_accessory/clothing/socks/usa_thigh
+ name = "Thigh-high (Freedom)"
+ icon_state = "assblastusa_thigh"
+
+/datum/sprite_accessory/clothing/socks/rainbow_thigh
+ name = "Thigh-high (Rainbow)"
+ icon_state = "rainbow_thigh"
+
+/datum/sprite_accessory/clothing/socks/striped_thigh
+ name = "Thigh-high (Striped)"
+ icon_state = "striped_thigh"
+
+/datum/sprite_accessory/clothing/socks/thin_thigh
+ name = "Thigh-high (Thin)"
+ icon_state = "thin_thigh"
+
+/datum/sprite_accessory/clothing/socks/trans_thigh
+ name = "Thigh-high (Trans)"
+ icon_state = "trans_thigh"
+
+/datum/sprite_accessory/clothing/socks/uk_thigh
+ name = "Thigh-high (UK)"
+ icon_state = "uk_thigh"
+
+/datum/sprite_accessory/clothing/socks/white_thigh
+ name = "Thigh-high (White)"
+ icon_state = "white_thigh"
+
+/datum/sprite_accessory/clothing/socks/fishnet_thigh
+ name = "Thigh-high (Fishnet)"
+ icon_state = "fishnet_thigh"
+
+/datum/sprite_accessory/clothing/socks/thocks
+ name = "Thocks"
+ icon_state = "thocks"
diff --git a/code/datums/sprite_accessories/ears.dm b/code/datums/sprite_accessories/ears.dm
new file mode 100644
index 000000000000..bc770a2ec884
--- /dev/null
+++ b/code/datums/sprite_accessories/ears.dm
@@ -0,0 +1,40 @@
+/datum/sprite_accessory/ears
+ icon = 'icons/mob/human/cat_features.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/ears/cat
+ name = "Cat"
+ icon_state = "cat"
+ color_src = HAIR_COLOR
+
+/datum/sprite_accessory/ears/cat/big
+ name = "Big"
+ icon_state = "big"
+
+/datum/sprite_accessory/ears/cat/miqo
+ name = "Coeurl"
+ icon_state = "miqo"
+
+/datum/sprite_accessory/ears/cat/fold
+ name = "Fold"
+ icon_state = "fold"
+
+/datum/sprite_accessory/ears/cat/lynx
+ name = "Lynx"
+ icon_state = "lynx"
+
+/datum/sprite_accessory/ears/cat/round
+ name = "Round"
+ icon_state = "round"
+
+/datum/sprite_accessory/ears/cat/cybernetic
+ name = "Cybernetic"
+ icon_state = "cyber"
+ locked = TRUE
+
+/datum/sprite_accessory/ears/fox
+ icon = 'icons/mob/human/fox_features.dmi'
+ name = "Fox"
+ icon_state = "fox"
+ color_src = HAIR_COLOR
+ locked = TRUE
diff --git a/code/datums/sprite_accessories/facial_hair.dm b/code/datums/sprite_accessories/facial_hair.dm
new file mode 100644
index 000000000000..1fd30435b4da
--- /dev/null
+++ b/code/datums/sprite_accessories/facial_hair.dm
@@ -0,0 +1,161 @@
+/datum/sprite_accessory/facial_hair
+ icon = 'icons/mob/human/human_face.dmi'
+ gender = MALE // barf (unless you're a dorf, dorfs dig chix w/ beards :P)
+ em_block = TRUE
+
+// please make sure they're sorted alphabetically and categorized
+
+/datum/sprite_accessory/facial_hair/abe
+ name = "Beard (Abraham Lincoln)"
+ icon_state = "facial_abe"
+
+/datum/sprite_accessory/facial_hair/brokenman
+ name = "Beard (Broken Man)"
+ icon_state = "facial_brokenman"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/facial_hair/chinstrap
+ name = "Beard (Chinstrap)"
+ icon_state = "facial_chin"
+
+/datum/sprite_accessory/facial_hair/dwarf
+ name = "Beard (Dwarf)"
+ icon_state = "facial_dwarf"
+
+/datum/sprite_accessory/facial_hair/fullbeard
+ name = "Beard (Full)"
+ icon_state = "facial_fullbeard"
+
+/datum/sprite_accessory/facial_hair/croppedfullbeard
+ name = "Beard (Cropped Fullbeard)"
+ icon_state = "facial_croppedfullbeard"
+
+/datum/sprite_accessory/facial_hair/gt
+ name = "Beard (Goatee)"
+ icon_state = "facial_gt"
+
+/datum/sprite_accessory/facial_hair/hip
+ name = "Beard (Hipster)"
+ icon_state = "facial_hip"
+
+/datum/sprite_accessory/facial_hair/jensen
+ name = "Beard (Jensen)"
+ icon_state = "facial_jensen"
+
+/datum/sprite_accessory/facial_hair/neckbeard
+ name = "Beard (Neckbeard)"
+ icon_state = "facial_neckbeard"
+
+/datum/sprite_accessory/facial_hair/vlongbeard
+ name = "Beard (Very Long)"
+ icon_state = "facial_wise"
+
+/datum/sprite_accessory/facial_hair/muttonmus
+ name = "Beard (Muttonmus)"
+ icon_state = "facial_muttonmus"
+
+/datum/sprite_accessory/facial_hair/martialartist
+ name = "Beard (Martial Artist)"
+ icon_state = "facial_martialartist"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/facial_hair/chinlessbeard
+ name = "Beard (Chinless Beard)"
+ icon_state = "facial_chinlessbeard"
+
+/datum/sprite_accessory/facial_hair/moonshiner
+ name = "Beard (Moonshiner)"
+ icon_state = "facial_moonshiner"
+
+/datum/sprite_accessory/facial_hair/longbeard
+ name = "Beard (Long)"
+ icon_state = "facial_longbeard"
+
+/datum/sprite_accessory/facial_hair/volaju
+ name = "Beard (Volaju)"
+ icon_state = "facial_volaju"
+
+/datum/sprite_accessory/facial_hair/threeoclock
+ name = "Beard (Three o Clock Shadow)"
+ icon_state = "facial_3oclock"
+
+/datum/sprite_accessory/facial_hair/fiveoclock
+ name = "Beard (Five o Clock Shadow)"
+ icon_state = "facial_fiveoclock"
+
+/datum/sprite_accessory/facial_hair/fiveoclockm
+ name = "Beard (Five o Clock Moustache)"
+ icon_state = "facial_5oclockmoustache"
+
+/datum/sprite_accessory/facial_hair/sevenoclock
+ name = "Beard (Seven o Clock Shadow)"
+ icon_state = "facial_7oclock"
+
+/datum/sprite_accessory/facial_hair/sevenoclockm
+ name = "Beard (Seven o Clock Moustache)"
+ icon_state = "facial_7oclockmoustache"
+
+/datum/sprite_accessory/facial_hair/moustache
+ name = "Moustache"
+ icon_state = "facial_moustache"
+
+/datum/sprite_accessory/facial_hair/pencilstache
+ name = "Moustache (Pencilstache)"
+ icon_state = "facial_pencilstache"
+
+/datum/sprite_accessory/facial_hair/smallstache
+ name = "Moustache (Smallstache)"
+ icon_state = "facial_smallstache"
+
+/datum/sprite_accessory/facial_hair/walrus
+ name = "Moustache (Walrus)"
+ icon_state = "facial_walrus"
+
+/datum/sprite_accessory/facial_hair/fu
+ name = "Moustache (Fu Manchu)"
+ icon_state = "facial_fumanchu"
+
+/datum/sprite_accessory/facial_hair/hogan
+ name = "Moustache (Hulk Hogan)"
+ icon_state = "facial_hogan" //-Neek
+
+/datum/sprite_accessory/facial_hair/selleck
+ name = "Moustache (Selleck)"
+ icon_state = "facial_selleck"
+
+/datum/sprite_accessory/facial_hair/chaplin
+ name = "Moustache (Square)"
+ icon_state = "facial_chaplin"
+
+/datum/sprite_accessory/facial_hair/vandyke
+ name = "Moustache (Van Dyke)"
+ icon_state = "facial_vandyke"
+
+/datum/sprite_accessory/facial_hair/watson
+ name = "Moustache (Watson)"
+ icon_state = "facial_watson"
+
+/datum/sprite_accessory/facial_hair/handlebar
+ name = "Moustache (Handlebar)"
+ icon_state = "facial_handlebar"
+
+/datum/sprite_accessory/facial_hair/handlebar2
+ name = "Moustache (Handlebar 2)"
+ icon_state = "facial_handlebar2"
+
+/datum/sprite_accessory/facial_hair/elvis
+ name = "Sideburns (Elvis)"
+ icon_state = "facial_elvis"
+
+/datum/sprite_accessory/facial_hair/mutton
+ name = "Sideburns (Mutton Chops)"
+ icon_state = "facial_mutton"
+
+/datum/sprite_accessory/facial_hair/sideburn
+ name = "Sideburns"
+ icon_state = "facial_sideburn"
+
+/datum/sprite_accessory/facial_hair/shaved
+ name = "Shaved"
+ icon_state = SPRITE_ACCESSORY_NONE
+ gender = NEUTER
diff --git a/code/datums/sprite_accessories/frills.dm b/code/datums/sprite_accessories/frills.dm
new file mode 100644
index 000000000000..b470da027899
--- /dev/null
+++ b/code/datums/sprite_accessories/frills.dm
@@ -0,0 +1,14 @@
+/datum/sprite_accessory/frills
+ icon = 'icons/mob/human/species/lizard/lizard_misc.dmi'
+
+/datum/sprite_accessory/frills/simple
+ name = "Simple"
+ icon_state = "simple"
+
+/datum/sprite_accessory/frills/short
+ name = "Short"
+ icon_state = "short"
+
+/datum/sprite_accessory/frills/aquatic
+ name = "Aquatic"
+ icon_state = "aqua"
diff --git a/code/datums/sprite_accessories/hair.dm b/code/datums/sprite_accessories/hair.dm
new file mode 100644
index 000000000000..7a2dbf475d77
--- /dev/null
+++ b/code/datums/sprite_accessories/hair.dm
@@ -0,0 +1,1022 @@
+/datum/hair_mask
+ var/icon/icon = 'icons/mob/human/hair_masks.dmi'
+ var/icon_state = ""
+ /// Strict coverage zones will always have the hair mask applied to them, even if a piece of hair at that location would normally resist being masked.
+ /// If a piece of headware only covers the top of the head, it should only strictly cover the top zone. But a mostly-enclosed helmet might strictly cover almost all zones.
+ var/strict_coverage_zones = NONE
+
+/datum/hair_mask/standard_hat_middle
+ icon_state = "hide_above_45deg"
+ strict_coverage_zones = HAIR_APPENDAGE_TOP
+
+/datum/hair_mask/standard_hat_low
+ icon_state = "hide_above_45deg_low"
+ strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR
+
+/datum/hair_mask/winterhood
+ icon_state = "hide_winterhood"
+ strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR
+
+/datum/hair_mask/hoodie
+ icon_state = "hide_hoodie"
+ strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR
+
+// Cache of each hairstyle's icon after being blended with the given masks
+// "joined mask types" is each mask's type as a string joined by commas (for no masks, it is the empty string)
+// /datum/sprite_accessory/hair path -> list(joined mask types -> icon)
+GLOBAL_LIST_EMPTY(blended_hair_icons_cache)
+
+/datum/sprite_accessory/hair
+ icon = 'icons/mob/human/human_face.dmi' // default icon for all hairs
+ var/y_offset = 0 // Y offset to apply so we can have hair that reaches above the player sprite's visual bounding box
+
+ // Some hair will have "appendages", such as pony tails, that stick out from certain parts of the head. These can be layered above or below headwear and resist being masked away by hair masks.
+ // Lists should be icon_state strings associated with the HAIR_APPENDAGE defines specifying the part of the head they stick out from.
+ // hair_appendages_inner contains icon_states that go in the normal hair layer, hair_appendages_outer contains icon_states that go above the layer for headwear.
+ // hair_appendages_inner will be masked normally if their HAIR_APPENDAGE zone is strictly masked by a piece of clothing (a fully enclosed helmet with a transparent visor will strictly mask all zones, a small hat will only strictly mask the top, etc.).
+ // hair_appendages_outer will never be masked at all and will just not be shown if their zone has strict masking. These should generally not have visible sprites for every dir.
+ var/list/hair_appendages_inner = null
+ var/list/hair_appendages_outer = null
+
+/// Retrieve the base hair icon with all hair appendeges blended in, with hair masks applied, from the cache, or generate it if it doesn't exist
+/datum/sprite_accessory/hair/proc/getCachedIcon(list/hair_masks)
+ var/icon/cachedIcon
+ var/joinedMasks = LAZYLEN(hair_masks) ? jointext(hair_masks, ",") : ""
+ var/list/masks_to_icons = GLOB.blended_hair_icons_cache[type]
+ if(!masks_to_icons)
+ GLOB.blended_hair_icons_cache[type] = list()
+ else
+ cachedIcon = masks_to_icons[joinedMasks]
+
+ if(!cachedIcon)
+ if(LAZYLEN(hair_masks))
+ if(LAZYLEN(hair_appendages_inner))
+ // Check if there are any hair appendages in a zone that is not strictly masked
+ var/found_mask_dodger = FALSE
+ for(var/datum/hair_mask/mask as anything in hair_masks)
+ for(var/appendage in hair_appendages_inner)
+ var/zone = hair_appendages_inner[appendage]
+ if(!(zone & mask.strict_coverage_zones))
+ found_mask_dodger = TRUE
+
+ if(found_mask_dodger)
+ // We have to process each icon individually
+ cachedIcon = icon(icon, icon_state)
+ // mask the base icon
+ for(var/datum/hair_mask/mask as anything in hair_masks)
+ var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state)
+ mask_icon.Shift(SOUTH, y_offset)
+ cachedIcon.Blend(mask_icon, ICON_ADD)
+
+ // mask the appendages if required and add them to the base icon
+ for(var/appendage_icon_state in hair_appendages_inner)
+ var/icon/appendage_icon = icon(icon, appendage_icon_state)
+ var/zone = hair_appendages_inner[appendage_icon_state]
+ for(var/datum/hair_mask/mask as anything in hair_masks)
+ if(zone & mask.strict_coverage_zones)
+ var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state)
+ mask_icon.Shift(SOUTH, y_offset)
+ appendage_icon.Blend(mask_icon, ICON_ADD)
+ cachedIcon.Blend(appendage_icon, ICON_OVERLAY)
+ else
+ // No mask dodgers, so we can just mask the full (hopefully cached) icon
+ cachedIcon = icon(getCachedIcon())
+ for(var/datum/hair_mask/mask as anything in hair_masks)
+ var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state)
+ mask_icon.Shift(SOUTH, y_offset)
+ cachedIcon.Blend(mask_icon, ICON_ADD)
+ else
+ // No hair appendages, so just apply all hair masks to the base icon
+ cachedIcon = icon(icon, icon_state)
+ for(var/datum/hair_mask/mask as anything in hair_masks)
+ var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state)
+ mask_icon.Shift(SOUTH, y_offset)
+ cachedIcon.Blend(mask_icon, ICON_ADD)
+ else
+ // no hair masks
+ cachedIcon = icon(icon, icon_state)
+ if(LAZYLEN(hair_appendages_inner))
+ for(var/appendage_icon_state in hair_appendages_inner)
+ var/icon/appendage_icon = icon(icon, appendage_icon_state)
+ cachedIcon.Blend(appendage_icon, ICON_OVERLAY)
+ // set cache
+ GLOB.blended_hair_icons_cache[type][joinedMasks] = cachedIcon
+ return cachedIcon
+
+
+// please make sure they're sorted alphabetically and, where needed, categorized
+// try to capitalize the names please~
+// try to spell
+// you do not need to define _s or _l sub-states, game automatically does this for you
+
+/datum/sprite_accessory/hair/afro
+ name = "Afro"
+ icon_state = "hair_afro"
+
+/datum/sprite_accessory/hair/afro2
+ name = "Afro 2"
+ icon_state = "hair_afro2"
+
+/datum/sprite_accessory/hair/afro_large
+ name = "Afro (Large)"
+ icon_state = "hair_bigafro"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/afro_huge
+ name = "Afro (Huge)"
+ icon_state = "hair_hugeafro"
+ y_offset = 6
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/allthefuzz
+ name = "All The Fuzz"
+ icon_state = "hair_allthefuzz"
+
+/datum/sprite_accessory/hair/antenna
+ name = "Ahoge"
+ icon_state = "hair_antenna"
+ hair_appendages_inner = list("hair_antenna_a1" = HAIR_APPENDAGE_TOP)
+
+/datum/sprite_accessory/hair/bald
+ name = "Bald"
+ icon_state = null
+
+/datum/sprite_accessory/hair/balding
+ name = "Balding Hair"
+ icon_state = "hair_e"
+
+/datum/sprite_accessory/hair/bedhead
+ name = "Bedhead"
+ icon_state = "hair_bedhead"
+
+/datum/sprite_accessory/hair/bedhead2
+ name = "Bedhead 2"
+ icon_state = "hair_bedheadv2"
+
+/datum/sprite_accessory/hair/bedhead3
+ name = "Bedhead 3"
+ icon_state = "hair_bedheadv3"
+
+/datum/sprite_accessory/hair/bedheadv4
+ name = "Bedhead 4x"
+ icon_state = "hair_bedheadv4"
+
+/datum/sprite_accessory/hair/bedheadlong
+ name = "Long Bedhead"
+ icon_state = "hair_long_bedhead"
+
+/datum/sprite_accessory/hair/bedheadfloorlength
+ name = "Floorlength Bedhead"
+ icon_state = "hair_floorlength_bedhead"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/badlycut
+ name = "Shorter Long Bedhead"
+ icon_state = "hair_verybadlycut"
+
+/datum/sprite_accessory/hair/beehive
+ name = "Beehive"
+ icon_state = "hair_beehive"
+
+/datum/sprite_accessory/hair/beehive2
+ name = "Beehive 2"
+ icon_state = "hair_beehivev2"
+
+/datum/sprite_accessory/hair/bob
+ name = "Bob Hair"
+ icon_state = "hair_bob"
+
+/datum/sprite_accessory/hair/bob2
+ name = "Bob Hair 2"
+ icon_state = "hair_bob2"
+
+/datum/sprite_accessory/hair/bob3
+ name = "Bob Hair 3"
+ icon_state = "hair_bobcut"
+
+/datum/sprite_accessory/hair/bob4
+ name = "Bob Hair 4"
+ icon_state = "hair_bob4"
+
+/datum/sprite_accessory/hair/bobcurl
+ name = "Bobcurl"
+ icon_state = "hair_bobcurl"
+
+/datum/sprite_accessory/hair/boddicker
+ name = "Boddicker"
+ icon_state = "hair_boddicker"
+
+/datum/sprite_accessory/hair/bowlcut
+ name = "Bowlcut"
+ icon_state = "hair_bowlcut"
+
+/datum/sprite_accessory/hair/bowlcut2
+ name = "Bowlcut 2"
+ icon_state = "hair_bowlcut2"
+
+/datum/sprite_accessory/hair/braid
+ name = "Braid (Floorlength)"
+ icon_state = "hair_braid"
+ hair_appendages_inner = list("hair_braid_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_braid_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/braided
+ name = "Braided"
+ icon_state = "hair_braided"
+
+/datum/sprite_accessory/hair/front_braid
+ name = "Braided Front"
+ icon_state = "hair_braidfront"
+ hair_appendages_inner = list("hair_braidfront_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_braidfront_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/not_floorlength_braid
+ name = "Braid (High)"
+ icon_state = "hair_braid2"
+ hair_appendages_inner = list("hair_braid2_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_braid2_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/lowbraid
+ name = "Braid (Low)"
+ icon_state = "hair_hbraid"
+
+/datum/sprite_accessory/hair/shortbraid
+ name = "Braid (Short)"
+ icon_state = "hair_shortbraid"
+ hair_appendages_inner = list("hair_shortbraid_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_shortbraid_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/braidtail
+ name = "Braided Tail"
+ icon_state = "hair_braidtail"
+ hair_appendages_inner = list("hair_braidtail_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_braidtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/bun
+ name = "Bun Head"
+ icon_state = "hair_bun"
+
+/datum/sprite_accessory/hair/bun2
+ name = "Bun Head 2"
+ icon_state = "hair_bunhead2"
+ hair_appendages_inner = list("hair_bunhead2_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_bunhead2_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/bun3
+ name = "Bun Head 3"
+ icon_state = "hair_bun3"
+
+/datum/sprite_accessory/hair/largebun
+ name = "Bun (Large)"
+ icon_state = "hair_largebun"
+
+/datum/sprite_accessory/hair/manbun
+ name = "Bun (Manbun)"
+ icon_state = "hair_manbun"
+ hair_appendages_inner = list("hair_manbun_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_manbun_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/tightbun
+ name = "Bun (Tight)"
+ icon_state = "hair_tightbun"
+
+/datum/sprite_accessory/hair/business
+ name = "Business Hair"
+ icon_state = "hair_business"
+
+/datum/sprite_accessory/hair/business2
+ name = "Business Hair 2"
+ icon_state = "hair_business2"
+
+/datum/sprite_accessory/hair/business3
+ name = "Business Hair 3"
+ icon_state = "hair_business3"
+
+/datum/sprite_accessory/hair/business4
+ name = "Business Hair 4"
+ icon_state = "hair_business4"
+
+/datum/sprite_accessory/hair/buzz
+ name = "Buzzcut"
+ icon_state = "hair_buzzcut"
+
+/datum/sprite_accessory/hair/chinbob
+ name = "Chin-Length Bob Cut"
+ icon_state = "hair_chinbob"
+
+/datum/sprite_accessory/hair/comet
+ name = "Comet"
+ icon_state = "hair_comet"
+
+/datum/sprite_accessory/hair/cia
+ name = "CIA"
+ icon_state = "hair_cia"
+
+/datum/sprite_accessory/hair/coffeehouse
+ name = "Coffee House"
+ icon_state = "hair_coffeehouse"
+
+/datum/sprite_accessory/hair/combover
+ name = "Combover"
+ icon_state = "hair_combover"
+
+/datum/sprite_accessory/hair/cornrows1
+ name = "Cornrows"
+ icon_state = "hair_cornrows"
+
+/datum/sprite_accessory/hair/cornrows2
+ name = "Cornrows 2"
+ icon_state = "hair_cornrows2"
+
+/datum/sprite_accessory/hair/cornrowbun
+ name = "Cornrow Bun"
+ icon_state = "hair_cornrowbun"
+
+/datum/sprite_accessory/hair/cornrowbraid
+ name = "Cornrow Braid"
+ icon_state = "hair_cornrowbraid"
+
+/datum/sprite_accessory/hair/cornrowdualtail
+ name = "Cornrow Tail"
+ icon_state = "hair_cornrowtail"
+ hair_appendages_inner = list("hair_cornrowtail_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_cornrowtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/crew
+ name = "Crewcut"
+ icon_state = "hair_crewcut"
+
+/datum/sprite_accessory/hair/curls
+ name = "Curls"
+ icon_state = "hair_curls"
+
+/datum/sprite_accessory/hair/cut
+ name = "Cut Hair"
+ icon_state = "hair_c"
+
+/datum/sprite_accessory/hair/dandpompadour
+ name = "Dandy Pompadour"
+ icon_state = "hair_dandypompadour"
+
+/datum/sprite_accessory/hair/devillock
+ name = "Devil Lock"
+ icon_state = "hair_devilock"
+
+/datum/sprite_accessory/hair/doublebun
+ name = "Double Bun"
+ icon_state = "hair_doublebun"
+ hair_appendages_inner = list("hair_doublebun_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_doublebun_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/dreadlocks
+ name = "Dreadlocks"
+ icon_state = "hair_dreads"
+
+/datum/sprite_accessory/hair/drillhair
+ name = "Drillruru"
+ icon_state = "hair_drillruru"
+ hair_appendages_inner = list("hair_drillruru_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_drillruru_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/drillhairextended
+ name = "Drill Hair (Extended)"
+ icon_state = "hair_drillhairextended"
+ hair_appendages_inner = list("hair_drillhairextended_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_drillhairextended_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/emo
+ name = "Emo"
+ icon_state = "hair_emo"
+
+/datum/sprite_accessory/hair/emofrine
+ name = "Emo Fringe"
+ icon_state = "hair_emofringe"
+
+/datum/sprite_accessory/hair/nofade
+ name = "Fade (None)"
+ icon_state = "hair_nofade"
+
+/datum/sprite_accessory/hair/highfade
+ name = "Fade (High)"
+ icon_state = "hair_highfade"
+
+/datum/sprite_accessory/hair/medfade
+ name = "Fade (Medium)"
+ icon_state = "hair_medfade"
+
+/datum/sprite_accessory/hair/lowfade
+ name = "Fade (Low)"
+ icon_state = "hair_lowfade"
+
+/datum/sprite_accessory/hair/baldfade
+ name = "Fade (Bald)"
+ icon_state = "hair_baldfade"
+
+/datum/sprite_accessory/hair/feather
+ name = "Feather"
+ icon_state = "hair_feather"
+
+/datum/sprite_accessory/hair/father
+ name = "Father"
+ icon_state = "hair_father"
+
+/datum/sprite_accessory/hair/sargeant
+ name = "Flat Top"
+ icon_state = "hair_sargeant"
+
+/datum/sprite_accessory/hair/flair
+ name = "Flair"
+ icon_state = "hair_flair"
+
+/datum/sprite_accessory/hair/bigflattop
+ name = "Flat Top (Big)"
+ icon_state = "hair_bigflattop"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/flow_hair
+ name = "Flow Hair"
+ icon_state = "hair_f"
+
+/datum/sprite_accessory/hair/gelled
+ name = "Gelled Back"
+ icon_state = "hair_gelled"
+
+/datum/sprite_accessory/hair/gentle
+ name = "Gentle"
+ icon_state = "hair_gentle"
+
+/datum/sprite_accessory/hair/halfbang
+ name = "Half-banged Hair"
+ icon_state = "hair_halfbang"
+
+/datum/sprite_accessory/hair/halfbang2
+ name = "Half-banged Hair 2"
+ icon_state = "hair_halfbang2"
+
+/datum/sprite_accessory/hair/halfshaved
+ name = "Half-shaved"
+ icon_state = "hair_halfshaved"
+
+/datum/sprite_accessory/hair/hedgehog
+ name = "Hedgehog Hair"
+ icon_state = "hair_hedgehog"
+
+/datum/sprite_accessory/hair/himecut
+ name = "Hime Cut"
+ icon_state = "hair_himecut"
+
+/datum/sprite_accessory/hair/himecut2
+ name = "Hime Cut 2"
+ icon_state = "hair_himecut2"
+
+/datum/sprite_accessory/hair/shorthime
+ name = "Hime Cut (Short)"
+ icon_state = "hair_shorthime"
+
+/datum/sprite_accessory/hair/himeup
+ name = "Hime Updo"
+ icon_state = "hair_himeup"
+
+/datum/sprite_accessory/hair/hitop
+ name = "Hitop"
+ icon_state = "hair_hitop"
+
+/datum/sprite_accessory/hair/jade
+ name = "Jade"
+ icon_state = "hair_jade"
+
+/datum/sprite_accessory/hair/jensen
+ name = "Jensen Hair"
+ icon_state = "hair_jensen"
+
+/datum/sprite_accessory/hair/joestar
+ name = "Joestar"
+ icon_state = "hair_joestar"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/keanu
+ name = "Keanu Hair"
+ icon_state = "hair_keanu"
+
+/datum/sprite_accessory/hair/kusangi
+ name = "Kusanagi Hair"
+ icon_state = "hair_kusanagi"
+
+/datum/sprite_accessory/hair/long
+ name = "Long Hair 1"
+ icon_state = "hair_long"
+ hair_appendages_inner = list("hair_long_a1" = HAIR_APPENDAGE_HANGING_REAR)
+
+/datum/sprite_accessory/hair/long2
+ name = "Long Hair 2"
+ icon_state = "hair_long2"
+ hair_appendages_inner = list("hair_long2_a1" = HAIR_APPENDAGE_HANGING_REAR)
+
+/datum/sprite_accessory/hair/long3
+ name = "Long Hair 3"
+ icon_state = "hair_long3"
+ hair_appendages_inner = list("hair_long3_a1" = HAIR_APPENDAGE_HANGING_REAR)
+
+/datum/sprite_accessory/hair/long_over_eye
+ name = "Long Over Eye"
+ icon_state = "hair_longovereye"
+
+/datum/sprite_accessory/hair/longbangs
+ name = "Long Bangs"
+ icon_state = "hair_lbangs"
+
+/datum/sprite_accessory/hair/longemo
+ name = "Long Emo"
+ icon_state = "hair_longemo"
+
+/datum/sprite_accessory/hair/longfringe
+ name = "Long Fringe"
+ icon_state = "hair_longfringe"
+
+/datum/sprite_accessory/hair/sidepartlongalt
+ name = "Long Side Part"
+ icon_state = "hair_longsidepart"
+ hair_appendages_inner = list("hair_longsidepart_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_longsidepart_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/megaeyebrows
+ name = "Mega Eyebrows"
+ icon_state = "hair_megaeyebrows"
+
+/datum/sprite_accessory/hair/messy
+ name = "Messy"
+ icon_state = "hair_messy"
+
+/datum/sprite_accessory/hair/modern
+ name = "Modern"
+ icon_state = "hair_modern"
+
+/datum/sprite_accessory/hair/mohawk
+ name = "Mohawk"
+ icon_state = "hair_d"
+ natural_spawn = FALSE // sorry little one
+
+/datum/sprite_accessory/hair/nitori
+ name = "Nitori"
+ icon_state = "hair_nitori"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/reversemohawk
+ name = "Mohawk (Reverse)"
+ icon_state = "hair_reversemohawk"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/shavedmohawk
+ name = "Mohawk (Shaved)"
+ icon_state = "hair_shavedmohawk"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/unshavenmohawk
+ name = "Mohawk (Unshaven)"
+ icon_state = "hair_unshaven_mohawk"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/mulder
+ name = "Mulder"
+ icon_state = "hair_mulder"
+
+/datum/sprite_accessory/hair/odango
+ name = "Odango"
+ icon_state = "hair_odango"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/ombre
+ name = "Ombre"
+ icon_state = "hair_ombre"
+
+/datum/sprite_accessory/hair/oneshoulder
+ name = "One Shoulder"
+ icon_state = "hair_oneshoulder"
+
+/datum/sprite_accessory/hair/over_eye
+ name = "Over Eye"
+ icon_state = "hair_shortovereye"
+
+/datum/sprite_accessory/hair/hair_overeyetwo
+ name = "Over Eye 2"
+ icon_state = "hair_overeyetwo"
+
+/datum/sprite_accessory/hair/oxton
+ name = "Oxton"
+ icon_state = "hair_oxton"
+
+/datum/sprite_accessory/hair/parted
+ name = "Parted"
+ icon_state = "hair_parted"
+
+/datum/sprite_accessory/hair/partedside
+ name = "Parted (Side)"
+ icon_state = "hair_part"
+
+/datum/sprite_accessory/hair/kagami
+ name = "Pigtails"
+ icon_state = "hair_kagami"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/pigtail
+ name = "Pigtails 2"
+ icon_state = "hair_pigtails"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/hair/pigtail2
+ name = "Pigtails 3"
+ icon_state = "hair_pigtails2"
+ natural_spawn = FALSE
+ hair_appendages_inner = list("hair_pigtails2_a1" = HAIR_APPENDAGE_LEFT, "hair_pigtails2_a2" = HAIR_APPENDAGE_RIGHT)
+
+/datum/sprite_accessory/hair/pixie
+ name = "Pixie Cut"
+ icon_state = "hair_pixie"
+
+/datum/sprite_accessory/hair/pompadour
+ name = "Pompadour"
+ icon_state = "hair_pompadour"
+
+/datum/sprite_accessory/hair/bigpompadour
+ name = "Pompadour (Big)"
+ icon_state = "hair_bigpompadour"
+
+/datum/sprite_accessory/hair/ponytail1
+ name = "Ponytail"
+ icon_state = "hair_ponytail"
+
+/datum/sprite_accessory/hair/ponytail2
+ name = "Ponytail 2"
+ icon_state = "hair_ponytail2"
+
+/datum/sprite_accessory/hair/ponytail3
+ name = "Ponytail 3"
+ icon_state = "hair_ponytail3"
+
+/datum/sprite_accessory/hair/ponytail4
+ name = "Ponytail 4"
+ icon_state = "hair_ponytail4"
+ hair_appendages_inner = list("hair_ponytail4_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_ponytail4_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/ponytail5
+ name = "Ponytail 5"
+ icon_state = "hair_ponytail5"
+ hair_appendages_inner = list("hair_ponytail5_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_ponytail5_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/ponytail6
+ name = "Ponytail 6"
+ icon_state = "hair_ponytail6"
+ hair_appendages_inner = list("hair_ponytail6_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_ponytail6_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/ponytail7
+ name = "Ponytail 7"
+ icon_state = "hair_ponytail7"
+ hair_appendages_inner = list("hair_ponytail7_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_ponytail7_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/highponytail
+ name = "Ponytail (High)"
+ icon_state = "hair_highponytail"
+ hair_appendages_inner = list("hair_highponytail_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_highponytail_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/stail
+ name = "Ponytail (Short)"
+ icon_state = "hair_stail"
+ hair_appendages_inner = list("hair_stail_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_stail_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/longponytail
+ name = "Ponytail (Long)"
+ icon_state = "hair_longstraightponytail"
+ hair_appendages_inner = list("hair_longstraightponytail_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_longstraightponytail_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/countryponytail
+ name = "Ponytail (Country)"
+ icon_state = "hair_country"
+ hair_appendages_inner = list("hair_country_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_country_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/fringetail
+ name = "Ponytail (Fringe)"
+ icon_state = "hair_fringetail"
+
+/datum/sprite_accessory/hair/sidetail
+ name = "Ponytail (Side)"
+ icon_state = "hair_sidetail"
+
+/datum/sprite_accessory/hair/sidetail2
+ name = "Ponytail (Side) 2"
+ icon_state = "hair_sidetail2"
+
+/datum/sprite_accessory/hair/sidetail3
+ name = "Ponytail (Side) 3"
+ icon_state = "hair_sidetail3"
+ hair_appendages_inner = list("hair_sidetail3_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_sidetail3_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/sidetail4
+ name = "Ponytail (Side) 4"
+ icon_state = "hair_sidetail4"
+ hair_appendages_inner = list("hair_sidetail4_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_sidetail4_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/spikyponytail
+ name = "Ponytail (Spiky)"
+ icon_state = "hair_spikyponytail"
+ hair_appendages_inner = list("hair_spikyponytail_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_spikyponytail_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/poofy
+ name = "Poofy"
+ icon_state = "hair_poofy"
+
+/datum/sprite_accessory/hair/quiff
+ name = "Quiff"
+ icon_state = "hair_quiff"
+
+/datum/sprite_accessory/hair/ronin
+ name = "Ronin"
+ icon_state = "hair_ronin"
+
+/datum/sprite_accessory/hair/shaved
+ name = "Shaved"
+ icon_state = "hair_shaved"
+
+/datum/sprite_accessory/hair/shavedpart
+ name = "Shaved Part"
+ icon_state = "hair_shavedpart"
+
+/datum/sprite_accessory/hair/shortbangs
+ name = "Short Bangs"
+ icon_state = "hair_shortbangs"
+
+/datum/sprite_accessory/hair/shortbangs2
+ name = "Short Bangs 2"
+ icon_state = "hair_shortbangs2"
+
+/datum/sprite_accessory/hair/short
+ name = "Short Hair"
+ icon_state = "hair_a"
+
+/datum/sprite_accessory/hair/shorthair2
+ name = "Short Hair 2"
+ icon_state = "hair_shorthair2"
+
+/datum/sprite_accessory/hair/shorthair3
+ name = "Short Hair 3"
+ icon_state = "hair_shorthair3"
+
+/datum/sprite_accessory/hair/shorthair4
+ name = "Short Hair 4"
+ icon_state = "hair_d"
+
+/datum/sprite_accessory/hair/shorthair5
+ name = "Short Hair 5"
+ icon_state = "hair_e"
+
+/datum/sprite_accessory/hair/shorthair6
+ name = "Short Hair 6"
+ icon_state = "hair_f"
+
+/datum/sprite_accessory/hair/shorthair7
+ name = "Short Hair 7"
+ icon_state = "hair_shorthairg"
+
+/datum/sprite_accessory/hair/shorthaireighties
+ name = "Short Hair 80s"
+ icon_state = "hair_80s"
+
+/datum/sprite_accessory/hair/rosa
+ name = "Short Hair Rosa"
+ icon_state = "hair_rosa"
+
+/datum/sprite_accessory/hair/shoulderlength
+ name = "Shoulder-length Hair"
+ icon_state = "hair_b"
+
+/datum/sprite_accessory/hair/sidecut
+ name = "Sidecut"
+ icon_state = "hair_sidecut"
+
+/datum/sprite_accessory/hair/skinhead
+ name = "Skinhead"
+ icon_state = "hair_skinhead"
+
+/datum/sprite_accessory/hair/protagonist
+ name = "Slightly Long Hair"
+ icon_state = "hair_protagonist"
+
+/datum/sprite_accessory/hair/spiky
+ name = "Spiky"
+ icon_state = "hair_spikey"
+
+/datum/sprite_accessory/hair/spiky2
+ name = "Spiky 2"
+ icon_state = "hair_spiky"
+
+/datum/sprite_accessory/hair/spiky3
+ name = "Spiky 3"
+ icon_state = "hair_spiky2"
+
+/datum/sprite_accessory/hair/swept
+ name = "Swept Back Hair"
+ icon_state = "hair_swept"
+
+/datum/sprite_accessory/hair/swept2
+ name = "Swept Back Hair 2"
+ icon_state = "hair_swept2"
+
+/datum/sprite_accessory/hair/thinning
+ name = "Thinning"
+ icon_state = "hair_thinning"
+
+/datum/sprite_accessory/hair/thinningfront
+ name = "Thinning (Front)"
+ icon_state = "hair_thinningfront"
+
+/datum/sprite_accessory/hair/thinningrear
+ name = "Thinning (Rear)"
+ icon_state = "hair_thinningrear"
+
+/datum/sprite_accessory/hair/topknot
+ name = "Topknot"
+ icon_state = "hair_topknot"
+
+/datum/sprite_accessory/hair/tressshoulder
+ name = "Tress Shoulder"
+ icon_state = "hair_tressshoulder"
+ hair_appendages_inner = list("hair_tressshoulder_a1" = HAIR_APPENDAGE_HANGING_FRONT)
+ hair_appendages_outer = list("hair_tressshoulder_a1o" = HAIR_APPENDAGE_HANGING_FRONT)
+
+/datum/sprite_accessory/hair/trimmed
+ name = "Trimmed"
+ icon_state = "hair_trimmed"
+
+/datum/sprite_accessory/hair/trimflat
+ name = "Trim Flat"
+ icon_state = "hair_trimflat"
+
+/datum/sprite_accessory/hair/twintails
+ name = "Twintails"
+ icon_state = "hair_twintail"
+
+/datum/sprite_accessory/hair/undercut
+ name = "Undercut"
+ icon_state = "hair_undercut"
+
+/datum/sprite_accessory/hair/undercutleft
+ name = "Undercut Left"
+ icon_state = "hair_undercutleft"
+
+/datum/sprite_accessory/hair/undercutright
+ name = "Undercut Right"
+ icon_state = "hair_undercutright"
+
+/datum/sprite_accessory/hair/unkept
+ name = "Unkept"
+ icon_state = "hair_unkept"
+
+/datum/sprite_accessory/hair/updo
+ name = "Updo"
+ icon_state = "hair_updo"
+ hair_appendages_inner = list("hair_updo_a1" = HAIR_APPENDAGE_TOP)
+
+/datum/sprite_accessory/hair/longer
+ name = "Very Long Hair"
+ icon_state = "hair_vlong"
+
+/datum/sprite_accessory/hair/longest
+ name = "Very Long Hair 2"
+ icon_state = "hair_longest"
+
+/datum/sprite_accessory/hair/longest2
+ name = "Very Long Over Eye"
+ icon_state = "hair_longest2"
+
+/datum/sprite_accessory/hair/veryshortovereye
+ name = "Very Short Over Eye"
+ icon_state = "hair_veryshortovereyealternate"
+
+/datum/sprite_accessory/hair/longestalt
+ name = "Very Long with Fringe"
+ icon_state = "hair_vlongfringe"
+
+/datum/sprite_accessory/hair/volaju
+ name = "Volaju"
+ icon_state = "hair_volaju"
+
+/datum/sprite_accessory/hair/wisp
+ name = "Wisp"
+ icon_state = "hair_wisp"
+ hair_appendages_inner = list("hair_wisp_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_wisp_a1o" = HAIR_APPENDAGE_REAR)
+
+/datum/sprite_accessory/hair/ziegler
+ name = "Ziegler"
+ icon_state = "hair_ziegler"
+ hair_appendages_inner = list("hair_ziegler_a1" = HAIR_APPENDAGE_REAR)
+ hair_appendages_outer = list("hair_ziegler_a1o" = HAIR_APPENDAGE_REAR)
+
+/*
+/////////////////////////////////////
+/ =---------------------------= /
+/ == Gradient Hair Definitions == /
+/ =---------------------------= /
+/////////////////////////////////////
+*/
+
+/datum/sprite_accessory/gradient
+ icon = 'icons/mob/human/species/hair_gradients.dmi'
+ ///whether this gradient applies to hair and/or beards. Some gradients do not work well on beards.
+ var/gradient_category = GRADIENT_APPLIES_TO_HAIR|GRADIENT_APPLIES_TO_FACIAL_HAIR
+
+/datum/sprite_accessory/gradient/none
+ name = SPRITE_ACCESSORY_NONE
+ icon_state = SPRITE_ACCESSORY_NONE
+
+/datum/sprite_accessory/gradient/full
+ name = "Full"
+ icon_state = "full"
+
+/datum/sprite_accessory/gradient/fadeup
+ name = "Fade Up"
+ icon_state = "fadeup"
+
+/datum/sprite_accessory/gradient/fadedown
+ name = "Fade Down"
+ icon_state = "fadedown"
+
+/datum/sprite_accessory/gradient/vertical_split
+ name = "Vertical Split"
+ icon_state = "vsplit"
+
+/datum/sprite_accessory/gradient/horizontal_split
+ name = "Horizontal Split"
+ icon_state = "bottomflat"
+
+/datum/sprite_accessory/gradient/reflected
+ name = "Reflected"
+ icon_state = "reflected_high"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/reflected/beard
+ icon_state = "reflected_high_beard"
+ gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR
+
+/datum/sprite_accessory/gradient/reflected_inverse
+ name = "Reflected Inverse"
+ icon_state = "reflected_inverse_high"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/reflected_inverse/beard
+ icon_state = "reflected_inverse_high_beard"
+ gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR
+
+/datum/sprite_accessory/gradient/wavy
+ name = "Wavy"
+ icon_state = "wavy"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/long_fade_up
+ name = "Long Fade Up"
+ icon_state = "long_fade_up"
+
+/datum/sprite_accessory/gradient/long_fade_down
+ name = "Long Fade Down"
+ icon_state = "long_fade_down"
+
+/datum/sprite_accessory/gradient/short_fade_up
+ name = "Short Fade Up"
+ icon_state = "short_fade_up"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/short_fade_up/beard
+ icon_state = "short_fade_down"
+ gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR
+
+/datum/sprite_accessory/gradient/short_fade_down
+ name = "Short Fade Down"
+ icon_state = "short_fade_down_beard"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/short_fade_down/beard
+ icon_state = "short_fade_down_beard"
+ gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR
+
+/datum/sprite_accessory/gradient/wavy_spike
+ name = "Spiked Wavy"
+ icon_state = "wavy_spiked"
+ gradient_category = GRADIENT_APPLIES_TO_HAIR
+
+/datum/sprite_accessory/gradient/striped
+ name = "striped"
+ icon_state = "striped"
+
+/datum/sprite_accessory/gradient/striped_vertical
+ name = "Striped Vertical"
+ icon_state = "striped_vertical"
diff --git a/code/datums/sprite_accessories/horns.dm b/code/datums/sprite_accessories/horns.dm
new file mode 100644
index 000000000000..dc9331281777
--- /dev/null
+++ b/code/datums/sprite_accessories/horns.dm
@@ -0,0 +1,23 @@
+/datum/sprite_accessory/horns
+ icon = 'icons/mob/human/species/lizard/lizard_misc.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/horns/simple
+ name = "Simple"
+ icon_state = "simple"
+
+/datum/sprite_accessory/horns/short
+ name = "Short"
+ icon_state = "short"
+
+/datum/sprite_accessory/horns/curled
+ name = "Curled"
+ icon_state = "curled"
+
+/datum/sprite_accessory/horns/ram
+ name = "Ram"
+ icon_state = "ram"
+
+/datum/sprite_accessory/horns/angler
+ name = "Angeler"
+ icon_state = "angler"
diff --git a/code/datums/sprite_accessories/markings.dm b/code/datums/sprite_accessories/markings.dm
new file mode 100644
index 000000000000..d8e483a80ec7
--- /dev/null
+++ b/code/datums/sprite_accessories/markings.dm
@@ -0,0 +1,85 @@
+/datum/sprite_accessory/lizard_markings
+ icon = 'icons/mob/human/species/lizard/lizard_markings.dmi'
+
+/datum/sprite_accessory/lizard_markings/dtiger
+ name = "Dark Tiger Body"
+ icon_state = "dtiger"
+ gender_specific = TRUE
+
+/datum/sprite_accessory/lizard_markings/ltiger
+ name = "Light Tiger Body"
+ icon_state = "ltiger"
+ gender_specific = TRUE
+
+/datum/sprite_accessory/lizard_markings/lbelly
+ name = "Light Belly"
+ icon_state = "lbelly"
+ gender_specific = TRUE
+
+/datum/sprite_accessory/moth_markings // the markings that moths can have. finally something other than the boring tan
+ icon = 'icons/mob/human/species/moth/moth_markings.dmi'
+ color_src = null
+
+/datum/sprite_accessory/moth_markings/reddish
+ name = "Reddish"
+ icon_state = "reddish"
+
+/datum/sprite_accessory/moth_markings/royal
+ name = "Royal"
+ icon_state = "royal"
+
+/datum/sprite_accessory/moth_markings/gothic
+ name = "Gothic"
+ icon_state = "gothic"
+
+/datum/sprite_accessory/moth_markings/whitefly
+ name = "White Fly"
+ icon_state = "whitefly"
+
+/datum/sprite_accessory/moth_markings/lovers
+ name = "Lovers"
+ icon_state = "lovers"
+
+/datum/sprite_accessory/moth_markings/burnt_off
+ name = "Burnt Off"
+ icon_state = "burnt_off"
+
+/datum/sprite_accessory/moth_markings/firewatch
+ name = "Firewatch"
+ icon_state = "firewatch"
+
+/datum/sprite_accessory/moth_markings/deathhead
+ name = "Deathshead"
+ icon_state = "deathhead"
+
+/datum/sprite_accessory/moth_markings/poison
+ name = "Poison"
+ icon_state = "poison"
+
+/datum/sprite_accessory/moth_markings/ragged
+ name = "Ragged"
+ icon_state = "ragged"
+
+/datum/sprite_accessory/moth_markings/moonfly
+ name = "Moon Fly"
+ icon_state = "moonfly"
+
+/datum/sprite_accessory/moth_markings/oakworm
+ name = "Oak Worm"
+ icon_state = "oakworm"
+
+/datum/sprite_accessory/moth_markings/jungle
+ name = "Jungle"
+ icon_state = "jungle"
+
+/datum/sprite_accessory/moth_markings/witchwing
+ name = "Witch Wing"
+ icon_state = "witchwing"
+
+/datum/sprite_accessory/moth_markings/lightbearer
+ name = "Lightbearer"
+ icon_state = "lightbearer"
+
+/datum/sprite_accessory/moth_markings/dipped
+ name = "Dipped"
+ icon_state = "dipped"
diff --git a/code/datums/sprite_accessories/moth_antennae.dm b/code/datums/sprite_accessories/moth_antennae.dm
new file mode 100644
index 000000000000..54a8cdc979a0
--- /dev/null
+++ b/code/datums/sprite_accessories/moth_antennae.dm
@@ -0,0 +1,94 @@
+/datum/sprite_accessory/moth_antennae //Finally splitting the sprite
+ icon = 'icons/mob/human/species/moth/moth_antennae.dmi'
+ color_src = null
+
+/datum/sprite_accessory/moth_antennae/plain
+ name = "Plain"
+ icon_state = "plain"
+
+/datum/sprite_accessory/moth_antennae/reddish
+ name = "Reddish"
+ icon_state = "reddish"
+
+/datum/sprite_accessory/moth_antennae/royal
+ name = "Royal"
+ icon_state = "royal"
+
+/datum/sprite_accessory/moth_antennae/gothic
+ name = "Gothic"
+ icon_state = "gothic"
+
+/datum/sprite_accessory/moth_antennae/whitefly
+ name = "White Fly"
+ icon_state = "whitefly"
+
+/datum/sprite_accessory/moth_antennae/lovers
+ name = "Lovers"
+ icon_state = "lovers"
+
+/datum/sprite_accessory/moth_antennae/burnt_off
+ name = "Burnt Off"
+ icon_state = "burnt_off"
+
+/datum/sprite_accessory/moth_antennae/firewatch
+ name = "Firewatch"
+ icon_state = "firewatch"
+
+/datum/sprite_accessory/moth_antennae/deathhead
+ name = "Deathshead"
+ icon_state = "deathhead"
+
+/datum/sprite_accessory/moth_antennae/poison
+ name = "Poison"
+ icon_state = "poison"
+
+/datum/sprite_accessory/moth_antennae/ragged
+ name = "Ragged"
+ icon_state = "ragged"
+
+/datum/sprite_accessory/moth_antennae/moonfly
+ name = "Moon Fly"
+ icon_state = "moonfly"
+
+/datum/sprite_accessory/moth_antennae/oakworm
+ name = "Oak Worm"
+ icon_state = "oakworm"
+
+/datum/sprite_accessory/moth_antennae/jungle
+ name = "Jungle"
+ icon_state = "jungle"
+
+/datum/sprite_accessory/moth_antennae/witchwing
+ name = "Witch Wing"
+ icon_state = "witchwing"
+
+/datum/sprite_accessory/moth_antennae/regal
+ name = "Regal"
+ icon_state = "regal"
+/datum/sprite_accessory/moth_antennae/rosy
+ name = "Rosy"
+ icon_state = "rosy"
+
+/datum/sprite_accessory/moth_antennae/feathery
+ name = "Feathery"
+ icon_state = "feathery"
+
+/datum/sprite_accessory/moth_antennae/brown
+ name = "Brown"
+ icon_state = "brown"
+
+/datum/sprite_accessory/moth_antennae/plasmafire
+ name = "Plasmafire"
+ icon_state = "plasmafire"
+
+/datum/sprite_accessory/moth_antennae/moffra
+ name = "Moffra"
+ icon_state = "moffra"
+
+/datum/sprite_accessory/moth_antennae/lightbearer
+ name = "Lightbearer"
+ icon_state = "lightbearer"
+
+/datum/sprite_accessory/moth_antennae/dipped
+ name = "Dipped"
+ icon_state = "dipped"
diff --git a/code/datums/sprite_accessories/pod_hair.dm b/code/datums/sprite_accessories/pod_hair.dm
new file mode 100644
index 000000000000..b1f696098e6f
--- /dev/null
+++ b/code/datums/sprite_accessories/pod_hair.dm
@@ -0,0 +1,43 @@
+/datum/sprite_accessory/pod_hair
+ icon = 'icons/mob/human/species/podperson_hair.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/pod_hair/ivy
+ name = "Ivy"
+ icon_state = "ivy"
+
+/datum/sprite_accessory/pod_hair/cabbage
+ name = "Cabbage"
+ icon_state = "cabbage"
+
+/datum/sprite_accessory/pod_hair/spinach
+ name = "Spinach"
+ icon_state = "spinach"
+
+/datum/sprite_accessory/pod_hair/prayer
+ name = "Prayer"
+ icon_state = "prayer"
+
+/datum/sprite_accessory/pod_hair/vine
+ name = "Vine"
+ icon_state = "vine"
+
+/datum/sprite_accessory/pod_hair/shrub
+ name = "Shrub"
+ icon_state = "shrub"
+
+/datum/sprite_accessory/pod_hair/rose
+ name = "Rose"
+ icon_state = "rose"
+
+/datum/sprite_accessory/pod_hair/orchid
+ name = "Orchid"
+ icon_state = "orchid"
+
+/datum/sprite_accessory/pod_hair/fig
+ name = "Fig"
+ icon_state = "fig"
+
+/datum/sprite_accessory/pod_hair/hibiscus
+ name = "Hibiscus"
+ icon_state = "hibiscus"
diff --git a/code/datums/sprite_accessories/snouts.dm b/code/datums/sprite_accessories/snouts.dm
new file mode 100644
index 000000000000..c4add777bbe1
--- /dev/null
+++ b/code/datums/sprite_accessories/snouts.dm
@@ -0,0 +1,19 @@
+/datum/sprite_accessory/snouts
+ icon = 'icons/mob/human/species/lizard/lizard_misc.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/snouts/sharp
+ name = "Sharp"
+ icon_state = "sharp"
+
+/datum/sprite_accessory/snouts/round
+ name = "Round"
+ icon_state = "round"
+
+/datum/sprite_accessory/snouts/sharplight
+ name = "Sharp + Light"
+ icon_state = "sharplight"
+
+/datum/sprite_accessory/snouts/roundlight
+ name = "Round + Light"
+ icon_state = "roundlight"
diff --git a/code/datums/sprite_accessories/spines.dm b/code/datums/sprite_accessories/spines.dm
new file mode 100644
index 000000000000..8812c07dbc45
--- /dev/null
+++ b/code/datums/sprite_accessories/spines.dm
@@ -0,0 +1,47 @@
+/datum/sprite_accessory/spines
+ icon = 'icons/mob/human/species/lizard/lizard_spines.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/tail_spines
+ icon = 'icons/mob/human/species/lizard/lizard_spines.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/spines/short
+ name = "Short"
+ icon_state = "short"
+
+/datum/sprite_accessory/tail_spines/short
+ name = "Short"
+ icon_state = "short"
+
+/datum/sprite_accessory/spines/shortmeme
+ name = "Short + Membrane"
+ icon_state = "shortmeme"
+
+/datum/sprite_accessory/tail_spines/shortmeme
+ name = "Short + Membrane"
+ icon_state = "shortmeme"
+
+/datum/sprite_accessory/spines/long
+ name = "Long"
+ icon_state = "long"
+
+/datum/sprite_accessory/tail_spines/long
+ name = "Long"
+ icon_state = "long"
+
+/datum/sprite_accessory/spines/longmeme
+ name = "Long + Membrane"
+ icon_state = "longmeme"
+
+/datum/sprite_accessory/tail_spines/longmeme
+ name = "Long + Membrane"
+ icon_state = "longmeme"
+
+/datum/sprite_accessory/spines/aquatic
+ name = "Aquatic"
+ icon_state = "aqua"
+
+/datum/sprite_accessory/tail_spines/aquatic
+ name = "Aquatic"
+ icon_state = "aqua"
diff --git a/code/datums/sprite_accessories/tails.dm b/code/datums/sprite_accessories/tails.dm
new file mode 100644
index 000000000000..3948108670d4
--- /dev/null
+++ b/code/datums/sprite_accessories/tails.dm
@@ -0,0 +1,97 @@
+/datum/sprite_accessory/tails
+ em_block = TRUE
+ /// Describes which tail spine sprites to use, if any.
+ var/spine_key = NONE
+
+///Used for fish-infused tails, which come in different flavors.
+/datum/sprite_accessory/tails/fish
+ icon = 'icons/mob/human/fish_features.dmi'
+ color_src = TRUE
+
+/datum/sprite_accessory/tails/fish/simple
+ name = "Simple"
+ icon_state = "simple"
+
+/datum/sprite_accessory/tails/fish/crescent
+ name = "Crescent"
+ icon_state = "crescent"
+
+/datum/sprite_accessory/tails/fish/long
+ name = "Long"
+ icon_state = "long"
+ center = TRUE
+ dimension_x = 38
+
+/datum/sprite_accessory/tails/fish/shark
+ name = "Shark"
+ icon_state = "shark"
+
+/datum/sprite_accessory/tails/fish/chonky
+ name = "Chonky"
+ icon_state = "chonky"
+ center = TRUE
+ dimension_x = 36
+
+/datum/sprite_accessory/tails/lizard
+ icon = 'icons/mob/human/species/lizard/lizard_tails.dmi'
+ spine_key = SPINE_KEY_LIZARD
+
+/datum/sprite_accessory/tails/lizard/none
+ name = SPRITE_ACCESSORY_NONE
+ icon_state = "none"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/tails/lizard/smooth
+ name = "Smooth"
+ icon_state = "smooth"
+
+/datum/sprite_accessory/tails/lizard/dtiger
+ name = "Dark Tiger"
+ icon_state = "dtiger"
+
+/datum/sprite_accessory/tails/lizard/ltiger
+ name = "Light Tiger"
+ icon_state = "ltiger"
+
+/datum/sprite_accessory/tails/lizard/spikes
+ name = "Spikes"
+ icon_state = "spikes"
+
+/datum/sprite_accessory/tails/lizard/short
+ name = "Short"
+ icon_state = "short"
+ spine_key = NONE
+
+/datum/sprite_accessory/tails/felinid/cat
+ name = "Cat"
+ icon = 'icons/mob/human/cat_features.dmi'
+ icon_state = "default"
+ color_src = HAIR_COLOR
+
+/datum/sprite_accessory/tails/monkey
+
+/datum/sprite_accessory/tails/monkey/none
+ name = SPRITE_ACCESSORY_NONE
+ icon_state = "none"
+ natural_spawn = FALSE
+
+/datum/sprite_accessory/tails/monkey/default
+ name = "Monkey"
+ icon = 'icons/mob/human/species/monkey/monkey_tail.dmi'
+ icon_state = "default"
+ color_src = FALSE
+
+/datum/sprite_accessory/tails/xeno
+ icon_state = "default"
+ color_src = FALSE
+ center = TRUE
+
+/datum/sprite_accessory/tails/xeno/default
+ name = "Xeno"
+ icon = 'icons/mob/human/species/alien/tail_xenomorph.dmi'
+ dimension_x = 40
+
+/datum/sprite_accessory/tails/xeno/queen
+ name = "Xeno Queen"
+ icon = 'icons/mob/human/species/alien/tail_xenomorph_queen.dmi'
+ dimension_x = 64
diff --git a/code/datums/sprite_accessories/wings.dm b/code/datums/sprite_accessories/wings.dm
new file mode 100644
index 000000000000..083f6f6f5482
--- /dev/null
+++ b/code/datums/sprite_accessories/wings.dm
@@ -0,0 +1,249 @@
+/datum/sprite_accessory/moth_wings
+ icon = 'icons/mob/human/species/moth/moth_wings.dmi'
+ color_src = null
+ em_block = TRUE
+
+/datum/sprite_accessory/moth_wings/plain
+ name = "Plain"
+ icon_state = "plain"
+
+/datum/sprite_accessory/moth_wings/monarch
+ name = "Monarch"
+ icon_state = "monarch"
+
+/datum/sprite_accessory/moth_wings/luna
+ name = "Luna"
+ icon_state = "luna"
+
+/datum/sprite_accessory/moth_wings/atlas
+ name = "Atlas"
+ icon_state = "atlas"
+
+/datum/sprite_accessory/moth_wings/reddish
+ name = "Reddish"
+ icon_state = "redish"
+
+/datum/sprite_accessory/moth_wings/royal
+ name = "Royal"
+ icon_state = "royal"
+
+/datum/sprite_accessory/moth_wings/gothic
+ name = "Gothic"
+ icon_state = "gothic"
+
+/datum/sprite_accessory/moth_wings/lovers
+ name = "Lovers"
+ icon_state = "lovers"
+
+/datum/sprite_accessory/moth_wings/whitefly
+ name = "White Fly"
+ icon_state = "whitefly"
+
+/datum/sprite_accessory/moth_wings/burnt_off
+ name = "Burnt Off"
+ icon_state = "burnt_off"
+ locked = TRUE
+
+/datum/sprite_accessory/moth_wings/firewatch
+ name = "Firewatch"
+ icon_state = "firewatch"
+
+/datum/sprite_accessory/moth_wings/deathhead
+ name = "Deathshead"
+ icon_state = "deathhead"
+
+/datum/sprite_accessory/moth_wings/poison
+ name = "Poison"
+ icon_state = "poison"
+
+/datum/sprite_accessory/moth_wings/ragged
+ name = "Ragged"
+ icon_state = "ragged"
+
+/datum/sprite_accessory/moth_wings/moonfly
+ name = "Moon Fly"
+ icon_state = "moonfly"
+
+/datum/sprite_accessory/moth_wings/snow
+ name = "Snow"
+ icon_state = "snow"
+
+/datum/sprite_accessory/moth_wings/oakworm
+ name = "Oak Worm"
+ icon_state = "oakworm"
+
+/datum/sprite_accessory/moth_wings/jungle
+ name = "Jungle"
+ icon_state = "jungle"
+
+/datum/sprite_accessory/moth_wings/witchwing
+ name = "Witch Wing"
+ icon_state = "witchwing"
+
+/datum/sprite_accessory/moth_wings/rosy
+ name = "Rosy"
+ icon_state = "rosy"
+
+/datum/sprite_accessory/moth_wings/feathery
+ name = "Feathery"
+ icon_state = "feathery"
+
+/datum/sprite_accessory/moth_wings/brown
+ name = "Brown"
+ icon_state = "brown"
+
+/datum/sprite_accessory/moth_wings/plasmafire
+ name = "Plasmafire"
+ icon_state = "plasmafire"
+
+/datum/sprite_accessory/moth_wings/moffra
+ name = "Moffra"
+ icon_state = "moffra"
+
+/datum/sprite_accessory/moth_wings/lightbearer
+ name = "Lightbearer"
+ icon_state = "lightbearer"
+
+/datum/sprite_accessory/moth_wings/dipped
+ name = "Dipped"
+ icon_state = "dipped"
+
+/datum/sprite_accessory/wings
+ icon = 'icons/mob/human/species/wings.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/wings_open
+ icon = 'icons/mob/human/species/wings.dmi'
+ em_block = TRUE
+
+/datum/sprite_accessory/wings/angel
+ name = "Angel"
+ icon_state = "angel"
+ color_src = FALSE
+ dimension_x = 46
+ center = TRUE
+ dimension_y = 34
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/angel
+ name = "Angel"
+ icon_state = "angel"
+ color_src = FALSE
+ dimension_x = 46
+ center = TRUE
+ dimension_y = 34
+
+/datum/sprite_accessory/wings/dragon
+ name = "Dragon"
+ icon_state = "dragon"
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/dragon
+ name = "Dragon"
+ icon_state = "dragon"
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/megamoth
+ name = "Megamoth"
+ icon_state = "megamoth"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/megamoth
+ name = "Megamoth"
+ icon_state = "megamoth"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/mothra
+ name = "Mothra"
+ icon_state = "mothra"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/mothra
+ name = "Mothra"
+ icon_state = "mothra"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/skeleton
+ name = "Skeleton"
+ icon_state = "skele"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/skeleton
+ name = "Skeleton"
+ icon_state = "skele"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/robotic
+ name = "Robotic"
+ icon_state = "robotic"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/robotic
+ name = "Robotic"
+ icon_state = "robotic"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/fly
+ name = "Fly"
+ icon_state = "fly"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/fly
+ name = "Fly"
+ icon_state = "fly"
+ color_src = FALSE
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+
+/datum/sprite_accessory/wings/slime
+ name = "Slime"
+ icon_state = "slime"
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
+ locked = TRUE
+
+/datum/sprite_accessory/wings_open/slime
+ name = "Slime"
+ icon_state = "slime"
+ dimension_x = 96
+ center = TRUE
+ dimension_y = 32
diff --git a/code/datums/status_effects/wound_effects.dm b/code/datums/status_effects/wound_effects.dm
index 5ea0be0b256d..6b10d609778b 100644
--- a/code/datums/status_effects/wound_effects.dm
+++ b/code/datums/status_effects/wound_effects.dm
@@ -79,18 +79,21 @@
// less limping while we have determination still
var/determined_mod = owner.has_status_effect(/datum/status_effect/determined) ? 0.5 : 1
+ var/obj/item/bodypart/leg_about_to_limp
var/limp_chance
var/limp_slowdown
if(next_leg == left)
+ leg_about_to_limp = left
limp_chance = limp_chance_left
limp_slowdown = slowdown_left
next_leg = right
else
+ leg_about_to_limp = right
limp_chance = limp_chance_right
limp_slowdown = slowdown_right
next_leg = left
- if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING) & COMPONENT_CANCEL_LIMP)
+ if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING, leg_about_to_limp) & COMPONENT_CANCEL_LIMP)
return
if(prob(limp_chance * determined_mod))
diff --git a/code/datums/storage/subtypes/boxes.dm b/code/datums/storage/subtypes/boxes.dm
index 2a50119749f1..1bff890dc2cf 100644
--- a/code/datums/storage/subtypes/boxes.dm
+++ b/code/datums/storage/subtypes/boxes.dm
@@ -183,3 +183,14 @@
max_total_storage = WEIGHT_CLASS_SMALL * slots
. = ..()
set_holdable(/obj/item/fishing_lure) //can only hold lures
+
+/datum/storage/box/ramen_beef
+ max_slots = 2
+
+/datum/storage/box/ramen_beef/New(atom/parent, max_slots, max_specific_storage, max_total_storage, rustle_sound, remove_rustle_sound)
+ . = ..()
+ set_holdable(list(
+ /obj/item/reagent_containers/condiment/pack/beef_flavour,
+ /obj/item/food/spaghetti/ramen_dry,
+ ))
+
diff --git a/code/datums/weather/particle_weather.dm b/code/datums/weather/particle_weather.dm
new file mode 100644
index 000000000000..b8e1a5ac3fe2
--- /dev/null
+++ b/code/datums/weather/particle_weather.dm
@@ -0,0 +1,145 @@
+/// Subtype which uses particle systems instead of overlays to display its effects
+/datum/weather/particle
+ abstract_type = /datum/weather/particle
+ /// Particles used to display the weather visuals
+ var/particles/weather/particle_type = null
+ /// Secondary (layered ontop) particle type which is rendered as emissive in case you want embers or whatever
+ var/particles/weather/emissive_type = null
+ /// Minimum possible severity for the weather, (0~100]
+ var/min_severity = 1
+ /// Maximum possible severity for the weather, (0~100]
+ var/max_severity = MAXIMUM_WEATHER_SEVERITY
+ /// Maximum variation in severity each weather frame
+ var/severity_variation = 5
+ /// Optimal point towards which severity will try to gravitate by influencing random values
+ var/optimal_severity = 70
+ /// How often can we change our severity?
+ /// Don't set this too low or it'll look jank
+ var/severity_cooldown = 5 SECONDS
+
+ /// Current weather severity
+ var/severity = 0
+ /// Last tick during which we've changed our visual severity
+ var/last_severity_tick = 0
+ /// List of weather object lists (as we can have multiple if emissives are involved) by plane offset
+ var/list/weather_objects = list()
+ /// Direction of our wind
+ var/wind_sign = 0
+
+/datum/weather/particle/New(z_levels, list/weather_data)
+ . = ..()
+ if (isnull(particle_type) && isnull(emissive_type))
+ CRASH("[src] ([type]) attempted to initialize without normal or emissive particle types!")
+
+ for (var/offset in 0 to SSmapping.max_plane_offset)
+ var/list/object_list = list()
+ if (particle_type)
+ var/obj/effect/abstract/weather_holder/holder = new()
+ SET_PLANE_W_SCALAR(holder, RENDER_PLANE_PARTICLE_WEATHER, offset)
+ holder.particles = new particle_type()
+ object_list += holder
+
+ if (emissive_type)
+ var/obj/effect/abstract/weather_holder/holder = new()
+ holder.particles = new emissive_type()
+ SET_PLANE_W_SCALAR(holder, RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER, offset)
+ object_list += holder
+
+ weather_objects += list(object_list)
+
+ SSweather.add_weather_objects(weather_objects)
+
+/datum/weather/particle/Destroy()
+ SSweather.remove_weather_objects(weather_objects)
+ for(var/list/object_list as anything in weather_objects)
+ QDEL_LIST(object_list)
+ weather_objects = null
+ return ..()
+
+/datum/weather/particle/telegraph(list/weather_data)
+ . = ..()
+ if (!.)
+ return
+ animate_severity(0)
+
+/datum/weather/particle/end()
+ . = ..()
+ if (!.)
+ return
+ animate_severity(0)
+
+/// Adjust our severity by a random number based on our stage
+/datum/weather/particle/proc/process_particles()
+ if (last_severity_tick + severity_cooldown > world.time)
+ return
+
+ last_severity_tick = world.time
+ var/new_severity = severity
+ switch (stage)
+ if (STARTUP_STAGE)
+ // Aims at minimum severity, and can only go up
+ new_severity += rand() * severity_variation * (1 - severity / min_severity)
+ if (MAIN_STAGE)
+ // Tries to stay close to optimal severity
+ new_severity += LERP(-severity_variation * clamp(INVERSE_LERP(min_severity, optimal_severity, severity), 0, 1), severity_variation * clamp(INVERSE_LERP(max_severity, optimal_severity, severity), 0, 1), rand())
+ new_severity = clamp(new_severity, min(new_severity, min_severity), max_severity)
+ if (WIND_DOWN_STAGE)
+ // Slowly goes down to zero
+ new_severity += rand() * -severity_variation * (severity / min_severity)
+
+ animate_severity(new_severity)
+
+/datum/weather/particle/proc/animate_severity(new_severity)
+ if (!wind_sign)
+ wind_sign = pick(-1, 1)
+
+ severity = new_severity
+ for (var/list/holder_list as anything in weather_objects)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ var/particles/weather/particle_effect = holder.particles
+ particle_effect.animate_severity(severity / MAXIMUM_WEATHER_SEVERITY, wind_sign)
+
+/datum/weather/particle/generate_overlay_cache()
+ . = ..()
+ for (var/offset in 0 to SSmapping.max_plane_offset)
+ . += mutable_appearance('icons/effects/weather_overlay.dmi', "weather_overlay", overlay_layer, null, WEATHER_MASK_PLANE, offset_const = offset)
+
+/obj/effect/abstract/weather_holder
+ icon = null
+ appearance_flags = TILE_BOUND | PIXEL_SCALE
+ blocks_emissive = EMISSIVE_BLOCK_NONE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/particles/weather
+ spawning = 0
+ // Wider than our view due to weird BYOND jank
+ width = 800
+ height = 800
+ count = 8000
+
+ lifespan = 30 SECONDS
+ fade = 1 SECONDS
+ fadein = 0.5 SECONDS
+
+ /// Increase in speed per tick
+ var/wind_strength = 0
+ /// Minimum number of spawned particles per tick for easing
+ var/min_spawn = 0
+ /// Maximum amount of spawned particles at full strength
+ var/max_spawn = 0
+
+/// Changes the strength of the weather visual effect, severity should be between 0 and 1
+/particles/weather/proc/animate_severity(severity, wind_sign)
+ // Stop spawning if severity is zero or negative
+ if (severity <= 0)
+ spawning = 0
+ return
+
+ var/wind = wind_strength * severity * wind_sign
+ spawning = LERP(min_spawn, max_spawn, severity)
+ // Might already be set on init, in which case we preserve y and z components
+ if (islist(gravity) && length(gravity))
+ gravity[1] = wind
+ else
+ gravity = list(wind)
+
diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm
index 50fa90c79e29..de0d5cfc0577 100644
--- a/code/datums/weather/weather.dm
+++ b/code/datums/weather/weather.dm
@@ -13,9 +13,10 @@
*/
/datum/weather
- /// name of weather
+ abstract_type = /datum/weather
+ /// Name of weather
var/name = "space wind"
- /// description of weather
+ /// Description of weather
var/desc = "Heavy gusts of wind blanket the area, periodically knocking down anyone caught in the open."
/// The message displayed in chat to foreshadow the weather's beginning
var/telegraph_message = span_warning("The wind begins to pick up.")
@@ -42,6 +43,8 @@
var/weather_overlay
/// Color to apply to the area while weather is occuring
var/weather_color = null
+ /// Alpha of the weather overlay
+ var/weather_alpha = 255
/// Displayed once the weather is over
var/end_message = span_danger("The wind relents its assault.")
@@ -326,7 +329,7 @@
*/
/datum/weather/proc/start()
if(stage >= MAIN_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_START(type), src)
stage = MAIN_STAGE
update_areas()
@@ -335,6 +338,7 @@
addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration, TIMER_UNIQUE)
for(var/area/impacted_area as anything in impacted_areas)
SEND_SIGNAL(impacted_area, COMSIG_WEATHER_BEGAN_IN_AREA(type), src)
+ return TRUE
/**
* Weather enters the winding down phase, stops effects
@@ -345,12 +349,13 @@
*/
/datum/weather/proc/wind_down()
if(stage >= WIND_DOWN_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_WINDDOWN(type), src)
stage = WIND_DOWN_STAGE
update_areas()
send_alert(end_message, end_sound, end_sound_vol)
addtimer(CALLBACK(src, PROC_REF(end)), end_duration, TIMER_UNIQUE)
+ return TRUE
/**
* Fully ends the weather
@@ -361,7 +366,7 @@
*/
/datum/weather/proc/end()
if(stage == END_STAGE)
- return
+ return FALSE
SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_END(type), src)
UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED)
stage = END_STAGE
@@ -373,6 +378,7 @@
if(target_trait)
for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list)
UnregisterSignal(affected, COMSIG_MOB_LOGIN)
+ return TRUE
// handles sending all alerts
/datum/weather/proc/send_alert(alert_msg, alert_sfx, alert_sfx_vol = 100)
@@ -576,11 +582,11 @@
// I prefer it to creating 2 extra plane masters however, so it's a cost I'm willing to pay
// LU
if(use_glow)
- var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100, offset_const = offset)
+ var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100 / 255 * weather_alpha, offset_const = offset)
glow_overlay.color = weather_color
gen_overlay_cache += glow_overlay
- var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, offset_const = offset)
+ var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset)
new_weather_overlay.color = weather_color
gen_overlay_cache += new_weather_overlay
diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm
index 367977a90bca..ae12115e367f 100644
--- a/code/datums/weather/weather_types/ash_storm.dm
+++ b/code/datums/weather/weather_types/ash_storm.dm
@@ -1,8 +1,15 @@
-//Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside.
-/datum/weather/ash_storm
+/// Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside.
+/datum/weather/particle/ash_storm
name = "ash storm"
desc = "An intense atmospheric storm lifts ash off of the planet's surface and billows it down across the area, dealing intense fire damage to the unprotected."
+ particle_type = /particles/weather/ash_storm
+ emissive_type = /particles/weather/ash_storm/embers
+ min_severity = 60
+ optimal_severity = 80
+ weather_alpha = 100
+ wind_sign = -1 // Always blows left to sync with the animated overlays
+
telegraph_message = span_boldwarning("An eerie moan rises on the wind. Sheets of burning ash blacken the horizon. Seek shelter.")
telegraph_duration = 30 SECONDS
telegraph_overlay = "light_ash"
@@ -28,10 +35,51 @@
var/list/weak_sounds = list()
var/list/strong_sounds = list()
-/datum/weather/ash_storm/get_playlist_ref()
+/particles/weather/ash_storm
+ icon = 'icons/effects/particles/smoke.dmi'
+ icon_state = list("chill_1" = 4, "chill_2" = 3, "chill_3" = 2)
+ position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0))
+ grow = list(-0.01, -0.01)
+ gravity = list(0, -7, 0.5)
+ drift = generator(GEN_BOX, list(-1, -1, 0), list(1, 0, 0))
+ friction = 0.3
+ min_spawn = 20
+ max_spawn = 400
+ wind_strength = 16
+ spin = generator(GEN_NUM, -5, 5, NORMAL_RAND)
+ /// Y-coordinate of our gravity
+ var/gravity_power = -12
+
+/particles/weather/ash_storm/animate_severity(severity)
+ . = ..()
+ if (length(gravity) > 1)
+ gravity[2] = gravity_power * severity
+ else
+ gravity = list(gravity[1], gravity_power * severity, 0.5)
+
+/particles/weather/ash_storm/embers
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot" = 4, "cross" = 2, "curl" = 1)
+ color = LIGHT_COLOR_FIRE
+ min_spawn = 10
+ max_spawn = 80
+
+/particles/weather/ash_storm/emberfall
+ min_spawn = 20
+ max_spawn = 200
+ wind_strength = 3
+ gravity_power = -2
+
+/particles/weather/ash_storm/embers/emberfall
+ min_spawn = 20
+ max_spawn = 200
+ wind_strength = 3
+ gravity_power = -2
+
+/datum/weather/particle/ash_storm/get_playlist_ref()
return GLOB.ash_storm_sounds
-/datum/weather/ash_storm/telegraph()
+/datum/weather/particle/ash_storm/telegraph()
for(var/area/impacted_area as anything in impacted_areas)
if(impacted_area.outdoors)
weak_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm
@@ -45,17 +93,17 @@
GLOB.ash_storm_sounds += weak_sounds
return ..()
-/datum/weather/ash_storm/start()
+/datum/weather/particle/ash_storm/start()
GLOB.ash_storm_sounds -= weak_sounds
GLOB.ash_storm_sounds += strong_sounds
return ..()
-/datum/weather/ash_storm/wind_down()
+/datum/weather/particle/ash_storm/wind_down()
GLOB.ash_storm_sounds -= strong_sounds
GLOB.ash_storm_sounds += weak_sounds
return ..()
-/datum/weather/ash_storm/recursive_weather_protection_check(atom/to_check)
+/datum/weather/particle/ash_storm/recursive_weather_protection_check(atom/to_check)
. = ..()
if(. || !ishuman(to_check))
return
@@ -63,12 +111,13 @@
if(human_to_check.get_thermal_protection() >= FIRE_IMMUNITY_MAX_TEMP_PROTECT)
return TRUE
-/datum/weather/ash_storm/weather_act_mob(mob/living/victim)
+/datum/weather/particle/ash_storm/weather_act_mob(mob/living/victim)
victim.adjust_fire_loss(4, required_bodytype = BODYTYPE_ORGANIC)
return ..()
-/datum/weather/ash_storm/end()
+/datum/weather/particle/ash_storm/end()
GLOB.ash_storm_sounds -= weak_sounds
+ GLOB.ash_storm_sounds -= strong_sounds
for(var/turf/open/misc/asteroid/basalt/basalt as anything in GLOB.dug_up_basalt)
if(!(basalt.loc in impacted_areas) || !(basalt.z in impacted_z_levels))
continue
@@ -76,7 +125,7 @@
return ..()
//Emberfalls are the result of an ash storm passing by close to the playable area of lavaland. They have a 10% chance to trigger in place of an ash storm.
-/datum/weather/ash_storm/emberfall
+/datum/weather/particle/ash_storm/emberfall
name = "emberfall"
desc = "A passing ash storm blankets the area in harmless embers."
@@ -89,3 +138,6 @@
weather_flags = parent_type::weather_flags & ~(WEATHER_MOBS|WEATHER_THUNDER)
probability = 10
+
+ particle_type = /particles/weather/ash_storm/emberfall
+ emissive_type = /particles/weather/ash_storm/embers/emberfall
diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm
index 4516112824c4..2b2998fff727 100644
--- a/code/datums/weather/weather_types/rain_storm.dm
+++ b/code/datums/weather/weather_types/rain_storm.dm
@@ -1,16 +1,16 @@
-/datum/weather/rain_storm
+/datum/weather/particle/rain_storm
name = "rain"
desc = "Heavy thunderstorms rain down below, drenching anyone caught in it."
+ particle_type = /particles/weather/rain_storm
+ min_severity = 30
+
telegraph_message = span_danger("Thunder rumbles far above. You hear droplets drumming against the canopy.")
- telegraph_overlay = "rain_low"
telegraph_duration = 30 SECONDS
weather_message = span_userdanger("Rain pours down around you!")
- weather_overlay = "rain_high"
end_message = span_bolddanger("The downpour gradually slows to a light shower.")
- end_overlay = "rain_low"
end_duration = 30 SECONDS
weather_duration_lower = 3 MINUTES
@@ -27,10 +27,20 @@
weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_THUNDER | WEATHER_BAROMETER)
whitelist_weather_reagents = list(/datum/reagent/water)
-/datum/weather/rain_storm/get_playlist_ref()
+/datum/weather/particle/rain_storm/New(z_levels, list/weather_data)
+ . = ..()
+ if (isnull(weather_reagent) || istype(weather_reagent, /datum/reagent/water) || !weather_color)
+ return
+
+ // Non-water rain gets colored into their reagent's color
+ for (var/list/holder_list as anything in weather_objects)
+ for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list)
+ holder.particles.color = weather_color
+
+/datum/weather/particle/rain_storm/get_playlist_ref()
return GLOB.rain_storm_sounds
-/datum/weather/rain_storm/telegraph()
+/datum/weather/particle/rain_storm/telegraph()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/start
@@ -43,39 +53,53 @@
return ..()
-/datum/weather/rain_storm/start()
+/datum/weather/particle/rain_storm/start()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/middle
return ..()
-/datum/weather/rain_storm/wind_down()
+/datum/weather/particle/rain_storm/wind_down()
GLOB.rain_storm_sounds.Cut()
for(var/area/impacted_area as anything in impacted_areas)
GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/end
return ..()
-/datum/weather/rain_storm/end()
+/datum/weather/particle/rain_storm/end()
GLOB.rain_storm_sounds.Cut()
return ..()
-/datum/weather/rain_storm/blood
+/particles/weather/rain_storm
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = "drop"
+ color = "#ccffff"
+ position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0))
+ grow = list(-0.01, -0.01)
+ gravity = list(0, -10, 0.5)
+ drift = generator(GEN_CIRCLE, 0, 1)
+ friction = 0.3
+ min_spawn = 50
+ max_spawn = 300
+ wind_strength = 5
+ spin = 0
+
+/datum/weather/particle/rain_storm/blood
whitelist_weather_reagents = list(/datum/reagent/blood)
probability = 0 // admeme event
// Fun fact - if you increase the weather_temperature higher than LIQUID_PLASMA_BP
// the plasma rain will vaporize into a gas on whichever turf it lands on
-/datum/weather/rain_storm/plasma
+/datum/weather/particle/rain_storm/plasma
whitelist_weather_reagents = list(/datum/reagent/toxin/plasma)
probability = 0 // maybe for icebox maps one day?
-/datum/weather/rain_storm/deep_fried
+/datum/weather/particle/rain_storm/deep_fried
weather_temperature = 455 // just hot enough to apply the fried effect
whitelist_weather_reagents = list(/datum/reagent/consumable/nutriment/fat/oil)
weather_flags = (WEATHER_TURFS | WEATHER_INDOORS)
probability = 0 // admeme event
-/datum/weather/rain_storm/acid
+/datum/weather/particle/rain_storm/acid
desc = "The planet's thunderstorms are by nature acidic, and will incinerate anyone standing beneath them without protection."
telegraph_duration = 40 SECONDS
@@ -97,7 +121,7 @@
)
probability = 0
-/datum/weather/rain_storm/wizard
+/datum/weather/particle/rain_storm/wizard
name = "magical rain"
desc = "A magical thunderstorm rains down below, drenching anyone caught in it with mysterious rain."
@@ -116,7 +140,7 @@
probability = 0 // shouldn't spawn normally
weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_INDOORS | WEATHER_BAROMETER)
-/datum/weather/rain_storm/wizard/New(z_levels, list/weather_data)
+/datum/weather/particle/rain_storm/wizard/New(z_levels, list/weather_data)
if(length(GLOB.wizard_rain_reagents)) // the wizard event has already been run once and setup the whitelist
whitelist_weather_reagents = GLOB.wizard_rain_reagents
return ..()
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 87104b8cd473..fd0f586b98d5 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -12,6 +12,7 @@
plane = AREA_PLANE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
invisibility = INVISIBILITY_LIGHTING
+ tacmap_color = null
/// List of all turfs currently inside this area as nested lists indexed by zlevel.
/// Acts as a filtered version of area.contents For faster lookup
@@ -137,6 +138,9 @@
/// Are shuttles allowed to dock in this area
var/allow_shuttle_docking = FALSE
+ /// If TRUE, then this area will be skipped entirely by minimap rendering.
+ var/skip_minimap_rendering = FALSE
+
/**
* A list of teleport locations
*
diff --git a/code/game/area/areas/away_content.dm b/code/game/area/areas/away_content.dm
index 39d7ebce1dbe..7194a8494e4a 100644
--- a/code/game/area/areas/away_content.dm
+++ b/code/game/area/areas/away_content.dm
@@ -11,6 +11,7 @@ Unused icons for new areas are "awaycontent1" ~ "awaycontent30"
default_gravity = STANDARD_GRAVITY
ambience_index = AMBIENCE_AWAY
sound_environment = SOUND_ENVIRONMENT_ROOM
+ skip_minimap_rendering = TRUE
/area/awaymission/museum
name = "Nanotrasen Museum"
diff --git a/code/game/area/areas/mining.dm b/code/game/area/areas/mining.dm
index af047a00698d..51f054029739 100644
--- a/code/game/area/areas/mining.dm
+++ b/code/game/area/areas/mining.dm
@@ -196,6 +196,7 @@
sound_environment = SOUND_AREA_ICEMOON
ambient_buzz = 'sound/ambience/lavaland/magma.ogg'
allow_shuttle_docking = TRUE
+ skip_minimap_rendering = TRUE
/area/icemoon/surface
name = "Icemoon"
diff --git a/code/game/area/areas/misc.dm b/code/game/area/areas/misc.dm
index ac66943d5d1c..a922a1e147ab 100644
--- a/code/game/area/areas/misc.dm
+++ b/code/game/area/areas/misc.dm
@@ -17,6 +17,7 @@
sound_environment = SOUND_AREA_SPACE
ambient_buzz = null //Space is deafeningly quiet
allow_shuttle_docking = TRUE
+ skip_minimap_rendering = TRUE
/area/space/nearstation
icon_state = "space_near"
diff --git a/code/game/area/areas/ruins/_ruins.dm b/code/game/area/areas/ruins/_ruins.dm
index 92fff19386da..f0adbe7eb6bc 100644
--- a/code/game/area/areas/ruins/_ruins.dm
+++ b/code/game/area/areas/ruins/_ruins.dm
@@ -9,6 +9,7 @@
ambience_index = AMBIENCE_RUINS
flags_1 = CAN_BE_DIRTY_1
sound_environment = SOUND_ENVIRONMENT_STONEROOM
+ skip_minimap_rendering = TRUE
/area/ruin/unpowered
always_unpowered = TRUE
diff --git a/code/game/area/areas/station/cargo.dm b/code/game/area/areas/station/cargo.dm
index 8e892efda95c..430122cc769b 100644
--- a/code/game/area/areas/station/cargo.dm
+++ b/code/game/area/areas/station/cargo.dm
@@ -3,6 +3,7 @@
icon_state = "quart"
airlock_wires = /datum/wires/airlock/cargo
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = COLOR_CARGO_BROWN
/area/station/cargo/sorting
name = "\improper Delivery Office"
diff --git a/code/game/area/areas/station/command.dm b/code/game/area/areas/station/command.dm
index 30f126dc42cb..6cc80002aab1 100644
--- a/code/game/area/areas/station/command.dm
+++ b/code/game/area/areas/station/command.dm
@@ -6,6 +6,7 @@
)
airlock_wires = /datum/wires/airlock/command
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = TACMAP_AREA_COMMAND
/area/station/command/bridge
name = "\improper Bridge"
diff --git a/code/game/area/areas/station/common.dm b/code/game/area/areas/station/common.dm
index 1b6eba4dc25e..641c43dfd417 100644
--- a/code/game/area/areas/station/common.dm
+++ b/code/game/area/areas/station/common.dm
@@ -87,6 +87,7 @@
mood_message = "I love being in the bar!"
mood_trait = TRAIT_EXTROVERT
sound_environment = SOUND_AREA_SMALL_SOFTFLOOR
+ tacmap_color = TACMAP_AREA_SERVICE
/area/station/commons/fitness
name = "\improper Fitness Room"
diff --git a/code/game/area/areas/station/engineering.dm b/code/game/area/areas/station/engineering.dm
index 3f0464453857..04e27d738c4e 100644
--- a/code/game/area/areas/station/engineering.dm
+++ b/code/game/area/areas/station/engineering.dm
@@ -3,6 +3,7 @@
ambience_index = AMBIENCE_ENGI
airlock_wires = /datum/wires/airlock/engineering
sound_environment = SOUND_AREA_LARGE_ENCLOSED
+ tacmap_color = TACMAP_AREA_ENGINEERING
/area/station/engineering/circuit_workshop
name = "\improper Circuit Workshop"
@@ -150,6 +151,7 @@
icon_state = "construction"
ambience_index = AMBIENCE_ENGI
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = TACMAP_AREA_ENGINEERING
/area/station/construction/mining/aux_base
name = "Auxiliary Base Construction"
diff --git a/code/game/area/areas/station/hallway.dm b/code/game/area/areas/station/hallway.dm
index 12a5ba6816b4..3c82c9b1beea 100644
--- a/code/game/area/areas/station/hallway.dm
+++ b/code/game/area/areas/station/hallway.dm
@@ -92,6 +92,7 @@
/area/station/hallway/secondary/service
name = "\improper Service Hallway"
icon_state = "hall_service"
+ tacmap_color = TACMAP_AREA_SERVICE
/area/station/hallway/secondary/spacebridge
name = "\improper Space Bridge"
diff --git a/code/game/area/areas/station/maintenance.dm b/code/game/area/areas/station/maintenance.dm
index a6fa76cd80e9..84ce4032eb62 100644
--- a/code/game/area/areas/station/maintenance.dm
+++ b/code/game/area/areas/station/maintenance.dm
@@ -7,6 +7,7 @@
forced_ambience = TRUE
ambient_buzz = 'sound/ambience/maintenance/source_corridor2.ogg'
ambient_buzz_vol = 20
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/*
* Departmental Maintenance
@@ -248,6 +249,7 @@
/area/station/maintenance/disposal/incinerator
name = "\improper Incinerator"
icon_state = "incinerator"
+ tacmap_color = TACMAP_AREA_ENGINEERING
/area/station/maintenance/space_hut
name = "\improper Space Hut"
diff --git a/code/game/area/areas/station/medical.dm b/code/game/area/areas/station/medical.dm
index 03bdc8eb5bf1..a02e014a0c00 100644
--- a/code/game/area/areas/station/medical.dm
+++ b/code/game/area/areas/station/medical.dm
@@ -4,6 +4,7 @@
ambience_index = AMBIENCE_MEDICAL
airlock_wires = /datum/wires/airlock/medbay
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = TACMAP_AREA_MEDICAL
/area/station/medical/abandoned
name = "\improper Abandoned Medbay"
@@ -12,6 +13,7 @@
'sound/ambience/misc/signal.ogg',
)
sound_environment = SOUND_AREA_SMALL_ENCLOSED
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/medical/medbay/central
name = "Medbay Central"
diff --git a/code/game/area/areas/station/science.dm b/code/game/area/areas/station/science.dm
index b64e7efe75a4..9954a500673b 100644
--- a/code/game/area/areas/station/science.dm
+++ b/code/game/area/areas/station/science.dm
@@ -3,6 +3,7 @@
icon_state = "science"
airlock_wires = /datum/wires/airlock/science
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = TACMAP_AREA_SCIENCE
/area/station/science/lobby
name = "\improper Science Lobby"
@@ -131,6 +132,7 @@
name = "\improper Ordnance Bomb Site"
icon_state = "ord_boom"
area_flags = BLOBS_ALLOWED | CULT_PERMITTED | NO_GRAVITY
+ skip_minimap_rendering = TRUE
/area/station/science/ordnance/bomb/planet
area_flags = /area/station/science/ordnance/bomb::area_flags & ~NO_GRAVITY
diff --git a/code/game/area/areas/station/security.dm b/code/game/area/areas/station/security.dm
index 3166f7733d95..22d3bb0de85c 100644
--- a/code/game/area/areas/station/security.dm
+++ b/code/game/area/areas/station/security.dm
@@ -6,6 +6,7 @@
ambience_index = AMBIENCE_DANGER
airlock_wires = /datum/wires/airlock/security
sound_environment = SOUND_AREA_STANDARD_STATION
+ tacmap_color = TACMAP_AREA_SECURITY
/area/station/security/office
name = "\improper Security Office"
diff --git a/code/game/area/areas/station/service.dm b/code/game/area/areas/station/service.dm
index 355406563264..71e9421e3a5b 100644
--- a/code/game/area/areas/station/service.dm
+++ b/code/game/area/areas/station/service.dm
@@ -1,5 +1,6 @@
/area/station/service
airlock_wires = /datum/wires/airlock/service
+ tacmap_color = TACMAP_AREA_SERVICE
/*
* Bar/Kitchen Areas
@@ -199,27 +200,34 @@
name = "\improper Abandoned Garden"
icon_state = "abandoned_garden"
sound_environment = SOUND_AREA_SMALL_ENCLOSED
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/kitchen/abandoned
name = "\improper Abandoned Kitchen"
icon_state = "abandoned_kitchen"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/electronic_marketing_den
name = "\improper Electronic Marketing Den"
icon_state = "abandoned_marketing_den"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/abandoned_gambling_den
name = "\improper Abandoned Gambling Den"
icon_state = "abandoned_gambling_den"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/abandoned_gambling_den/gaming
name = "\improper Abandoned Gaming Den"
icon_state = "abandoned_gaming_den"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/theater/abandoned
name = "\improper Abandoned Theater"
icon_state = "abandoned_theatre"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
/area/station/service/library/abandoned
name = "\improper Abandoned Library"
icon_state = "abandoned_library"
+ tacmap_color = TACMAP_AREA_MAINTENANCE
diff --git a/code/game/area/areas/station/solars.dm b/code/game/area/areas/station/solars.dm
index 569a7f0a169e..d7765c6ee9b6 100644
--- a/code/game/area/areas/station/solars.dm
+++ b/code/game/area/areas/station/solars.dm
@@ -77,6 +77,7 @@
/area/station/maintenance/solars
name = "Solar Maintenance"
icon_state = "yellow"
+ tacmap_color = TACMAP_AREA_ENGINEERING
/area/station/maintenance/solars/port
name = "Port Solar Maintenance"
diff --git a/code/game/area/areas/station/telecomm.dm b/code/game/area/areas/station/telecomm.dm
index adb4670b44b3..46c644acfebe 100644
--- a/code/game/area/areas/station/telecomm.dm
+++ b/code/game/area/areas/station/telecomm.dm
@@ -15,6 +15,7 @@
'sound/ambience/misc/ambimystery.ogg',
)
airlock_wires = /datum/wires/airlock/engineering
+ tacmap_color = TACMAP_AREA_ENGINEERING
/area/station/tcommsat/maints
name = "\improper Telecomms Maintenance Room"
diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm
index ad246f5ff825..d59f478a2dba 100644
--- a/code/game/atom/_atom.dm
+++ b/code/game/atom/_atom.dm
@@ -147,6 +147,9 @@
/// Generally for niche objects, atoms blacklisted can spawn if enabled by spawner.
var/spawn_blacklisted = FALSE
+ /// What color this shows up as on the tactical map
+ var/tacmap_color = TACMAP_SOLID
+
/**
* Top level of the destroy chain for most atoms
*
@@ -438,6 +441,10 @@
/atom/proc/HasProximity(atom/movable/proximity_check_mob as mob|obj)
return
+/// has a previously nearby atom moved away
+/atom/proc/OnProximityExit(atom/movable/proximity_check_mob as mob|obj)
+ return
+
/// Sets the wire datum of an atom
/atom/proc/set_wires(datum/wires/new_wires)
wires = new_wires
diff --git a/code/game/machinery/big_manipulator/big_manipulator.dm b/code/game/machinery/big_manipulator/big_manipulator.dm
index 83ab74f9a034..eb78f7708a62 100644
--- a/code/game/machinery/big_manipulator/big_manipulator.dm
+++ b/code/game/machinery/big_manipulator/big_manipulator.dm
@@ -500,26 +500,26 @@
var/datum/manipulator_task/cargo/dropoff_base/use/t = task
td["turf"] = "[t.offset_dx],[t.offset_dy]"
td["filters_status"] = t.should_use_filters
- td["filtering_mode"] = t.filtering_mode
td["item_filters"] = _collect_filter_names(t.atom_filters)
td["settings_list"] = _collect_priorities(t.interaction_priorities)
td["worker_interaction"] = t.worker_interaction
td["use_post_interaction"] = t.use_post_interaction
td["worker_use_rmb"] = t.worker_use_rmb
td["worker_combat_mode"] = t.worker_combat_mode
+ td["skip_anchored"] = t.skip_anchored
else if(istype(task, /datum/manipulator_task/cargo/interact))
td["task_type"] = TASK_TYPE_INTERACT
var/datum/manipulator_task/cargo/interact/t = task
td["turf"] = "[t.offset_dx],[t.offset_dy]"
td["filters_status"] = t.should_use_filters
- td["filtering_mode"] = t.filtering_mode
td["item_filters"] = _collect_filter_names(t.atom_filters)
td["settings_list"] = _collect_priorities(t.interaction_priorities)
td["worker_interaction"] = t.worker_interaction
td["use_post_interaction"] = t.use_post_interaction
td["worker_use_rmb"] = t.worker_use_rmb
td["worker_combat_mode"] = t.worker_combat_mode
+ td["skip_anchored"] = t.skip_anchored
else if(istype(task, /datum/manipulator_task/simple/wait))
td["task_type"] = TASK_TYPE_WAIT
@@ -587,7 +587,10 @@
return TRUE
if("unbuckle")
- unbuckle_all_mobs()
+ var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker?.resolve()
+ if(poor_monkey)
+ poor_monkey.drop_all_held_items()
+ poor_monkey.forceMove(get_turf(src))
monkey_worker = null
return TRUE
@@ -812,6 +815,8 @@
return TRUE
if("cycle_filtering_mode")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use) || istype(target_task, /datum/manipulator_task/cargo/interact))
+ return FALSE
if(!istype(target_task, /datum/manipulator_task/cargo))
return FALSE
var/datum/manipulator_task/cargo/ct = target_task
@@ -897,6 +902,17 @@
return TRUE
return FALSE
+ if("toggle_skip_anchored")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/unanchor_target = target_task
+ unanchor_target.skip_anchored = !unanchor_target.skip_anchored
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/unanchor_target = target_task
+ unanchor_target.skip_anchored = !unanchor_target.skip_anchored
+ return TRUE
+ return FALSE
+
/// Cycles the given value in the given list.
/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values)
var/current_index = possible_values.Find(current_value)
diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
index 01500efdd5aa..e5f7c361330d 100644
--- a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
+++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
@@ -137,14 +137,18 @@
return FALSE
var/obj/item/held_item = obj_resolve
- var/atom/type_to_use = destination_task.find_type_priority()
+ var/atom/type_to_use = destination_task.find_type_priority(destination_task.skip_anchored)
if(isnull(type_to_use))
- check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point)
+ drop_held_after_use(destination_task)
return FALSE
if(isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use))
- check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point)
+ drop_held_after_use(destination_task)
+ return FALSE
+
+ if(istype(destination_task, /datum/manipulator_task/cargo/interact) && destination_task.should_use_filters && isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use))
+ drop_held_after_use(destination_task)
return FALSE
var/original_loc = held_item.loc
@@ -153,9 +157,10 @@
if(held_item.GetComponent(/datum/component/two_handed))
held_item.attack_self(monkey_resolve)
+ var/old_combat_mode = monkey_resolve.combat_mode
monkey_resolve.combat_mode = destination_task.worker_combat_mode
held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE))
- monkey_resolve.combat_mode = FALSE
+ monkey_resolve.combat_mode = old_combat_mode
do_attack_animation(destination_turf)
manipulator_arm.do_attack_animation(destination_turf)
@@ -217,7 +222,7 @@
obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
finish_manipulation()
else
- schedule_next_cycle()
+ addtimer(CALLBACK(src, PROC_REF(try_use_thing), destination_task, TRUE), BASE_INTERACTION_TIME * 2)
/obj/machinery/big_manipulator/proc/throw_thing(datum/manipulator_task/cargo/dropoff_base/throw/throw_task)
var/drop_turf = throw_task.interaction_turf
@@ -241,21 +246,19 @@
finish_manipulation()
return
- var/atom/type_to_use = destination_task.find_type_priority()
+ var/atom/type_to_use = destination_task.find_type_priority(destination_task.skip_anchored)
if(isnull(type_to_use))
check_end_of_use_for_use_with_empty_hand(destination_task, FALSE)
return
- if(isitem(type_to_use))
- var/obj/item/interact_with_item = type_to_use
- var/resolve_loc = interact_with_item.loc
- monkey_resolve.put_in_active_hand(interact_with_item)
- interact_with_item.attack_self(monkey_resolve)
- interact_with_item.forceMove(resolve_loc)
- else
- monkey_resolve.combat_mode = destination_task.worker_combat_mode
- monkey_resolve.UnarmedAttack(type_to_use)
- monkey_resolve.combat_mode = FALSE
+ if(destination_task.should_use_filters && isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use))
+ check_end_of_use_for_use_with_empty_hand(destination_task, FALSE)
+ return
+
+ var/old_combat_mode = monkey_resolve.combat_mode
+ monkey_resolve.combat_mode = destination_task.worker_combat_mode
+ monkey_resolve.UnarmedAttack(type_to_use, modifiers = list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE))
+ monkey_resolve.combat_mode = old_combat_mode
var/turf/dest_turf = destination_task.interaction_turf
if(dest_turf)
@@ -277,6 +280,10 @@
/// Completes the current manipulation action and schedules the next step.
/obj/machinery/big_manipulator/proc/finish_manipulation()
+ if(held_object)
+ var/obj/resolved = held_object.resolve()
+ if(resolved && resolved.loc == src)
+ resolved.forceMove(drop_location())
held_object = null
manipulator_arm.update_claw(null)
current_task = null
diff --git a/code/game/machinery/big_manipulator/manipulator_tasks.dm b/code/game/machinery/big_manipulator/manipulator_tasks.dm
index 48a136ff255f..00fe12715770 100644
--- a/code/game/machinery/big_manipulator/manipulator_tasks.dm
+++ b/code/game/machinery/big_manipulator/manipulator_tasks.dm
@@ -100,7 +100,7 @@
/datum/manipulator_task/cargo/proc/fill_priority_list(manipulator_tier)
return list()
-/datum/manipulator_task/cargo/proc/find_type_priority()
+/datum/manipulator_task/cargo/proc/find_type_priority(skip_anchored = FALSE)
var/atom/movable/best_candidate = null
var/best_priority_index = INFINITY
@@ -117,6 +117,9 @@
if(!istype(thing, prio.atom_typepath))
continue
+ if(skip_anchored && thing.anchored)
+ continue
+
if(isliving(thing))
var/mob/living/living_mob = thing
if(living_mob.stat == DEAD)
@@ -299,12 +302,16 @@
return FALSE
return TRUE
-/datum/manipulator_task/cargo/dropoff_base/can_run(obj/machinery/big_manipulator/manipulator)
+/datum/manipulator_task/cargo/dropoff_base/use/can_run(obj/machinery/big_manipulator/manipulator)
if(!..())
return FALSE
var/atom/movable/target = manipulator.held_object?.resolve()
if(!target)
return FALSE
+ if(!manipulator.monkey_worker?.resolve())
+ return FALSE
+ if(!find_type_priority(skip_anchored))
+ return FALSE
return can_accept(target)
/datum/manipulator_task/cargo/dropoff_base/run_task(obj/machinery/big_manipulator/manipulator)
@@ -409,6 +416,7 @@
var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
var/worker_combat_mode = FALSE
var/worker_use_rmb = FALSE
+ var/skip_anchored = FALSE
/datum/manipulator_task/cargo/dropoff_base/use/fill_priority_list(manipulator_tier)
var/list/priorities = list(
@@ -434,6 +442,7 @@
data["use_post_interaction"] = use_post_interaction
data["worker_combat_mode"] = worker_combat_mode
data["worker_use_rmb"] = worker_use_rmb
+ data["skip_anchored"] = skip_anchored
return data
/datum/manipulator_task/cargo/dropoff_base/use/New(turf/new_turf, manipulator_tier, serialized_data)
@@ -443,6 +452,7 @@
use_post_interaction = serialized_data["use_post_interaction"]
worker_combat_mode = !!serialized_data["worker_combat_mode"]
worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ skip_anchored = !!serialized_data["skip_anchored"]
return
/datum/manipulator_task/cargo/dropoff_base/use/do_dropoff(obj/machinery/big_manipulator/manipulator)
@@ -456,6 +466,7 @@
var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
var/worker_combat_mode = FALSE
var/worker_use_rmb = FALSE
+ var/skip_anchored = FALSE
/datum/manipulator_task/cargo/interact/fill_priority_list(manipulator_tier)
var/list/priorities = list(
@@ -482,6 +493,7 @@
data["use_post_interaction"] = use_post_interaction
data["worker_combat_mode"] = worker_combat_mode
data["worker_use_rmb"] = worker_use_rmb
+ data["skip_anchored"] = skip_anchored
return data
/datum/manipulator_task/cargo/interact/New(turf/new_turf, manipulator_tier, serialized_data)
@@ -491,11 +503,15 @@
use_post_interaction = serialized_data["use_post_interaction"]
worker_combat_mode = !!serialized_data["worker_combat_mode"]
worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ skip_anchored = !!serialized_data["skip_anchored"]
return
/datum/manipulator_task/cargo/interact/proc/try_interact(obj/machinery/big_manipulator/manipulator)
var/atom/movable/held = manipulator.held_object?.resolve()
if(held)
+ if(!manipulator.monkey_worker?.resolve())
+ manipulator.nothing_ever_happens()
+ return
manipulator.try_use_thing(src)
else
manipulator.use_thing_with_empty_hand(src)
diff --git a/code/game/machinery/big_manipulator/tasking.dm b/code/game/machinery/big_manipulator/tasking.dm
index cc91434363e5..65d355c0b23a 100644
--- a/code/game/machinery/big_manipulator/tasking.dm
+++ b/code/game/machinery/big_manipulator/tasking.dm
@@ -13,6 +13,8 @@
// Moves through the list, skipping tasks that can't run.
/datum/tasking_strategy/sequential
+ /// Separate index for candidate selection to avoid corrupting the task index.
+ var/candidate_index = 1
/datum/tasking_strategy/sequential/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
if(!length(tasks))
@@ -33,16 +35,18 @@
/datum/tasking_strategy/sequential/get_next_candidate(list/candidates)
if(!length(candidates))
return null
- if(current_index < 1 || current_index > length(candidates))
- current_index = 1
- var/candidate = candidates[current_index]
- current_index++
- if(current_index > length(candidates))
- current_index = 1
+ if(candidate_index < 1 || candidate_index > length(candidates))
+ candidate_index = 1
+ var/candidate = candidates[candidate_index]
+ candidate_index++
+ if(candidate_index > length(candidates))
+ candidate_index = 1
return candidate
// Stays on the current task until it can run.
/datum/tasking_strategy/strict
+ /// Separate index for candidate selection to avoid corrupting the task index.
+ var/candidate_index = 1
/datum/tasking_strategy/strict/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
if(!length(tasks))
@@ -60,10 +64,10 @@
/datum/tasking_strategy/strict/get_next_candidate(list/candidates)
if(!length(candidates))
return null
- if(current_index < 1 || current_index > length(candidates))
- current_index = 1
- var/candidate = candidates[current_index]
- current_index++
- if(current_index > length(candidates))
- current_index = 1
+ if(candidate_index < 1 || candidate_index > length(candidates))
+ candidate_index = 1
+ var/candidate = candidates[candidate_index]
+ candidate_index++
+ if(candidate_index > length(candidates))
+ candidate_index = 1
return candidate
diff --git a/code/game/machinery/camera/camera_construction.dm b/code/game/machinery/camera/camera_construction.dm
index 3b3fec8cd5d0..b067d19ae0ed 100644
--- a/code/game/machinery/camera/camera_construction.dm
+++ b/code/game/machinery/camera/camera_construction.dm
@@ -71,177 +71,171 @@
return ..()
/obj/machinery/camera/wrench_act(mob/user, obj/item/tool)
- if(camera_construction_state == CAMERA_STATE_WRENCHED)
- tool.play_tool_sound(src)
- to_chat(user, span_notice("You detach [src] from its place."))
- deconstruct(TRUE)
- return ITEM_INTERACT_SUCCESS
- return ..()
+ if(camera_construction_state != CAMERA_STATE_WRENCHED)
+ return NONE
+ tool.play_tool_sound(src)
+ to_chat(user, span_notice("You detach [src] from its place."))
+ deconstruct(TRUE)
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/camera/crowbar_act(mob/living/user, obj/item/tool)
- if(camera_construction_state == CAMERA_STATE_FINISHED)
- if(!panel_open)
- return ITEM_INTERACT_BLOCKING
- var/list/droppable_parts = list()
- if(xray_module)
- droppable_parts += xray_module
- if(emp_module)
- droppable_parts += emp_module
- if(proximity_monitor)
- droppable_parts += proximity_monitor
- if(!length(droppable_parts))
- return ITEM_INTERACT_BLOCKING
- var/obj/item/choice = tgui_input_list(user, "Select a part to remove", "Part Removal", sort_names(droppable_parts))
- if(isnull(choice))
- return ITEM_INTERACT_BLOCKING
- if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
- return ITEM_INTERACT_BLOCKING
- to_chat(user, span_notice("You remove [choice] from [src]."))
- if(choice == xray_module)
- drop_upgrade(xray_module)
- removeXRay()
- if(choice == emp_module)
- drop_upgrade(emp_module)
- removeEmpProof()
- if(choice == proximity_monitor)
- drop_upgrade(proximity_monitor)
- removeMotion()
- tool.play_tool_sound(src)
- return ITEM_INTERACT_SUCCESS
- return ..()
+ if(camera_construction_state != CAMERA_STATE_FINISHED || !panel_open)
+ return NONE
+ var/list/droppable_parts = list()
+ if(xray_module)
+ droppable_parts += xray_module
+ if(emp_module)
+ droppable_parts += emp_module
+ if(proximity_monitor)
+ droppable_parts += proximity_monitor
+ if(!length(droppable_parts))
+ return ITEM_INTERACT_BLOCKING
+ var/obj/item/choice = tgui_input_list(user, "Select a part to remove", "Part Removal", sort_names(droppable_parts))
+ if(isnull(choice))
+ return ITEM_INTERACT_BLOCKING
+ if(!user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
+ return ITEM_INTERACT_BLOCKING
+ to_chat(user, span_notice("You remove [choice] from [src]."))
+ if(choice == xray_module)
+ drop_upgrade(xray_module)
+ removeXRay()
+ if(choice == emp_module)
+ drop_upgrade(emp_module)
+ removeEmpProof()
+ if(choice == proximity_monitor)
+ drop_upgrade(proximity_monitor)
+ removeMotion()
+ tool.play_tool_sound(src)
+ return ITEM_INTERACT_SUCCESS
/obj/machinery/camera/multitool_act(mob/living/user, obj/item/tool)
- if(camera_construction_state == CAMERA_STATE_FINISHED)
- if(!panel_open)
- return ITEM_INTERACT_BLOCKING
- setViewRange((view_range == initial(view_range)) ? short_range : initial(view_range))
- to_chat(user, span_notice("You [(view_range == initial(view_range)) ? "restore" : "mess up"] the camera's focus."))
- return ITEM_INTERACT_SUCCESS
- return ..()
-
-/obj/machinery/camera/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers)
- if(camera_construction_state != CAMERA_STATE_FINISHED || panel_open)
- if(attacking_item.tool_behaviour == TOOL_ANALYZER)
- if(!isXRay(TRUE)) //don't reveal it was already upgraded if was done via MALF AI Upgrade Camera Network ability
- if(!user.temporarilyRemoveItemFromInventory(attacking_item, newloc = src))
- return
- upgradeXRay(FALSE, TRUE)
- to_chat(user, span_notice("You attach [attacking_item] into [name]'s inner circuits."))
- qdel(attacking_item)
+ if(camera_construction_state != CAMERA_STATE_FINISHED || !panel_open)
+ return NONE
+ setViewRange((view_range == initial(view_range)) ? short_range : initial(view_range))
+ to_chat(user, span_notice("You [(view_range == initial(view_range)) ? "restore" : "mess up"] the camera's focus."))
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/gas_analyzer_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state == CAMERA_STATE_FINISHED && !panel_open)
+ return NONE
+ if(isXRay(TRUE))
+ to_chat(user, span_warning("[src] already has that upgrade!"))
+ return ITEM_INTERACT_BLOCKING
+ if(!user.temporarilyRemoveItemFromInventory(tool, newloc = src))
+ return ITEM_INTERACT_BLOCKING
+ upgradeXRay(FALSE, TRUE)
+ to_chat(user, span_notice("You attach [tool] into [src]'s inner circuits."))
+ qdel(tool)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/plasma_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state == CAMERA_STATE_FINISHED && !panel_open)
+ return NONE
+ if(isEmpProof(TRUE))
+ to_chat(user, span_warning("[src] already has that upgrade!"))
+ return ITEM_INTERACT_BLOCKING
+ if(!tool.use_tool(src, user, 0, amount = 1))
+ return ITEM_INTERACT_BLOCKING
+ upgradeEmpProof(FALSE, TRUE)
+ to_chat(user, span_notice("You attach [tool] into [src]'s inner circuits."))
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/prox_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state == CAMERA_STATE_FINISHED && !panel_open)
+ return NONE
+ if(isMotion())
+ to_chat(user, span_warning("[src] already has that upgrade!"))
+ return ITEM_INTERACT_BLOCKING
+ if(!user.temporarilyRemoveItemFromInventory(tool, newloc = src))
+ return ITEM_INTERACT_BLOCKING
+ upgradeMotion()
+ to_chat(user, span_notice("You attach [tool] into [src]'s inner circuits."))
+ qdel(tool)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/cable_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state != CAMERA_STATE_WELDED)
+ return NONE
+ if(!astype(tool, /obj/item/stack/cable_coil)?.use(2))
+ to_chat(user, span_warning("You need two lengths of cable to wire [src]!"))
+ return ITEM_INTERACT_BLOCKING
+ to_chat(user, span_notice("You add wires to [src]."))
+ camera_construction_state = CAMERA_STATE_WIRED
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/computer_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state != CAMERA_STATE_FINISHED)
+ return NONE
+ var/obj/item/modular_computer/computer = tool
+ var/note_name = sanitize(computer.name)
+ var/datum/computer_file/program/notepad/notepad_app = locate() in computer.stored_files
+ if(!notepad_app)
+ return ITEM_INTERACT_BLOCKING
+ var/note_text = sanitize(notepad_app.written_note)
+ if(!note_text)
+ return ITEM_INTERACT_BLOCKING
+ display_note(user, note_name, note_text, TRUE)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/paper_act(mob/living/user, obj/item/tool)
+ if(camera_construction_state != CAMERA_STATE_FINISHED)
+ return NONE
+ var/obj/item/paper/paper = tool
+ last_shown_paper = paper.copy(paper.type, null)
+ var/note_name = sanitize(last_shown_paper.name)
+ last_shown_paper.camera_holder = WEAKREF(src)
+ display_note(user, note_name, null, FALSE)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/camera/proc/display_note(mob/living/user, title, text, is_computer)
+ to_chat(user, span_notice("You hold up \the [title] up to the camera..."))
+ user.log_talk(title, LOG_GAME, "Pressed to camera", TRUE)
+ user.changeNext_move(CLICK_CD_MELEE)
+
+ // Iterate over all living mobs and check if anyone is elibile to view the paper.
+ // This is backwards, but cameras don't store a list of people that are looking through them,
+ // and we'll have to iterate this list anyway so we can use it to pull out AIs too.
+ for(var/mob/potential_viewer as anything in GLOB.player_list)
+ // All AIs view through cameras, so we need to check them regardless.
+ if(isAI(potential_viewer))
+ var/mob/living/silicon/ai/ai = potential_viewer
+ if(ai.control_disabled || ai.stat == DEAD)
+ continue
+
+ ai.log_talk(title, LOG_VICTIM, "Pressed to camera from [key_name(user)]", FALSE)
+ if(is_computer)
+ ai.last_tablet_note_seen = "
[title][text]"
else
- to_chat(user, span_warning("[src] already has that upgrade!"))
- return
- else if(istype(attacking_item, /obj/item/stack/sheet/mineral/plasma))
- if(!isEmpProof(TRUE)) //don't reveal it was already upgraded if was done via MALF AI Upgrade Camera Network ability
- if(attacking_item.use_tool(src, user, 0, amount=1))
- upgradeEmpProof(FALSE, TRUE)
- to_chat(user, span_notice("You attach [attacking_item] into [name]'s inner circuits."))
+ log_paper("[key_name(user)] held [last_shown_paper] up to [src], requesting [key_name(ai)] read it.")
+
+ var/href_string = is_computer ? "show_tablet_note=1" : "show_paper_note=[REF(last_shown_paper)]"
+ if(user.name == "Unknown")
+ to_chat(ai, "[span_name("[user]")] holds \a [title] up to one of your cameras...")
else
- to_chat(user, span_warning("[src] already has that upgrade!"))
- return
- else if(isprox(attacking_item))
- if(!isMotion())
- if(!user.temporarilyRemoveItemFromInventory(attacking_item, newloc = src))
- return
- upgradeMotion()
- to_chat(user, span_notice("You attach [attacking_item] into [name]'s inner circuits."))
- qdel(attacking_item)
+ to_chat(ai, "[span_name("[user]")] holds \a [title] up to one of your cameras...")
+
+ // If it's not an AI, eye if the client's eye is set to the camera. I wonder if this even works anymore with tgui camera apps and stuff?
+ else if(potential_viewer.client?.eye == src)
+ potential_viewer.log_talk(title, LOG_VICTIM, "Pressed to camera from [key_name(user)]", FALSE)
+ if(!is_computer)
+ log_paper("[key_name(user)] held [last_shown_paper] up to [src], and [key_name(potential_viewer)] may read it.")
+ to_chat(potential_viewer, "[span_name("[user]")] holds \a [title] up to your camera...")
else
- to_chat(user, span_warning("[src] already has that upgrade!"))
- return
- switch(camera_construction_state)
- if(CAMERA_STATE_WELDED)
- if(istype(attacking_item, /obj/item/stack/cable_coil))
- var/obj/item/stack/cable_coil/attacking_cable = attacking_item
- if(attacking_cable.use(2))
- to_chat(user, span_notice("You add wires to [src]."))
- camera_construction_state = CAMERA_STATE_WIRED
- else
- to_chat(user, span_warning("You need two lengths of cable to wire a camera!"))
- return
- if(CAMERA_STATE_FINISHED)
- if(istype(attacking_item, /obj/item/modular_computer))
- var/itemname = ""
- var/info = ""
-
- var/obj/item/modular_computer/computer = attacking_item
- for(var/datum/computer_file/program/notepad/notepad_app in computer.stored_files)
- info = notepad_app.written_note
- break
-
- if(!info)
- return
-
- itemname = computer.name
- itemname = sanitize(itemname)
- info = sanitize(info)
- to_chat(user, span_notice("You hold \the [itemname] up to the camera..."))
- user.log_talk(itemname, LOG_GAME, log_globally=TRUE, tag="Pressed to camera")
- user.changeNext_move(CLICK_CD_MELEE)
-
- for(var/mob/potential_viewer as anything in GLOB.player_list)
- if(isAI(potential_viewer))
- var/mob/living/silicon/ai/ai = potential_viewer
- if(ai.control_disabled || (ai.stat == DEAD))
- continue
-
- ai.log_talk(itemname, LOG_VICTIM, tag="Pressed to camera from [key_name(user)]", log_globally=FALSE)
- ai.last_tablet_note_seen = "[itemname][info]"
-
- if(user.name == "Unknown")
- to_chat(ai, "[span_name(user)] holds \a [itemname] up to one of your cameras ...")
- else
- to_chat(ai, "[user] holds \a [itemname] up to one of your cameras ...")
- continue
-
- if (potential_viewer.client?.eye == src)
- to_chat(potential_viewer, "[span_name("[user]")] holds \a [itemname] up to one of the cameras ...")
- potential_viewer.log_talk(itemname, LOG_VICTIM, tag="Pressed to camera from [key_name(user)]", log_globally=FALSE)
- potential_viewer << browse("[itemname][info]", "window=[itemname]")
- return
-
- if(istype(attacking_item, /obj/item/paper))
- // Grab the paper, sanitise the name as we're about to just throw it into chat wrapped in HTML tags.
- var/obj/item/paper/paper = attacking_item
-
- // Make a complete copy of the paper, store a ref to it locally on the camera.
- last_shown_paper = paper.copy(paper.type, null)
-
- // Then sanitise the name because we're putting it directly in chat later.
- var/item_name = sanitize(last_shown_paper.name)
-
- // Start the process of holding it up to the camera.
- to_chat(user, span_notice("You hold \the [item_name] up to the camera..."))
- user.log_talk(item_name, LOG_GAME, log_globally=TRUE, tag="Pressed to camera")
- user.changeNext_move(CLICK_CD_MELEE)
-
- // And make a weakref we can throw around to all potential viewers.
- last_shown_paper.camera_holder = WEAKREF(src)
-
- // Iterate over all living mobs and check if anyone is elibile to view the paper.
- // This is backwards, but cameras don't store a list of people that are looking through them,
- // and we'll have to iterate this list anyway so we can use it to pull out AIs too.
- for(var/mob/potential_viewer in GLOB.player_list)
- // All AIs view through cameras, so we need to check them regardless.
- if(isAI(potential_viewer))
- var/mob/living/silicon/ai/ai = potential_viewer
- if(ai.control_disabled || (ai.stat == DEAD))
- continue
-
- ai.log_talk(item_name, LOG_VICTIM, tag="Pressed to camera from [key_name(user)]", log_globally=FALSE)
- log_paper("[key_name(user)] held [last_shown_paper] up to [src], requesting [key_name(ai)] read it.")
-
- if(user.name == "Unknown")
- to_chat(ai, "[span_name(user.name)] holds \a [item_name] up to one of your cameras ...")
- else
- to_chat(ai, "[user] holds \a [item_name] up to one of your cameras ...")
- continue
-
- // If it's not an AI, eye if the client's eye is set to the camera. I wonder if this even works anymore with tgui camera apps and stuff?
- if (potential_viewer.client?.eye == src)
- log_paper("[key_name(user)] held [last_shown_paper] up to [src], and [key_name(potential_viewer)] may read it.")
- potential_viewer.log_talk(item_name, LOG_VICTIM, tag="Pressed to camera from [key_name(user)]", log_globally=FALSE)
- to_chat(potential_viewer, "[span_name(user)] holds \a [item_name] up to your camera...")
- return
-
- return ..()
+ potential_viewer << browse("[title][text]", "window=[title]")
+
+/obj/machinery/camera/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(user.combat_mode)
+ return ITEM_INTERACT_SKIP_TO_ATTACK
+ if(istype(tool, /obj/item/stack/sheet/mineral/plasma))
+ return plasma_act(user, tool)
+ if(istype(tool, /obj/item/analyzer))
+ return gas_analyzer_act(user, tool)
+ if(isprox(tool))
+ return prox_act(user, tool)
+ if(istype(tool, /obj/item/stack/cable_coil))
+ return cable_act(user, tool)
+ if(istype(tool, /obj/item/modular_computer))
+ return computer_act(user, tool)
+ if(istype(tool, /obj/item/paper))
+ return paper_act(user, tool)
+ return NONE
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 57dffa377e5f..6bbab50fa790 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -28,6 +28,8 @@
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.1
active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.2
+ tacmap_color = TACMAP_DOOR
+
/// The animation we're currently playing, if any
var/animation
var/visible = TRUE
diff --git a/code/game/machinery/mining_weather_monitor.dm b/code/game/machinery/mining_weather_monitor.dm
index c057b44f56cd..402a40dd8f16 100644
--- a/code/game/machinery/mining_weather_monitor.dm
+++ b/code/game/machinery/mining_weather_monitor.dm
@@ -346,8 +346,8 @@ GLOBAL_LIST_EMPTY(weather_towers)
/// Return a list of weather typepaths that this tower can summon when given a weather core.
/obj/machinery/power/weather_tower/proc/get_summonable_weather_types()
. = list(
- /datum/weather/ash_storm,
- /datum/weather/rain_storm,
+ /datum/weather/particle/ash_storm,
+ /datum/weather/particle/rain_storm,
/datum/weather/sand_storm,
/datum/weather/snow_storm,
)
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index 801ed06603cd..f682219a06bd 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -782,6 +782,19 @@ DEFINE_BITFIELD(turret_flags, list(
. = ..()
AddElement(/datum/element/empprotection, EMP_PROTECT_SELF | EMP_PROTECT_WIRES)
AddElement(/datum/element/nav_computer_icon, 'icons/effects/nav_computer_indicators.dmi', "turret", FALSE)
+ add_minimap_blip(src, MINIMAP_SYNDIE_TURRET_BLIP, "sentry_passive")
+
+/obj/machinery/porta_turret/syndicate/proc/update_turret_minimap_icon(new_icon_state)
+ var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(MINIMAP_SYNDIE_TURRET_BLIP, src)
+ if(isnull(blip))
+ return
+ blip.icon_state = new_icon_state
+
+/obj/machinery/porta_turret/syndicate/shootAt(atom/movable/target)
+ . = ..()
+ if(raised && (obj_flags & EMAGGED || last_fired == world.time))
+ update_turret_minimap_icon("sentry_firing")
+ addtimer(CALLBACK(src, PROC_REF(update_turret_minimap_icon), "sentry_passive"), shot_delay + 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
/obj/machinery/porta_turret/syndicate/setup()
return
diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm
index ea407acfa3af..17e6f415af58 100644
--- a/code/game/objects/effects/anomalies/anomalies_weather.dm
+++ b/code/game/objects/effects/anomalies/anomalies_weather.dm
@@ -55,7 +55,7 @@
/obj/effect/anomaly/weather/proc/select_weather()
return pick(
- /datum/weather/rain_storm,
+ /datum/weather/particle/rain_storm,
/datum/weather/snow_storm,
/datum/weather/sand_storm,
)
@@ -97,4 +97,4 @@
// maybe we can put acid rain in this later?
// though it'd feel unfair if it showed up and immediately dumped acid on people.
// we would need an even longer telegraphing time for that
- weather_type = /datum/weather/rain_storm
+ weather_type = /datum/weather/particle/rain_storm
diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm
index 481849c00eb1..c35b1d645489 100644
--- a/code/game/objects/effects/particles/fire.dm
+++ b/code/game/objects/effects/particles/fire.dm
@@ -31,7 +31,7 @@
gradient = list("#FBAF4D", "#FCE6B6", "#FD481C")
position = generator(GEN_BOX, list(-12,-16,0), list(12,16,0), NORMAL_RAND)
drift = generator(GEN_VECTOR, list(-0.1,0), list(0.1,0.025), UNIFORM_RAND)
- spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND)
+ spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND)
/particles/embers/minor
diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm
index c5626ef646c6..cd7766ccff7e 100644
--- a/code/game/objects/effects/particles/smoke.dm
+++ b/code/game/objects/effects/particles/smoke.dm
@@ -128,7 +128,7 @@
position = generator(GEN_BOX, list(-17,-15,0), list(24,15,0), NORMAL_RAND)
scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
drift = generator(GEN_SPHERE, list(-0.01,0), list(0.01,0.01), UNIFORM_RAND)
- spin = generator(GEN_NUM, list(-2,2), NORMAL_RAND)
+ spin = generator(GEN_NUM, -2, 2, NORMAL_RAND)
gravity = list(0.05, 0.28)
friction = 0.3
grow = 0.037
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index 13d2f6f4aa35..774bbc2765e4 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1333,24 +1333,23 @@
/obj/item/circuitboard/machine/hydroponics/proc/changeindicators(mob/living/user, obj/item/I)
if(build_path == /obj/machinery/hydroponics/constructable/oldstyle)
- name = "Hydroponics Tray"
+ name = "Hydroponics Tray [name_extension]"
build_path = /obj/machinery/hydroponics/constructable
- balloon_alert(user, "defaulting indicator location.")
+ balloon_alert(user, "defaulting indicator location")
else
- name = "Old-Designed Hydropoincs Tray"
+ name = "Hydroponics Tray (Alt) [name_extension]"
build_path = /obj/machinery/hydroponics/constructable/oldstyle
- balloon_alert(user, "moving the indicators...")
- return TRUE
+ balloon_alert(user, "moved indicators location")
/obj/item/circuitboard/machine/hydroponics/item_interaction(mob/living/user, obj/item/I, list/modifiers)
if(istype(I, /obj/item/plant_analyzer))
changeindicators(user)
- else
- return ..()
+ return ITEM_INTERACT_SUCCESS
+ return ..()
/obj/item/circuitboard/machine/hydroponics/screwdriver_act(mob/living/user, obj/item/tool)
- src.changeindicators(user)
- return
+ changeindicators(user)
+ return ITEM_INTERACT_SUCCESS
/obj/item/circuitboard/machine/hydroponics/fullupgrade
build_path = /obj/machinery/hydroponics/constructable/fullupgrade
diff --git a/code/game/objects/items/food/martian.dm b/code/game/objects/items/food/martian.dm
index c6d6c3e51f82..ce92aa455585 100644
--- a/code/game/objects/items/food/martian.dm
+++ b/code/game/objects/items/food/martian.dm
@@ -171,6 +171,31 @@
foodtypes = GRAIN
crafting_complexity = FOOD_COMPLEXITY_3
+/obj/item/food/spaghetti/ramen_dry
+ name = "dry space ramen"
+ desc = "Compressed dehydrated noodles."
+ icon = 'icons/obj/food/martian.dmi'
+ icon_state = "raw_ramen"
+
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 3
+ )
+ tastes = list("ramen" = 1)
+ foodtypes = GRAIN
+ crafting_complexity = FOOD_COMPLEXITY_2
+
+/obj/item/food/spaghetti/ramen_beef
+ name = "beef space ramen"
+ desc = "Steaming springy noodles dripping with rich artificial beef broth. Somehow both comforting and vaguely chemical."
+ icon = 'icons/obj/food/martian.dmi'
+ icon_state = "cooked_ramen"
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 3
+ )
+ tastes = list("ramen" = 1, "beef" = 1)
+ foodtypes = GRAIN
+ crafting_complexity = FOOD_COMPLEXITY_3
+
/obj/item/food/bread/reispan
name = "reispan"
desc = "Though the concept of rice bread has been common in Asia for centuries, the reispan as we know it today is most commonly associated with Mars- where limited arable land has forced ingenuity."
diff --git a/code/game/objects/items/food/snacks.dm b/code/game/objects/items/food/snacks.dm
index 7caf726e8123..b20e3cfb8baa 100644
--- a/code/game/objects/items/food/snacks.dm
+++ b/code/game/objects/items/food/snacks.dm
@@ -70,6 +70,7 @@
tastes = list("dried meat" = 1)
w_class = WEIGHT_CLASS_SMALL
foodtypes = JUNKFOOD | MEAT | SUGAR
+ food_flags = FOOD_FINGER_FOOD
crafting_complexity = FOOD_COMPLEXITY_1
custom_materials = list(/datum/material/meat = MEATSLAB_MATERIAL_AMOUNT)
@@ -96,6 +97,7 @@
junkiness = 20
tastes = list("salt" = 1, "crisps" = 1)
foodtypes = VEGETABLES|JUNKFOOD|FRIED
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/chips/make_leave_trash()
@@ -115,6 +117,7 @@
)
tastes = list("salt" = 1, "shrimp" = 1)
foodtypes = JUNKFOOD | FRIED | SEAFOOD
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/no_raisin
@@ -190,6 +193,7 @@
junkiness = 25
tastes = list("cheese" = 5, "crisps" = 2)
foodtypes = JUNKFOOD | DAIRY | SUGAR
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/syndicake
@@ -203,6 +207,7 @@
)
tastes = list("sweetness" = 3, "cake" = 1)
foodtypes = GRAIN | FRUIT | VEGETABLES
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/energybar
@@ -227,6 +232,7 @@
food_reagents = list(/datum/reagent/consumable/nutriment = 2)
tastes = list("peanuts" = 4, "anger" = 1)
foodtypes = JUNKFOOD | NUTS
+ food_flags = FOOD_FINGER_FOOD
custom_price = PAYCHECK_CREW * 0.8 //nuts are expensive in real life, and this is the best food in the vendor.
junkiness = 10 //less junky than other options, since peanuts are a decently healthy snack option
w_class = WEIGHT_CLASS_SMALL
@@ -322,6 +328,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
tastes = list("chocolate candy" = 3)
junkiness = 25
foodtypes = JUNKFOOD
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/cnds/suicide_act(mob/living/user)
@@ -396,6 +403,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
) //a healthy but expensive snack
tastes = list("pistachios" = 4, "subtle sweetness" = 1)
foodtypes = JUNKFOOD | NUTS
+ food_flags = FOOD_FINGER_FOOD
custom_price = PAYCHECK_CREW//pistachios are even more expensive.
junkiness = 10 //on par with peanuts
w_class = WEIGHT_CLASS_SMALL
@@ -414,6 +422,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
) //1 cornoil is equal to 1.33 nutriment
tastes = list("sunflowers" = 5)
foodtypes = JUNKFOOD | NUTS
+ food_flags = FOOD_FINGER_FOOD
custom_price = PAYCHECK_LOWER * 0.4 //sunflowers are cheap in real life.
bite_consumption = 1
junkiness = 25
@@ -448,6 +457,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
custom_price = PAYCHECK_LOWER * 0.8 //we are filled to the brim with flavor
tastes = list("fried corn" = 1)
foodtypes = JUNKFOOD | FRIED
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/cornchips/make_leave_trash()
@@ -540,6 +550,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
)
tastes = list("popcorn" = 1, "caramel" = 1, "peanuts" = 1)
foodtypes = JUNKFOOD | SUGAR | NUTS
+ food_flags = FOOD_FINGER_FOOD
junkiness = 25
w_class = WEIGHT_CLASS_SMALL
@@ -558,6 +569,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
tastes = list("biscuit" = 1, "chocolate" = 1)
junkiness = 25
foodtypes = JUNKFOOD | GRAIN
+ food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
/obj/item/food/sticko/matcha
@@ -635,6 +647,7 @@ GLOBAL_LIST_INIT(safe_peanut_types, populate_safe_peanut_types())
/datum/reagent/consumable/liquidelectricity/enriched = 2,
/datum/reagent/consumable/sugar = 3
)
+ food_flags = FOOD_FINGER_FOOD
tastes = list("sugar" = 1, "lightning" = 1)
/obj/item/food/shok_roks/citrus
diff --git a/code/game/objects/items/implants/implant_tacmap.dm b/code/game/objects/items/implants/implant_tacmap.dm
new file mode 100644
index 000000000000..33ebbe88c08e
--- /dev/null
+++ b/code/game/objects/items/implants/implant_tacmap.dm
@@ -0,0 +1,120 @@
+/obj/item/implant/tacmap
+ name = "tactical map implant"
+ desc = "provides you with a map"
+ actions_types = list(/datum/action/minimap)
+ var/wearer_icon_state = null
+ /// Optional z-trait that resolves to the first z-level and locks the minimap there.
+ var/minimap_fixed_z_trait
+ /// Whether this implant allows drawing/labeling on the personal minimap HUD.
+ var/can_draw_on_personal_minimap = FALSE
+ var/static/list/minimap_refresh_signals = list(
+ COMSIG_MOB_STATCHANGE,
+ COMSIG_LIVING_REVIVE,
+ COMSIG_MOB_GHOSTIZED,
+ )
+
+/obj/item/implant/tacmap/implant(mob/living/target, mob/user, silent, force)
+ . = ..()
+ if(.)
+ configure_minimap_action()
+ RegisterSignals(target, minimap_refresh_signals, PROC_REF(refresh_minimap_icon))
+ addtimer(CALLBACK(src, PROC_REF(update_minimap_icon), target), 0.1 SECONDS) // Mobs are spawned inside nullspace sometimes so this avoids that hijinks.
+
+/obj/item/implant/tacmap/removed(mob/living/source, silent, special)
+ UnregisterSignal(source, minimap_refresh_signals)
+ remove_minimap(source)
+ return ..()
+
+///Remove all action of type minimap from the wearer, and make him disappear from the minimap
+/obj/item/implant/tacmap/proc/remove_minimap(mob/user)
+ remove_minimap_blip(MINIMAP_NUKEOP_BLIP, user)
+
+/obj/item/implant/tacmap/proc/refresh_minimap_icon()
+ SIGNAL_HANDLER
+ if(imp_in)
+ update_minimap_icon(imp_in)
+
+/obj/item/implant/tacmap/proc/resolve_fixed_minimap_z_level()
+ if(!isnull(minimap_fixed_z_trait))
+ var/list/trait_levels = SSmapping.levels_by_trait(minimap_fixed_z_trait)
+ if(length(trait_levels))
+ return trait_levels[1]
+ return null
+
+/obj/item/implant/tacmap/proc/configure_minimap_action()
+ var/datum/action/minimap/minimap_action = locate(/datum/action/minimap) in actions
+ if(isnull(minimap_action))
+ return
+ minimap_action.fixed_z_level = resolve_fixed_minimap_z_level()
+ minimap_action.can_draw = can_draw_on_personal_minimap
+
+/obj/item/implant/tacmap/proc/get_minimap_icon_state(mob/living/wearer)
+ return wearer_icon_state
+
+///Updates the wearer's minimap icon
+/obj/item/implant/tacmap/proc/update_minimap_icon(mob/wearer)
+ SIGNAL_HANDLER
+ remove_minimap_blip(MINIMAP_NUKEOP_BLIP, wearer)
+ add_minimap_blip(wearer, MINIMAP_NUKEOP_BLIP, get_minimap_icon_state(wearer))
+
+/obj/item/implant/tacmap/nuclear // Nukie subtype, map shows you nuke disk, operatives, cayenne and the nuke
+ actions_types = list(/datum/action/minimap/nuclear)
+ wearer_icon_state = "syndicate"
+
+/obj/item/implant/tacmap/nuclear/implant(mob/living/target, mob/user, silent, force)
+ . = ..()
+ if(.)
+ RegisterSignal(target, COMSIG_MINIMAP_ACTION_TRIGGER, PROC_REF(deny_nukie_base_open))
+
+/obj/item/implant/tacmap/nuclear/get_minimap_icon_state(mob/living/wearer)
+ if(wearer.stat != DEAD && istype(wearer, /mob/living/basic/carp/pet/cayenne))
+ return "cayenne"
+ . = ..()
+
+/obj/item/implant/tacmap/nuclear/removed(mob/living/source, silent, special)
+ UnregisterSignal(source, COMSIG_MINIMAP_ACTION_TRIGGER)
+ return ..()
+
+/obj/item/implant/tacmap/nuclear/proc/deny_nukie_base_open(mob/living/user)
+ var/turf/user_turf = get_turf(user)
+ if(user_turf.onSyndieBase())
+ user.balloon_alert(user, "can't use implant in the base, go to the holotable!")
+ return COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL
+
+/obj/item/implant/tacmap/nuclear/cayenne // subtype used for cayenne and syndie sentience potions in general
+ wearer_icon_state = "cayenne"
+
+/obj/item/implant/tacmap/nuclear/leader // Leader subtype lets him draw on the map
+ actions_types = list(/datum/action/minimap/nuclear)
+ can_draw_on_personal_minimap = TRUE
+ wearer_icon_state = "syndicate_leader"
+
+/obj/item/implant/tacmap/nuclear/leader/implant(mob/living/target, mob/user, silent, force)
+ . = ..()
+ if(.)
+ ADD_TRAIT(target, TRAIT_MINIMAP_TABLE_DRAW, REF(src))
+
+/obj/item/implant/tacmap/nuclear/leader/removed(mob/living/source, silent, special)
+ REMOVE_TRAIT(source, TRAIT_MINIMAP_TABLE_DRAW, REF(src))
+ return ..()
+
+// Subtype that just lets them open it off-base.
+/obj/item/implant/tacmap/nuclear/offbase
+ minimap_fixed_z_trait = ZTRAIT_STATION
+ can_draw_on_personal_minimap = TRUE
+
+/obj/item/implant/tacmap/nuclear/offbase/deny_nukie_base_open(mob/living/user)
+ return
+
+/obj/item/implanter/tacmap
+ name = "implanter (minimap)"
+ imp_type = /obj/item/implant/tacmap
+
+/obj/item/implanter/tacmap/nuclear
+ name = "implanter (operative minimap)"
+ imp_type = /obj/item/implant/tacmap/nuclear
+
+/obj/item/implantcase/tacmap
+ name = "implant case - 'Tactical Map'"
+ desc = "A glass case containing an implant with a virtual map."
+ imp_type = /obj/item/implant/tacmap
diff --git a/code/game/objects/items/mop.dm b/code/game/objects/items/mop.dm
index 591dd127411c..35f4e2675301 100644
--- a/code/game/objects/items/mop.dm
+++ b/code/game/objects/items/mop.dm
@@ -36,6 +36,7 @@
/obj/item/mop/Initialize(mapload)
. = ..()
AddComponent(/datum/component/cleaner, mopspeed, pre_clean_callback=CALLBACK(src, PROC_REF(should_clean)), on_cleaned_callback=CALLBACK(src, PROC_REF(apply_reagents)))
+ AddComponent(/datum/component/walking_aid)
create_reagents(max_reagent_volume)
GLOB.janitor_devices += src
diff --git a/code/game/objects/items/pinpointer.dm b/code/game/objects/items/pinpointer.dm
index 2e23ffdfbb77..db79f9388df7 100644
--- a/code/game/objects/items/pinpointer.dm
+++ b/code/game/objects/items/pinpointer.dm
@@ -17,12 +17,23 @@
sound_vary = TRUE
pickup_sound = SFX_GENERIC_DEVICE_PICKUP
drop_sound = SFX_GENERIC_DEVICE_DROP
+ /// Is the pinpointer on?
var/active = FALSE
- var/atom/movable/target //The thing we're searching for
- var/minimum_range = 0 //at what range the pinpointer declares you to be at your destination
- var/alert = FALSE // TRUE to display things more seriously
- var/process_scan = TRUE // some pinpointers change target every time they scan, which means we can't have it change very process but instead when it turns on.
- var/icon_suffix = "" // for special pinpointer icons
+ ///The thing we're searching for
+ var/atom/movable/target
+ /// TRUE to display things more seriously
+ var/alert = FALSE
+ /// Some pinpointers change target every time they scan, which means we can't have it change every process() but instead when it turns on.
+ var/process_scan = TRUE
+ /// Icon_state suffix for special pinpointer icons
+ var/icon_suffix = ""
+
+ /// At what range the pinpointer declares you to be at your destination. Use to hide the exact location of your target.
+ var/minimum_range = 0
+ /// From 1 to this value, the sprite will display as though you're close.
+ var/close_range = 8
+ /// From close_range + 1 to this value, the sprite will display as though you're medium distance away. Past this value, we'll display as though you're far.
+ var/medium_range = 16
/obj/item/pinpointer/Initialize(mapload)
. = ..()
@@ -85,13 +96,13 @@
return "pinon[alert ? "alert" : ""]direct[icon_suffix]"
else
setDir(get_dir(here, there))
- switch(get_dist(here, there))
- if(1 to 8)
- return "pinon[alert ? "alert" : "close"][icon_suffix]"
- if(9 to 16)
- return "pinon[alert ? "alert" : "medium"][icon_suffix]"
- if(16 to INFINITY)
- return "pinon[alert ? "alert" : "far"][icon_suffix]"
+ var/current_distance = get_dist(here, there)
+ if(current_distance >= 1 && current_distance <= close_range)
+ return "pinon[alert ? "alert" : "close"][icon_suffix]"
+ else if(current_distance > (close_range + 1) && current_distance <= medium_range)
+ return "pinon[alert ? "alert" : "medium"][icon_suffix]"
+ else if(current_distance > medium_range)
+ return "pinon[alert ? "alert" : "far"][icon_suffix]"
/obj/item/pinpointer/crew // A replacement for the old crew monitoring consoles
name = "crew pinpointer"
@@ -100,9 +111,14 @@
worn_icon_state = "pinpointer_crew"
custom_price = PAYCHECK_CREW * 6
custom_premium_price = PAYCHECK_CREW * 6
- var/has_owner = FALSE
+ /// The mob that the pinpointer is owned by.
var/pinpointer_owner = null
- var/ignore_suit_sensor_level = FALSE /// Do we find people even if their suit sensors are turned off
+ /// Do we find people even if their suit sensors are turned off
+ var/ignore_suit_sensor_level = FALSE
+
+/obj/item/pinpointer/crew/Destroy()
+ . = ..()
+ pinpointer_owner = null
/obj/item/pinpointer/crew/proc/trackable(mob/living/carbon/human/H)
var/turf/here = get_turf(src)
@@ -120,7 +136,7 @@
user.visible_message(span_notice("[user] deactivates [user.p_their()] pinpointer."), span_notice("You deactivate your pinpointer."))
return
- if (has_owner && !pinpointer_owner)
+ if (!pinpointer_owner)
pinpointer_owner = user
if (pinpointer_owner && pinpointer_owner != user)
@@ -173,6 +189,7 @@
/obj/item/pinpointer/pair
name = "pair pinpointer"
desc = "A handheld tracking device that locks onto its other half of the matching pair."
+ /// Reference to the other, specific pinpointer that it's bought with. Assigned on /obj/item/storage/box/pinpointer_pairs.
var/other_pair
/obj/item/pinpointer/pair/Destroy()
@@ -197,6 +214,7 @@
icon_state = "pinpointer_hunter"
worn_icon_state = "pinpointer_black"
icon_suffix = "_hunter"
+ /// Reference to the bounty hunter shuttle's docking port.
var/obj/docking_port/mobile/shuttleport
/obj/item/pinpointer/shuttle/Initialize(mapload)
diff --git a/code/game/objects/items/pitchfork.dm b/code/game/objects/items/pitchfork.dm
index 99f714f09f3b..497e213acba0 100644
--- a/code/game/objects/items/pitchfork.dm
+++ b/code/game/objects/items/pitchfork.dm
@@ -32,6 +32,7 @@
. = ..()
AddComponent(/datum/component/jousting)
AddComponent(/datum/component/two_handed, force_unwielded=7, force_wielded=15, icon_wielded="[base_icon_state]1")
+ AddComponent(/datum/component/walking_aid)
/obj/item/pitchfork/update_icon_state()
icon_state = "[base_icon_state]0"
diff --git a/code/game/objects/items/rcd/RDD.dm b/code/game/objects/items/rcd/RDD.dm
new file mode 100644
index 000000000000..b3b65931fe17
--- /dev/null
+++ b/code/game/objects/items/rcd/RDD.dm
@@ -0,0 +1,344 @@
+//RAPID DECORATION DEVICE
+
+/// Multiplier applied to cost when using RDD — each decoration costs this many matter units
+#define RDD_COST_MULTIPLIER 2
+
+/// All decoration designs available in the RDD
+GLOBAL_LIST_INIT(rdd_designs, list(
+ "Grasses" = list(
+ list("name" = "Plastic Grass Patch", "path" = /obj/structure/decoration/grass/first),
+ list("name" = "Plastic Grass Patch (Alt)", "path" = /obj/structure/decoration/grass/second),
+ list("name" = "Plastic Grass Patch (Alt 2)", "path" = /obj/structure/decoration/grass/third),
+ list("name" = "Plastic Grass Patch (Random)", "path" = /obj/structure/decoration/grass/style_random),
+ list("name" = "Plastic Brown Grass", "path" = /obj/structure/decoration/grass/brown/first),
+ list("name" = "Plastic Brown Grass (Alt)", "path" = /obj/structure/decoration/grass/brown/second),
+ list("name" = "Plastic Brown Grass (Alt 2)", "path" = /obj/structure/decoration/grass/brown/third),
+ list("name" = "Plastic Brown Grass (Random)", "path" = /obj/structure/decoration/grass/brown/style_random),
+ list("name" = "Plastic Jungle Grass", "path" = /obj/structure/decoration/jungle_grass/first),
+ list("name" = "Plastic Jungle Grass (Alt)", "path" = /obj/structure/decoration/jungle_grass/second),
+ list("name" = "Plastic Jungle Grass (Alt 2)", "path" = /obj/structure/decoration/jungle_grass/third),
+ list("name" = "Plastic Jungle Grass (Alt 3)", "path" = /obj/structure/decoration/jungle_grass/fourth),
+ list("name" = "Plastic Jungle Grass (Alt 4)", "path" = /obj/structure/decoration/jungle_grass/fifth),
+ list("name" = "Plastic Jungle Grass (Random)", "path" = /obj/structure/decoration/jungle_grass/style_random),
+ list("name" = "Plastic Jungle Grass B", "path" = /obj/structure/decoration/jungle_grass/b/first),
+ list("name" = "Plastic Jungle Grass B (Alt)", "path" = /obj/structure/decoration/jungle_grass/b/second),
+ list("name" = "Plastic Jungle Grass B (Alt 2)", "path" = /obj/structure/decoration/jungle_grass/b/third),
+ list("name" = "Plastic Jungle Grass B (Alt 3)", "path" = /obj/structure/decoration/jungle_grass/b/fourth),
+ list("name" = "Plastic Jungle Grass B (Alt 4)", "path" = /obj/structure/decoration/jungle_grass/b/fifth),
+ list("name" = "Plastic Jungle Grass B (Random)", "path" = /obj/structure/decoration/jungle_grass/b/style_random),
+ ),
+ "Bushes" = list(
+ list("name" = "Plastic Bush", "path" = /obj/structure/decoration/bush/first),
+ list("name" = "Plastic Bush (Alt)", "path" = /obj/structure/decoration/bush/second),
+ list("name" = "Plastic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/third),
+ list("name" = "Plastic Bush (Alt 3)", "path" = /obj/structure/decoration/bush/fourth),
+ list("name" = "Plastic Bush (Random)", "path" = /obj/structure/decoration/bush/style_random),
+ list("name" = "Plastic Reeds", "path" = /obj/structure/decoration/bush/reed/first),
+ list("name" = "Plastic Reeds (Alt)", "path" = /obj/structure/decoration/bush/reed/second),
+ list("name" = "Plastic Reeds (Alt 2)", "path" = /obj/structure/decoration/bush/reed/third),
+ list("name" = "Plastic Reeds (Alt 3)", "path" = /obj/structure/decoration/bush/reed/fourth),
+ list("name" = "Plastic Reeds (Random)", "path" = /obj/structure/decoration/bush/reed/style_random),
+ list("name" = "Plastic Leafy Bush", "path" = /obj/structure/decoration/bush/leafy/first),
+ list("name" = "Plastic Leafy Bush (Alt)", "path" = /obj/structure/decoration/bush/leafy/second),
+ list("name" = "Plastic Leafy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/leafy/third),
+ list("name" = "Plastic Leafy Bush (Random)", "path" = /obj/structure/decoration/bush/leafy/style_random),
+ list("name" = "Plastic Pale Bush", "path" = /obj/structure/decoration/bush/pale/first),
+ list("name" = "Plastic Pale Bush (Alt)", "path" = /obj/structure/decoration/bush/pale/second),
+ list("name" = "Plastic Pale Bush (Alt 2)", "path" = /obj/structure/decoration/bush/pale/third),
+ list("name" = "Plastic Pale Bush (Alt 3)", "path" = /obj/structure/decoration/bush/pale/fourth),
+ list("name" = "Plastic Pale Bush (Random)", "path" = /obj/structure/decoration/bush/pale/style_random),
+ list("name" = "Plastic Stalky Bush", "path" = /obj/structure/decoration/bush/stalky/first),
+ list("name" = "Plastic Stalky Bush (Alt)", "path" = /obj/structure/decoration/bush/stalky/second),
+ list("name" = "Plastic Stalky Bush (Alt 2)", "path" = /obj/structure/decoration/bush/stalky/third),
+ list("name" = "Plastic Stalky Bush (Random)", "path" = /obj/structure/decoration/bush/stalky/style_random),
+ list("name" = "Plastic Grassy Bush", "path" = /obj/structure/decoration/bush/grassy/first),
+ list("name" = "Plastic Grassy Bush (Alt)", "path" = /obj/structure/decoration/bush/grassy/second),
+ list("name" = "Plastic Grassy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/grassy/third),
+ list("name" = "Plastic Grassy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/grassy/fourth),
+ list("name" = "Plastic Grassy Bush (Random)", "path" = /obj/structure/decoration/bush/grassy/style_random),
+ list("name" = "Plastic Sparse Grass", "path" = /obj/structure/decoration/bush/sparsegrass/first),
+ list("name" = "Plastic Sparse Grass (Alt)", "path" = /obj/structure/decoration/bush/sparsegrass/second),
+ list("name" = "Plastic Sparse Grass (Alt 2)", "path" = /obj/structure/decoration/bush/sparsegrass/third),
+ list("name" = "Plastic Sparse Grass (Random)", "path" = /obj/structure/decoration/bush/sparsegrass/style_random),
+ list("name" = "Plastic Full Grass", "path" = /obj/structure/decoration/bush/fullgrass/first),
+ list("name" = "Plastic Full Grass (Alt)", "path" = /obj/structure/decoration/bush/fullgrass/second),
+ list("name" = "Plastic Full Grass (Alt 2)", "path" = /obj/structure/decoration/bush/fullgrass/third),
+ list("name" = "Plastic Full Grass (Random)", "path" = /obj/structure/decoration/bush/fullgrass/style_random),
+ list("name" = "Plastic Ferny Bush", "path" = /obj/structure/decoration/bush/ferny/first),
+ list("name" = "Plastic Ferny Bush (Alt)", "path" = /obj/structure/decoration/bush/ferny/second),
+ list("name" = "Plastic Ferny Bush (Alt 2)", "path" = /obj/structure/decoration/bush/ferny/third),
+ list("name" = "Plastic Ferny Bush (Random)", "path" = /obj/structure/decoration/bush/ferny/style_random),
+ list("name" = "Plastic Sunny Bush", "path" = /obj/structure/decoration/bush/sunny/first),
+ list("name" = "Plastic Sunny Bush (Alt)", "path" = /obj/structure/decoration/bush/sunny/second),
+ list("name" = "Plastic Sunny Bush (Alt 2)", "path" = /obj/structure/decoration/bush/sunny/third),
+ list("name" = "Plastic Sunny Bush (Random)", "path" = /obj/structure/decoration/bush/sunny/style_random),
+ list("name" = "Plastic Generic Bush", "path" = /obj/structure/decoration/bush/generic/first),
+ list("name" = "Plastic Generic Bush (Alt)", "path" = /obj/structure/decoration/bush/generic/second),
+ list("name" = "Plastic Generic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/generic/third),
+ list("name" = "Plastic Generic Bush (Alt 3)", "path" = /obj/structure/decoration/bush/generic/fourth),
+ list("name" = "Plastic Generic Bush (Random)", "path" = /obj/structure/decoration/bush/generic/style_random),
+ list("name" = "Plastic Pointy Bush", "path" = /obj/structure/decoration/bush/pointy/first),
+ list("name" = "Plastic Pointy Bush (Alt)", "path" = /obj/structure/decoration/bush/pointy/second),
+ list("name" = "Plastic Pointy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/pointy/third),
+ list("name" = "Plastic Pointy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/pointy/fourth),
+ list("name" = "Plastic Pointy Bush (Random)", "path" = /obj/structure/decoration/bush/pointy/style_random),
+ list("name" = "Plastic Lavender Grass", "path" = /obj/structure/decoration/bush/lavendergrass/first),
+ list("name" = "Plastic Lavender Grass (Alt)", "path" = /obj/structure/decoration/bush/lavendergrass/second),
+ list("name" = "Plastic Lavender Grass (Alt 2)", "path" = /obj/structure/decoration/bush/lavendergrass/third),
+ list("name" = "Plastic Lavender Grass (Alt 3)", "path" = /obj/structure/decoration/bush/lavendergrass/fourth),
+ list("name" = "Plastic Lavender Grass (Random)", "path" = /obj/structure/decoration/bush/lavendergrass/style_random),
+ ),
+ "Flowers" = list(
+ list("name" = "Plastic Yellow-White Flowers", "path" = /obj/structure/decoration/bush/flowers_yw/first),
+ list("name" = "Plastic Yellow-White Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_yw/second),
+ list("name" = "Plastic Yellow-White Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_yw/third),
+ list("name" = "Plastic Yellow-White Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_yw/style_random),
+ list("name" = "Plastic Blue-Red Flowers", "path" = /obj/structure/decoration/bush/flowers_br/first),
+ list("name" = "Plastic Blue-Red Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_br/second),
+ list("name" = "Plastic Blue-Red Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_br/third),
+ list("name" = "Plastic Blue-Red Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_br/style_random),
+ list("name" = "Plastic Purple Flowers", "path" = /obj/structure/decoration/bush/flowers_pp/first),
+ list("name" = "Plastic Purple Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_pp/second),
+ list("name" = "Plastic Purple Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_pp/third),
+ list("name" = "Plastic Purple Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_pp/style_random),
+ ),
+ "Snow" = list(
+ list("name" = "Plastic Snowy Bush", "path" = /obj/structure/decoration/bush/snow/first),
+ list("name" = "Plastic Snowy Bush (Alt)", "path" = /obj/structure/decoration/bush/snow/second),
+ list("name" = "Plastic Snowy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/snow/third),
+ list("name" = "Plastic Snowy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/snow/fourth),
+ list("name" = "Plastic Snowy Bush (Alt 4)", "path" = /obj/structure/decoration/bush/snow/fifth),
+ list("name" = "Plastic Snowy Bush (Alt 5)", "path" = /obj/structure/decoration/bush/snow/sixth),
+ list("name" = "Plastic Snowy Bush (Random)", "path" = /obj/structure/decoration/bush/snow/style_random),
+ ),
+ "Jungle" = list(
+ list("name" = "Plastic Jungle Bush", "path" = /obj/structure/decoration/bush/jungle/first),
+ list("name" = "Plastic Jungle Bush (Alt)", "path" = /obj/structure/decoration/bush/jungle/second),
+ list("name" = "Plastic Jungle Bush (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/third),
+ list("name" = "Plastic Jungle Bush (Random)", "path" = /obj/structure/decoration/bush/jungle/style_random),
+ list("name" = "Plastic Jungle Bush B", "path" = /obj/structure/decoration/bush/jungle/b/first),
+ list("name" = "Plastic Jungle Bush B (Alt)", "path" = /obj/structure/decoration/bush/jungle/b/second),
+ list("name" = "Plastic Jungle Bush B (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/b/third),
+ list("name" = "Plastic Jungle Bush B (Random)", "path" = /obj/structure/decoration/bush/jungle/b/style_random),
+ list("name" = "Plastic Jungle Bush C", "path" = /obj/structure/decoration/bush/jungle/c/first),
+ list("name" = "Plastic Jungle Bush C (Alt)", "path" = /obj/structure/decoration/bush/jungle/c/second),
+ list("name" = "Plastic Jungle Bush C (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/c/third),
+ list("name" = "Plastic Jungle Bush C (Random)", "path" = /obj/structure/decoration/bush/jungle/c/style_random),
+ list("name" = "Large Plastic Bush", "path" = /obj/structure/decoration/bush/large/first),
+ list("name" = "Large Plastic Bush (Alt)", "path" = /obj/structure/decoration/bush/large/second),
+ list("name" = "Large Plastic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/large/third),
+ list("name" = "Large Plastic Bush (Random)", "path" = /obj/structure/decoration/bush/large/style_random),
+ ),
+ "Rocks" = list(
+ list("name" = "Plastic Rock", "path" = /obj/structure/decoration/rock/first),
+ list("name" = "Plastic Rock (Alt)", "path" = /obj/structure/decoration/rock/second),
+ list("name" = "Plastic Rock (Alt 2)", "path" = /obj/structure/decoration/rock/third),
+ list("name" = "Plastic Rock (Alt 3)", "path" = /obj/structure/decoration/rock/fourth),
+ list("name" = "Plastic Rock (Random)", "path" = /obj/structure/decoration/rock/style_random),
+ list("name" = "Plastic Rock Pile", "path" = /obj/structure/decoration/rock/pile/first),
+ list("name" = "Plastic Rock Pile (Alt)", "path" = /obj/structure/decoration/rock/pile/second),
+ list("name" = "Plastic Rock Pile (Alt 2)", "path" = /obj/structure/decoration/rock/pile/third),
+ list("name" = "Plastic Rock Pile (Random)", "path" = /obj/structure/decoration/rock/pile/style_random),
+ list("name" = "Plastic Jungle Rocks", "path" = /obj/structure/decoration/rock/pile/jungle/first),
+ list("name" = "Plastic Jungle Rocks (Alt)", "path" = /obj/structure/decoration/rock/pile/jungle/second),
+ list("name" = "Plastic Jungle Rocks (Alt 2)", "path" = /obj/structure/decoration/rock/pile/jungle/third),
+ list("name" = "Plastic Jungle Rocks (Alt 3)", "path" = /obj/structure/decoration/rock/pile/jungle/fourth),
+ list("name" = "Plastic Jungle Rocks (Alt 4)", "path" = /obj/structure/decoration/rock/pile/jungle/fifth),
+ list("name" = "Plastic Jungle Rocks (Random)", "path" = /obj/structure/decoration/rock/pile/jungle/style_random),
+ list("name" = "Large Plastic Rocks", "path" = /obj/structure/decoration/rock/pile/jungle/large/first),
+ list("name" = "Large Plastic Rocks (Alt)", "path" = /obj/structure/decoration/rock/pile/jungle/large/second),
+ list("name" = "Large Plastic Rocks (Alt 2)", "path" = /obj/structure/decoration/rock/pile/jungle/large/third),
+ list("name" = "Large Plastic Rocks (Random)", "path" = /obj/structure/decoration/rock/pile/jungle/large/style_random),
+ ),
+))
+
+/obj/item/construction/rdd
+ name = "rapid-decoration-device (RDD)"
+ desc = "A device used to rapidly deploy plastic decorative flora. \
+ Internally synthesizes cheap plastic replicas of natural scenery."
+ icon = 'icons/obj/tools.dmi'
+ icon_state = "rdd"
+ worn_icon_state = "RCD"
+ lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
+ custom_premium_price = PAYCHECK_COMMAND * 1
+ max_matter = 200
+ slot_flags = ITEM_SLOT_BELT
+ item_flags = NO_MAT_REDEMPTION | NOBLUDGEON
+ has_ammobar = TRUE
+ banned_upgrades = RCD_ALL_UPGRADES & ~RCD_UPGRADE_SILO_LINK
+ charge_icon_state = "rtd"
+ drop_sound = 'sound/items/handling/tools/rcd_drop.ogg'
+ pickup_sound = 'sound/items/handling/tools/rcd_pickup.ogg'
+ sound_vary = TRUE
+
+ /// Currently selected decoration path
+ var/obj/structure/decoration/selected_decoration
+ /// Currently selected category name (for UI)
+ var/selected_category
+ /// Currently selected design name (for UI)
+ var/selected_design_name
+
+/obj/item/construction/rdd/Initialize(mapload)
+ . = ..()
+ selected_category = GLOB.rdd_designs[1]
+ var/list/category_designs = GLOB.rdd_designs[selected_category]
+ if(length(category_designs))
+ var/list/first_design = category_designs[1]
+ selected_decoration = first_design["path"]
+ selected_design_name = first_design["name"]
+
+/obj/item/construction/rdd/examine(mob/user)
+ . = ..()
+ . += span_info("Currently set to produce: [span_bold(initial(selected_decoration.name))].")
+
+/obj/item/construction/rdd/attack_self(mob/user)
+ . = ..()
+ ui_interact(user)
+
+/obj/item/construction/rdd/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "RapidDecorationDevice", name)
+ ui.open()
+
+/obj/item/construction/rdd/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet_batched/rdd),
+ )
+
+/obj/item/construction/rdd/ui_static_data(mob/user)
+ var/list/data = ..()
+
+ data["categories"] = list()
+ for(var/category in GLOB.rdd_designs)
+ var/list/cat_entry = list("cat_name" = category, "designs" = list())
+ for(var/list/design in GLOB.rdd_designs[category])
+ cat_entry["designs"] += list(list(
+ "name" = design["name"],
+ "icon" = sanitize_css_class_name(design["name"]),
+ ))
+ data["categories"] += list(cat_entry)
+
+ return data
+
+/obj/item/construction/rdd/ui_data(mob/user)
+ var/list/data = ..()
+
+ var/total_matter = get_matter(user)
+ data["matter"] = isnum(total_matter) ? total_matter : 0
+ data["max_matter"] = max_matter
+ data["selected_category"] = selected_category
+ data["selected_design"] = selected_design_name
+
+ return data
+
+/obj/item/construction/rdd/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("design")
+ var/category = params["category"]
+ var/design_name = params["name"]
+ for(var/list/design as anything in GLOB.rdd_designs[category])
+ if(design["name"] == design_name)
+ selected_decoration = design["path"]
+ selected_category = category
+ selected_design_name = design_name
+ playsound(src, SFX_TOOL_SWITCH, 20, TRUE)
+ return TRUE
+
+ return TRUE
+
+/obj/item/construction/rdd/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ . = ..()
+ if(. & ITEM_INTERACT_ANY_BLOCKER)
+ return .
+
+ var/turf/target_turf = get_turf(interacting_with)
+ if(!target_turf)
+ return ITEM_INTERACT_BLOCKING
+
+ if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ balloon_alert(user, "tile is blocked!")
+ return ITEM_INTERACT_BLOCKING
+
+ var/decoration_count = 0
+ for(var/obj/structure/decoration/existing in target_turf.contents)
+ decoration_count++
+ if(decoration_count >= 3)
+ balloon_alert(user, "too many decorations here!")
+ return ITEM_INTERACT_BLOCKING
+
+ var/cost = RDD_COST_MULTIPLIER
+ if(!useResource(cost, user, TRUE))
+ return ITEM_INTERACT_BLOCKING
+
+ playsound(loc, 'sound/machines/click.ogg', 50, TRUE)
+ if(!do_after(user, 0.5 SECONDS, target_turf))
+ return ITEM_INTERACT_BLOCKING
+ if(!useResource(cost, user, TRUE))
+ return ITEM_INTERACT_BLOCKING
+
+ playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
+ new selected_decoration(target_turf)
+ useResource(cost, user)
+
+ log_tool("[key_name(user)] used [src] to create [initial(selected_decoration.name)] at [AREACOORD(target_turf)]")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/construction/rdd/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers)
+ var/turf/target_turf = get_turf(interacting_with)
+ if(!target_turf)
+ return NONE
+
+ var/obj/structure/decoration/found = locate() in target_turf
+
+ if(!found)
+ return NONE
+
+ playsound(target_turf, 'sound/machines/click.ogg', 50, TRUE)
+ if(!do_after(user, 0.5 SECONDS, target_turf))
+ return ITEM_INTERACT_BLOCKING
+
+ playsound(target_turf, 'sound/items/deconstruct.ogg', 50, TRUE)
+ qdel(found)
+
+ log_tool("[key_name(user)] used [src] to deconstruct [found] at [AREACOORD(target_turf)]")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/construction/rdd/borg
+ desc = "A device used to rapidly deploy plastic decorative flora. Uses the cyborg's internal cell."
+ /// energy usage per decoration
+ var/energyfactor = 0.05 * STANDARD_CELL_CHARGE
+
+/obj/item/construction/rdd/borg/get_matter(mob/user)
+ if(!iscyborg(user))
+ return 0
+ var/mob/living/silicon/robot/borgy = user
+ if(!borgy.cell)
+ return 0
+ max_matter = borgy.cell.maxcharge
+ return borgy.cell.charge
+
+/obj/item/construction/rdd/borg/useResource(amount, mob/user, dry_run)
+ var/mob/living/silicon/robot/borgy = user
+ if(!iscyborg(borgy))
+ return FALSE
+ if(!borgy.cell)
+ balloon_alert(user, "no cell found!")
+ return FALSE
+ if(borgy.cell.charge < amount * energyfactor)
+ balloon_alert(user, "insufficient charge!")
+ return FALSE
+ if(!dry_run)
+ playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
+ return borgy.cell.use(amount * energyfactor)
+ return TRUE
+
+/obj/item/construction/rdd/loaded
+ matter = 200
+
+#undef RDD_COST_MULTIPLIER
diff --git a/code/game/objects/items/rcd/RHD.dm b/code/game/objects/items/rcd/RHD.dm
index 40f989ee0882..93663639a9cc 100644
--- a/code/game/objects/items/rcd/RHD.dm
+++ b/code/game/objects/items/rcd/RHD.dm
@@ -30,6 +30,8 @@
var/has_ammobar = FALSE
/// amount of divisions in the ammo indicator overlay/number of ammo indicator states
var/ammo_sections = 10
+ /// icon_state prefix used for charge overlays — defaults to icon_state if not set
+ var/charge_icon_state
/// bitflags for upgrades
var/construction_upgrades = NONE
/// bitflags for banned upgrades
@@ -179,7 +181,7 @@
if(has_ammobar)
var/ratio = ceil((matter / max_matter) * ammo_sections)
if(ratio > 0)
- . += "[icon_state]_charge[ratio]"
+ . += "[charge_icon_state || icon_state]_charge[ratio]"
/**
* Uses resource to do some action. Returns amount of resource used or TRUE/FALSE if only an dry run is required
@@ -193,7 +195,7 @@
if(!silo_mats || !silo_link)
if(matter < amount)
if(has_ammobar)
- flick("[icon_state]_empty", src)
+ flick("[charge_icon_state || icon_state]_empty", src)
if(user)
balloon_alert(user, "not enough matter!")
return FALSE
diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm
index 0e96116ed3d1..a087dfeb897e 100644
--- a/code/game/objects/items/religion.dm
+++ b/code/game/objects/items/religion.dm
@@ -337,6 +337,10 @@
var/staffcooldown = 0
var/staffwait = 30
+/obj/item/godstaff/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
/obj/item/godstaff/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(SHOULD_SKIP_INTERACTION(interacting_with, src, user))
return NONE
diff --git a/code/game/objects/items/storage/boxes/food_boxes.dm b/code/game/objects/items/storage/boxes/food_boxes.dm
index fa4ecbda4fb4..738cb6e51207 100644
--- a/code/game/objects/items/storage/boxes/food_boxes.dm
+++ b/code/game/objects/items/storage/boxes/food_boxes.dm
@@ -593,3 +593,15 @@
name = "robusta beans"
desc = "A bag containing fresh, dry coffee robusta beans. Ethically sourced and packaged by Waffle Corp."
beantype = /obj/item/food/grown/coffee/robusta
+
+/obj/item/storage/box/ramen_beef
+ name = "beef space ramen"
+ desc = "A box containing a brick of dehydrated ramen and a beef flavour sachet."
+ icon_state = "ramen_box"
+ illustration = null
+ storage_type = /datum/storage/box/ramen_beef
+ w_class = WEIGHT_CLASS_SMALL //it's meant to come in packs of five and the box can only hold two items, beef flavour or dry ramen
+
+/obj/item/storage/box/ramen_beef/PopulateContents()
+ new /obj/item/reagent_containers/condiment/pack/beef_flavour(src)
+ new /obj/item/food/spaghetti/ramen_dry(src)
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index aff0ccc4e800..cd4baf7f1c99 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -764,6 +764,8 @@
new /obj/item/book/manual/nuclear(src) // Very important
// The most important part of the kit, the implant that gives them the syndicate faction.
new /obj/item/implanter/induction_implant(src)
+ // Tactical map implant so they can see the minimap with the rest of the team.
+ new /obj/item/implanter/tacmap/nuclear(src)
// All in all, 6+3+3+2+5+2+4 = ~25 TC of 'miscellaneous' items.
// This is a lot of value for 10 TC, but you have to keep in mind that you NEED someone to get this stuff station-side.
// Pretty much all of it is a bad deal for reinforcements or yourself as they already have similar or good-enough alternatives.
diff --git a/code/game/objects/items/tools/janitorial/broom.dm b/code/game/objects/items/tools/janitorial/broom.dm
index 970bb5a55261..e34e0044b08f 100644
--- a/code/game/objects/items/tools/janitorial/broom.dm
+++ b/code/game/objects/items/tools/janitorial/broom.dm
@@ -29,6 +29,7 @@
wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
)
+ AddComponent(/datum/component/walking_aid)
/obj/item/pushbroom/update_icon_state()
icon_state = "[base_icon_state]0"
diff --git a/code/game/objects/items/tools/medical/cane.dm b/code/game/objects/items/tools/medical/cane.dm
index 132f03a9b378..ca9de8325b7e 100644
--- a/code/game/objects/items/tools/medical/cane.dm
+++ b/code/game/objects/items/tools/medical/cane.dm
@@ -13,32 +13,18 @@
custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT * 0.5)
attack_verb_continuous = list("bludgeons", "whacks", "disciplines", "thrashes")
attack_verb_simple = list("bludgeon", "whack", "discipline", "thrash")
+ /// The amount of slowdown to reduce for a limbless leg
+ var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40%
+ /// Does this cause waddling when held
+ var/causes_waddling = FALSE
-/obj/item/cane/examine(mob/user, thats)
+/obj/item/cane/Initialize(mapload)
. = ..()
- . += span_notice("This item can be used to support your weight, preventing limping from any broken bones on your legs you may have.")
+ AddComponent(/datum/component/walking_aid, limbless_slowdown_modifier, get_walking_aid_required_trait(), causes_waddling)
-/obj/item/cane/equipped(mob/living/user, slot, initial)
- ..()
- if(!(slot & ITEM_SLOT_HANDS))
- return
- movement_support_add(user)
-
-/obj/item/cane/dropped(mob/living/user, silent = FALSE)
- . = ..()
- movement_support_del(user)
-
-/obj/item/cane/proc/movement_support_add(mob/living/user)
- RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping))
- return TRUE
-
-/obj/item/cane/proc/movement_support_del(mob/living/user)
- UnregisterSignal(user, list(COMSIG_CARBON_LIMPING))
- return TRUE
-
-/obj/item/cane/proc/handle_limping(mob/living/user)
- SIGNAL_HANDLER
- return COMPONENT_CANCEL_LIMP
+/// Determines if a trait is required to be used as a walking aid (ex. foldable canes)
+/obj/item/cane/proc/get_walking_aid_required_trait()
+ return null
/obj/item/cane/crutch
name = "medical crutch"
@@ -55,42 +41,13 @@
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 0.5)
attack_verb_continuous = list("bludgeons", "whacks", "thrashes")
attack_verb_simple = list("bludgeon", "whack", "thrash")
+ limbless_slowdown_modifier = 0.4 // reduces slowdown by 60%
+ causes_waddling = TRUE
/obj/item/cane/crutch/Initialize(mapload)
. = ..()
AddElement(/datum/element/cuffable_item)
-/obj/item/cane/crutch/examine(mob/user, thats)
- . = ..()
- // tacked on after the cane string
- . += span_notice("As a crutch, it can also help lessen the slowdown incurred by missing a leg.")
-
-/obj/item/cane/crutch/movement_support_add(mob/living/user)
- . = ..()
- if(!.)
- return
- RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown))
- user.update_usable_leg_status()
- user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
-
-/obj/item/cane/crutch/movement_support_del(mob/living/user)
- . = ..()
- if(!.)
- return
- UnregisterSignal(user, list(COMSIG_LIVING_LIMBLESS_SLOWDOWN, COMSIG_CARBON_LIMPING))
- user.update_usable_leg_status()
- REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src))
-
-/obj/item/cane/crutch/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods)
- SIGNAL_HANDLER
- var/leg_amount = user.usable_legs
- // Don't do anything if the number is equal (or higher) to the usual.
- if(leg_amount >= user.default_num_legs)
- return
- // If we have at least one leg and it's less than the default, reduce slowdown by 60%.
- if(leg_amount && (leg_amount < user.default_num_legs))
- slowdown_mods += 0.4
-
/obj/item/cane/crutch/wood
name = "wooden crutch"
desc = "A handmade crutch. Also makes a decent bludgeon if you need it."
@@ -124,15 +81,16 @@
)
RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform))
-/obj/item/cane/white/handle_limping(mob/living/user)
- return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? COMPONENT_CANCEL_LIMP : NONE
+// White canes only provide support while extended
+/obj/item/cane/white/get_walking_aid_required_trait()
+ return TRAIT_TRANSFORM_ACTIVE
/*
* Signal proc for [COMSIG_TRANSFORMING_ON_TRANSFORM].
*
* Gives feedback to the user and makes it show up inhand.
*/
-/obj/item/cane/white/proc/on_transform(obj/item/source, mob/user, active)
+/obj/item/cane/white/proc/on_transform(obj/item/source, mob/living/user, active)
SIGNAL_HANDLER
if(user)
diff --git a/code/game/objects/items/weaponry/melee/baseball_bat.dm b/code/game/objects/items/weaponry/melee/baseball_bat.dm
index 8d12e7724362..5bc6ca014be4 100644
--- a/code/game/objects/items/weaponry/melee/baseball_bat.dm
+++ b/code/game/objects/items/weaponry/melee/baseball_bat.dm
@@ -3,19 +3,21 @@
desc = "There ain't a skull in the league that can withstand a swatter."
icon = 'icons/obj/weapons/bat.dmi'
icon_state = "baseball_bat"
+ worn_icon_state="bostaff0"
inhand_icon_state = "baseball_bat"
icon_angle = -45
lefthand_file = 'icons/mob/inhands/weapons/melee_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/melee_righthand.dmi'
- force = 12
+ force = 13
wound_bonus = -10
- throwforce = 12
+ throwforce = 13
demolition_mod = 1.25
attack_verb_continuous = list("beats", "smacks")
attack_verb_simple = list("beat", "smack")
custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 3.5)
resistance_flags = FLAMMABLE
w_class = WEIGHT_CLASS_HUGE
+ slot_flags = ITEM_SLOT_BACK
/// Are we able to do a homerun?
var/homerun_able = FALSE
/// Are we ready to do a homerun?
diff --git a/code/game/objects/items/weaponry/melee/misc.dm b/code/game/objects/items/weaponry/melee/misc.dm
index b79ff035f20d..35f3151987ed 100644
--- a/code/game/objects/items/weaponry/melee/misc.dm
+++ b/code/game/objects/items/weaponry/melee/misc.dm
@@ -386,6 +386,7 @@
force_unwielded = 10, \
force_wielded = 14, \
)
+ AddComponent(/datum/component/walking_aid)
/obj/item/bambostaff/update_icon_state()
icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]"
@@ -414,6 +415,10 @@
attack_verb_simple = list("bludgeon", "whack", "discipline")
resistance_flags = FLAMMABLE
+/obj/item/staff/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
/obj/item/staff/broom
name = "broom"
desc = "Used for sweeping, and flying into the night while cackling. Black cat not included."
diff --git a/code/game/objects/items/weaponry/melee/soulscythe.dm b/code/game/objects/items/weaponry/melee/soulscythe.dm
index 596166d14e02..c7dabc9d44df 100644
--- a/code/game/objects/items/weaponry/melee/soulscythe.dm
+++ b/code/game/objects/items/weaponry/melee/soulscythe.dm
@@ -39,6 +39,7 @@
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED_SECONDARY, PROC_REF(on_secondary_attack))
RegisterSignal(src, COMSIG_ATOM_INTEGRITY_CHANGED, PROC_REF(on_integrity_change))
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
+ AddComponent(/datum/component/walking_aid)
/obj/item/soulscythe/examine(mob/user)
. = ..()
diff --git a/code/game/objects/items/weaponry/melee/spear.dm b/code/game/objects/items/weaponry/melee/spear.dm
index 39ca04c50173..6fc5634c7c0a 100644
--- a/code/game/objects/items/weaponry/melee/spear.dm
+++ b/code/game/objects/items/weaponry/melee/spear.dm
@@ -70,6 +70,7 @@
wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
)
+ AddComponent(/datum/component/walking_aid)
add_headpike_component()
update_appearance()
diff --git a/code/game/objects/structures/decorations.dm b/code/game/objects/structures/decorations.dm
new file mode 100644
index 000000000000..fb05b6478e2a
--- /dev/null
+++ b/code/game/objects/structures/decorations.dm
@@ -0,0 +1,609 @@
+/obj/structure/decoration
+ name = "plastic decoration"
+ desc = "A cheap plastic imitation of nature. At least it doesn't need watering."
+ icon = 'icons/obj/fluff/flora/ausflora.dmi'
+ resistance_flags = FLAMMABLE
+ max_integrity = 20
+ anchored = TRUE
+ alpha = 210
+ /// No material refund on deconstruction, it's cheap plastic
+ custom_materials = null
+
+/obj/structure/decoration/Initialize(mapload)
+ . = ..()
+
+/obj/structure/decoration/atom_deconstruct(disassembled = TRUE)
+ if(!disassembled)
+ new /obj/effect/decal/cleanable/plastic(loc)
+ return
+
+/obj/structure/decoration/examine(mob/user)
+ . = ..()
+ . += span_notice("It's made of cheap, hollow plastic.")
+
+/obj/structure/decoration/grass
+ name = "plastic grass patch"
+ desc = "Fake grass. Feels like a brillo pad."
+ icon = /obj/structure/flora/grass/green::icon
+ icon_state = /obj/structure/flora/grass/green::icon_state
+
+/obj/structure/decoration/grass/first
+ // inherits from parent
+
+/obj/structure/decoration/grass/second
+ icon_state = /obj/structure/flora/grass/green/style_2::icon_state
+
+/obj/structure/decoration/grass/third
+ icon_state = /obj/structure/flora/grass/green/style_3::icon_state
+
+/obj/structure/decoration/grass/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "snowgrass[rand(1, 3)]gb"
+ update_appearance()
+
+/obj/structure/decoration/grass/brown
+ icon = /obj/structure/flora/grass/brown::icon
+ icon_state = /obj/structure/flora/grass/brown::icon_state
+
+/obj/structure/decoration/grass/brown/first
+
+/obj/structure/decoration/grass/brown/second
+ icon_state = /obj/structure/flora/grass/brown/style_2::icon_state
+
+/obj/structure/decoration/grass/brown/third
+ icon_state = /obj/structure/flora/grass/brown/style_3::icon_state
+
+/obj/structure/decoration/grass/brown/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "snowgrass[rand(1, 3)]bb"
+ update_appearance()
+
+/obj/structure/decoration/jungle_grass
+ name = "plastic jungle grass"
+ desc = "Plastic alien-looking grass. The jungle vibe without the jungle bugs."
+ icon = /obj/structure/flora/grass/jungle::icon
+ icon_state = /obj/structure/flora/grass/jungle::icon_state
+
+/obj/structure/decoration/jungle_grass/first
+
+/obj/structure/decoration/jungle_grass/second
+ icon_state = /obj/structure/flora/grass/jungle/a/style_2::icon_state
+
+/obj/structure/decoration/jungle_grass/third
+ icon_state = /obj/structure/flora/grass/jungle/a/style_3::icon_state
+
+/obj/structure/decoration/jungle_grass/fourth
+ icon_state = /obj/structure/flora/grass/jungle/a/style_4::icon_state
+
+/obj/structure/decoration/jungle_grass/fifth
+ icon_state = /obj/structure/flora/grass/jungle/a/style_5::icon_state
+
+/obj/structure/decoration/jungle_grass/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "grassa[rand(1, 5)]"
+ update_appearance()
+
+/obj/structure/decoration/jungle_grass/b
+ icon = /obj/structure/flora/grass/jungle/b::icon
+ icon_state = /obj/structure/flora/grass/jungle/b::icon_state
+
+/obj/structure/decoration/jungle_grass/b/first
+
+/obj/structure/decoration/jungle_grass/b/second
+ icon_state = /obj/structure/flora/grass/jungle/b/style_2::icon_state
+
+/obj/structure/decoration/jungle_grass/b/third
+ icon_state = /obj/structure/flora/grass/jungle/b/style_3::icon_state
+
+/obj/structure/decoration/jungle_grass/b/fourth
+ icon_state = /obj/structure/flora/grass/jungle/b/style_4::icon_state
+
+/obj/structure/decoration/jungle_grass/b/fifth
+ icon_state = /obj/structure/flora/grass/jungle/b/style_5::icon_state
+
+/obj/structure/decoration/jungle_grass/b/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "grassb[rand(1, 5)]"
+ update_appearance()
+
+/obj/structure/decoration/bush
+ name = "plastic bush"
+ desc = "A plastic shrub. Bristly to the touch and slightly off-color."
+ icon = /obj/structure/flora/bush::icon
+ icon_state = /obj/structure/flora/bush::icon_state
+
+/obj/structure/decoration/bush/first
+
+/obj/structure/decoration/bush/second
+ icon_state = /obj/structure/flora/bush/style_2::icon_state
+
+/obj/structure/decoration/bush/third
+ icon_state = /obj/structure/flora/bush/style_3::icon_state
+
+/obj/structure/decoration/bush/fourth
+ icon_state = /obj/structure/flora/bush/style_4::icon_state
+
+/obj/structure/decoration/bush/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "firstbush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/reed
+ name = "plastic reeds"
+ icon = /obj/structure/flora/bush/reed::icon
+ icon_state = /obj/structure/flora/bush/reed::icon_state
+
+/obj/structure/decoration/bush/reed/first
+
+/obj/structure/decoration/bush/reed/second
+ icon_state = /obj/structure/flora/bush/reed/style_2::icon_state
+
+/obj/structure/decoration/bush/reed/third
+ icon_state = /obj/structure/flora/bush/reed/style_3::icon_state
+
+/obj/structure/decoration/bush/reed/fourth
+ icon_state = /obj/structure/flora/bush/reed/style_4::icon_state
+
+/obj/structure/decoration/bush/reed/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "reedbush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/leafy
+ name = "plastic leafy bush"
+ icon = /obj/structure/flora/bush/leafy::icon
+ icon_state = /obj/structure/flora/bush/leafy::icon_state
+
+/obj/structure/decoration/bush/leafy/first
+
+/obj/structure/decoration/bush/leafy/second
+ icon_state = /obj/structure/flora/bush/leavy/style_2::icon_state
+
+/obj/structure/decoration/bush/leafy/third
+ icon_state = /obj/structure/flora/bush/leavy/style_3::icon_state
+
+/obj/structure/decoration/bush/leafy/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "leafybush_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/pale
+ name = "plastic pale bush"
+ icon = /obj/structure/flora/bush/pale::icon
+ icon_state = /obj/structure/flora/bush/pale::icon_state
+
+/obj/structure/decoration/bush/pale/first
+
+/obj/structure/decoration/bush/pale/second
+ icon_state = /obj/structure/flora/bush/pale/style_2::icon_state
+
+/obj/structure/decoration/bush/pale/third
+ icon_state = /obj/structure/flora/bush/pale/style_3::icon_state
+
+/obj/structure/decoration/bush/pale/fourth
+ icon_state = /obj/structure/flora/bush/pale/style_4::icon_state
+
+/obj/structure/decoration/bush/pale/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "palebush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/stalky
+ name = "plastic stalky bush"
+ icon = /obj/structure/flora/bush/stalky::icon
+ icon_state = /obj/structure/flora/bush/stalky::icon_state
+
+/obj/structure/decoration/bush/stalky/first
+
+/obj/structure/decoration/bush/stalky/second
+ icon_state = /obj/structure/flora/bush/stalky/style_2::icon_state
+
+/obj/structure/decoration/bush/stalky/third
+ icon_state = /obj/structure/flora/bush/stalky/style_3::icon_state
+
+/obj/structure/decoration/bush/stalky/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "stalkybush_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/grassy
+ name = "plastic grassy bush"
+ icon = /obj/structure/flora/bush/grassy::icon
+ icon_state = /obj/structure/flora/bush/grassy::icon_state
+
+/obj/structure/decoration/bush/grassy/first
+
+/obj/structure/decoration/bush/grassy/second
+ icon_state = /obj/structure/flora/bush/grassy/style_2::icon_state
+
+/obj/structure/decoration/bush/grassy/third
+ icon_state = /obj/structure/flora/bush/grassy/style_3::icon_state
+
+/obj/structure/decoration/bush/grassy/fourth
+ icon_state = /obj/structure/flora/bush/grassy/style_4::icon_state
+
+/obj/structure/decoration/bush/grassy/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "grassybush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/sparsegrass
+ name = "plastic sparse grass"
+ icon = /obj/structure/flora/bush/sparsegrass::icon
+ icon_state = /obj/structure/flora/bush/sparsegrass::icon_state
+
+/obj/structure/decoration/bush/sparsegrass/first
+
+/obj/structure/decoration/bush/sparsegrass/second
+ icon_state = /obj/structure/flora/bush/sparsegrass/style_2::icon_state
+
+/obj/structure/decoration/bush/sparsegrass/third
+ icon_state = /obj/structure/flora/bush/sparsegrass/style_3::icon_state
+
+/obj/structure/decoration/bush/sparsegrass/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "sparsegrass_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/fullgrass
+ name = "plastic full grass"
+ icon = /obj/structure/flora/bush/fullgrass::icon
+ icon_state = /obj/structure/flora/bush/fullgrass::icon_state
+
+/obj/structure/decoration/bush/fullgrass/first
+
+/obj/structure/decoration/bush/fullgrass/second
+ icon_state = /obj/structure/flora/bush/fullgrass/style_2::icon_state
+
+/obj/structure/decoration/bush/fullgrass/third
+ icon_state = /obj/structure/flora/bush/fullgrass/style_3::icon_state
+
+/obj/structure/decoration/bush/fullgrass/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "fullgrass_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/ferny
+ name = "plastic ferny bush"
+ icon = /obj/structure/flora/bush/ferny::icon
+ icon_state = /obj/structure/flora/bush/ferny::icon_state
+
+/obj/structure/decoration/bush/ferny/first
+
+/obj/structure/decoration/bush/ferny/second
+ icon_state = /obj/structure/flora/bush/ferny/style_2::icon_state
+
+/obj/structure/decoration/bush/ferny/third
+ icon_state = /obj/structure/flora/bush/ferny/style_3::icon_state
+
+/obj/structure/decoration/bush/ferny/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "fernybush_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/sunny
+ name = "plastic sunny bush"
+ icon = /obj/structure/flora/bush/sunny::icon
+ icon_state = /obj/structure/flora/bush/sunny::icon_state
+
+/obj/structure/decoration/bush/sunny/first
+
+/obj/structure/decoration/bush/sunny/second
+ icon_state = /obj/structure/flora/bush/sunny/style_2::icon_state
+
+/obj/structure/decoration/bush/sunny/third
+ icon_state = /obj/structure/flora/bush/sunny/style_3::icon_state
+
+/obj/structure/decoration/bush/sunny/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "sunnybush_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/generic
+ name = "plastic generic bush"
+ icon = /obj/structure/flora/bush/generic::icon
+ icon_state = /obj/structure/flora/bush/generic::icon_state
+
+/obj/structure/decoration/bush/generic/first
+
+/obj/structure/decoration/bush/generic/second
+ icon_state = /obj/structure/flora/bush/generic/style_2::icon_state
+
+/obj/structure/decoration/bush/generic/third
+ icon_state = /obj/structure/flora/bush/generic/style_3::icon_state
+
+/obj/structure/decoration/bush/generic/fourth
+ icon_state = /obj/structure/flora/bush/generic/style_4::icon_state
+
+/obj/structure/decoration/bush/generic/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "genericbush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/pointy
+ name = "plastic pointy bush"
+ icon = /obj/structure/flora/bush/pointy::icon
+ icon_state = /obj/structure/flora/bush/pointy::icon_state
+
+/obj/structure/decoration/bush/pointy/first
+
+/obj/structure/decoration/bush/pointy/second
+ icon_state = /obj/structure/flora/bush/pointy/style_2::icon_state
+
+/obj/structure/decoration/bush/pointy/third
+ icon_state = /obj/structure/flora/bush/pointy/style_3::icon_state
+
+/obj/structure/decoration/bush/pointy/fourth
+ icon_state = /obj/structure/flora/bush/pointy/style_4::icon_state
+
+/obj/structure/decoration/bush/pointy/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "pointybush_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/lavendergrass
+ name = "plastic lavender grass"
+ icon = /obj/structure/flora/bush/lavendergrass::icon
+ icon_state = /obj/structure/flora/bush/lavendergrass::icon_state
+
+/obj/structure/decoration/bush/lavendergrass/first
+
+/obj/structure/decoration/bush/lavendergrass/second
+ icon_state = /obj/structure/flora/bush/lavendergrass/style_2::icon_state
+
+/obj/structure/decoration/bush/lavendergrass/third
+ icon_state = /obj/structure/flora/bush/lavendergrass/style_3::icon_state
+
+/obj/structure/decoration/bush/lavendergrass/fourth
+ icon_state = /obj/structure/flora/bush/lavendergrass/style_4::icon_state
+
+/obj/structure/decoration/bush/lavendergrass/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "lavendergrass_[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/flowers_yw
+ name = "plastic yellow-white flowers"
+ icon = /obj/structure/flora/bush/flowers_yw::icon
+ icon_state = /obj/structure/flora/bush/flowers_yw::icon_state
+
+/obj/structure/decoration/bush/flowers_yw/first
+
+/obj/structure/decoration/bush/flowers_yw/second
+ icon_state = /obj/structure/flora/bush/flowers_yw/style_2::icon_state
+
+/obj/structure/decoration/bush/flowers_yw/third
+ icon_state = /obj/structure/flora/bush/flowers_yw/style_3::icon_state
+
+/obj/structure/decoration/bush/flowers_yw/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "ywflowers_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/flowers_br
+ name = "plastic blue-red flowers"
+ icon = /obj/structure/flora/bush/flowers_br::icon
+ icon_state = /obj/structure/flora/bush/flowers_br::icon_state
+
+/obj/structure/decoration/bush/flowers_br/first
+
+/obj/structure/decoration/bush/flowers_br/second
+ icon_state = /obj/structure/flora/bush/flowers_br/style_2::icon_state
+
+/obj/structure/decoration/bush/flowers_br/third
+ icon_state = /obj/structure/flora/bush/flowers_br/style_3::icon_state
+
+/obj/structure/decoration/bush/flowers_br/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "brflowers_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/flowers_pp
+ name = "plastic purple flowers"
+ icon = /obj/structure/flora/bush/flowers_pp::icon
+ icon_state = /obj/structure/flora/bush/flowers_pp::icon_state
+
+/obj/structure/decoration/bush/flowers_pp/first
+
+/obj/structure/decoration/bush/flowers_pp/second
+ icon_state = /obj/structure/flora/bush/flowers_pp/style_2::icon_state
+
+/obj/structure/decoration/bush/flowers_pp/third
+ icon_state = /obj/structure/flora/bush/flowers_pp/style_3::icon_state
+
+/obj/structure/decoration/bush/flowers_pp/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "ppflowers_[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/snow
+ name = "plastic snowy bush"
+ desc = "A plastic bush dusted with fake snow. Year-round winter cheer."
+ icon = /obj/structure/flora/bush/snow::icon
+ icon_state = /obj/structure/flora/bush/snow::icon_state
+
+/obj/structure/decoration/bush/snow/first
+
+/obj/structure/decoration/bush/snow/second
+ icon_state = /obj/structure/flora/bush/snow/style_2::icon_state
+
+/obj/structure/decoration/bush/snow/third
+ icon_state = /obj/structure/flora/bush/snow/style_3::icon_state
+
+/obj/structure/decoration/bush/snow/fourth
+ icon_state = /obj/structure/flora/bush/snow/style_4::icon_state
+
+/obj/structure/decoration/bush/snow/fifth
+ icon_state = /obj/structure/flora/bush/snow/style_5::icon_state
+
+/obj/structure/decoration/bush/snow/sixth
+ icon_state = /obj/structure/flora/bush/snow/style_6::icon_state
+
+/obj/structure/decoration/bush/snow/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "snowbush[rand(1, 6)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/jungle
+ name = "plastic jungle bush"
+ desc = "Plastic jungle foliage. All the looks, none of the allergens."
+ icon = /obj/structure/flora/bush/jungle::icon
+ icon_state = /obj/structure/flora/bush/jungle::icon_state
+
+/obj/structure/decoration/bush/jungle/first
+
+/obj/structure/decoration/bush/jungle/second
+ icon_state = /obj/structure/flora/bush/jungle/a/style_2::icon_state
+
+/obj/structure/decoration/bush/jungle/third
+ icon_state = /obj/structure/flora/bush/jungle/a/style_3::icon_state
+
+/obj/structure/decoration/bush/jungle/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "busha[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/jungle/b
+ icon = /obj/structure/flora/bush/jungle/b::icon
+ icon_state = /obj/structure/flora/bush/jungle/b::icon_state
+
+/obj/structure/decoration/bush/jungle/b/first
+
+/obj/structure/decoration/bush/jungle/b/second
+ icon_state = /obj/structure/flora/bush/jungle/b/style_2::icon_state
+
+/obj/structure/decoration/bush/jungle/b/third
+ icon_state = /obj/structure/flora/bush/jungle/b/style_3::icon_state
+
+/obj/structure/decoration/bush/jungle/b/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "bushb[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/jungle/c
+ icon = /obj/structure/flora/bush/jungle/c::icon
+ icon_state = /obj/structure/flora/bush/jungle/c::icon_state
+
+/obj/structure/decoration/bush/jungle/c/first
+
+/obj/structure/decoration/bush/jungle/c/second
+ icon_state = /obj/structure/flora/bush/jungle/c/style_2::icon_state
+
+/obj/structure/decoration/bush/jungle/c/third
+ icon_state = /obj/structure/flora/bush/jungle/c/style_3::icon_state
+
+/obj/structure/decoration/bush/jungle/c/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "bushc[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/bush/large
+ name = "large plastic bush"
+ desc = "A large plastic bush. Dominates the room with its hollow presence."
+ icon = /obj/structure/flora/bush/large::icon
+ icon_state = /obj/structure/flora/bush/large::icon_state
+ pixel_x = /obj/structure/flora/bush/large::pixel_x
+ pixel_y = /obj/structure/flora/bush/large::pixel_y
+ layer = /obj/structure/flora/bush/large::layer
+ plane = /obj/structure/flora/bush/large::plane
+ density = /obj/structure/flora/bush/large::density
+
+/obj/structure/decoration/bush/large/first
+
+/obj/structure/decoration/bush/large/second
+ icon_state = /obj/structure/flora/bush/large/style_2::icon_state
+
+/obj/structure/decoration/bush/large/third
+ icon_state = /obj/structure/flora/bush/large/style_3::icon_state
+
+/obj/structure/decoration/bush/large/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "bush[rand(1, 3)]"
+ update_appearance()
+
+/obj/structure/decoration/rock
+ name = "plastic rock"
+ desc = "A hollow plastic boulder. Surprisingly convincing from a distance."
+ icon = /obj/structure/flora/rock::icon
+ icon_state = /obj/structure/flora/rock::icon_state
+
+/obj/structure/decoration/rock/first
+
+/obj/structure/decoration/rock/second
+ icon_state = /obj/structure/flora/rock/style_2::icon_state
+
+/obj/structure/decoration/rock/third
+ icon_state = /obj/structure/flora/rock/style_3::icon_state
+
+/obj/structure/decoration/rock/fourth
+ icon_state = /obj/structure/flora/rock/style_4::icon_state
+
+/obj/structure/decoration/rock/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "basalt[rand(1, 4)]"
+ update_appearance()
+
+/obj/structure/decoration/rock/pile
+ name = "plastic rock pile"
+ desc = "A pile of hollow plastic rocks. Light enough to kick over."
+ icon = /obj/structure/flora/rock/pile::icon
+ icon_state = /obj/structure/flora/rock/pile::icon_state
+
+/obj/structure/decoration/rock/pile/first
+
+/obj/structure/decoration/rock/pile/second
+ icon_state = /obj/structure/flora/rock/pile/style_2::icon_state
+
+/obj/structure/decoration/rock/pile/third
+ icon_state = /obj/structure/flora/rock/pile/style_3::icon_state
+
+/obj/structure/decoration/rock/pile/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "lavarocks[pick(3;1,3;2,1;3)]"
+ update_appearance()
+
+/obj/structure/decoration/rock/pile/jungle
+ name = "plastic jungle rocks"
+ desc = "Fake rocks with a jungle theme. No actual geological history."
+ icon = /obj/structure/flora/rock/pile/jungle::icon
+ icon_state = /obj/structure/flora/rock/pile/jungle::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/first
+
+/obj/structure/decoration/rock/pile/jungle/second
+ icon_state = /obj/structure/flora/rock/pile/jungle/style_2::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/third
+ icon_state = /obj/structure/flora/rock/pile/jungle/style_3::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/fourth
+ icon_state = /obj/structure/flora/rock/pile/jungle/style_4::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/fifth
+ icon_state = /obj/structure/flora/rock/pile/jungle/style_5::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "rock[rand(1, 5)]"
+ update_appearance()
+
+/obj/structure/decoration/rock/pile/jungle/large
+ name = "plastic large rocks"
+ desc = "A pile of large fake jungle rocks. Surprisingly light."
+ icon = /obj/structure/flora/rock/pile/jungle/large::icon
+ icon_state = /obj/structure/flora/rock/pile/jungle/large::icon_state
+ pixel_x = /obj/structure/flora/rock/pile/jungle/large::pixel_x
+ pixel_y = /obj/structure/flora/rock/pile/jungle/large::pixel_y
+
+/obj/structure/decoration/rock/pile/jungle/large/first
+
+/obj/structure/decoration/rock/pile/jungle/large/second
+ icon_state = /obj/structure/flora/rock/pile/jungle/large/style_2::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/large/third
+ icon_state = /obj/structure/flora/rock/pile/jungle/large/style_3::icon_state
+
+/obj/structure/decoration/rock/pile/jungle/large/style_random/Initialize(mapload)
+ . = ..()
+ icon_state = "rocks[rand(1, 3)]"
+ update_appearance()
diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm
index 95c725868ca1..f2f9d138df80 100644
--- a/code/game/objects/structures/fence.dm
+++ b/code/game/objects/structures/fence.dm
@@ -18,6 +18,7 @@
icon = 'icons/obj/fence.dmi'
icon_state = "straight"
+ tacmap_color = TACMAP_FENCE
var/cuttable = TRUE
var/hole_size= NO_HOLE
diff --git a/code/game/objects/structures/flora.dm b/code/game/objects/structures/flora.dm
index b02b29c2b72a..d46b7e133f4b 100644
--- a/code/game/objects/structures/flora.dm
+++ b/code/game/objects/structures/flora.dm
@@ -1126,7 +1126,7 @@
/obj/structure/flora/rock/volcano/Initialize(mapload)
. = ..()
- icon_state = "[base_icon_state]_[rand(1, 5)]"
+ icon_state = "[base_icon_state]_[rand(1, 4)]"
update_appearance()
/obj/structure/flora/rock/volcano/update_overlays()
diff --git a/code/game/objects/structures/ladders.dm b/code/game/objects/structures/ladders.dm
index 5d15c1af89f0..88423eb182c5 100644
--- a/code/game/objects/structures/ladders.dm
+++ b/code/game/objects/structures/ladders.dm
@@ -165,6 +165,7 @@
down.up = null
down.update_appearance(UPDATE_ICON_STATE)
+ down.update_minimap_blip()
down = null
update_appearance(UPDATE_ICON_STATE)
clear_base_transparency()
@@ -188,6 +189,7 @@
up.down = null
up.clear_base_transparency()
up.update_appearance(UPDATE_ICON_STATE)
+ up.update_minimap_blip()
up = null
update_appearance(UPDATE_ICON_STATE)
@@ -196,6 +198,11 @@
unlink_down()
unlink_up()
+/obj/structure/ladder/proc/update_minimap_blip()
+ remove_minimap_blip(MINIMAP_LADDER_BLIP, src)
+ if(up || down)
+ add_minimap_blip(src, MINIMAP_LADDER_BLIP, "ladder")
+
/obj/structure/ladder/LateInitialize()
// By default, discover ladders above and below us vertically
var/turf/base = get_turf(src)
@@ -213,6 +220,7 @@
// Linking updates our icon, so if we failed both links we need a manual update
if(isnull(down) && isnull(up))
update_appearance(UPDATE_ICON_STATE)
+ update_minimap_blip()
/obj/structure/ladder/update_icon_state()
icon_state = "[base_icon_state][!!up][!!down]"
diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm
index 8d7e1fc84e12..0f037fec4d5c 100644
--- a/code/game/objects/structures/stairs.dm
+++ b/code/game/objects/structures/stairs.dm
@@ -4,6 +4,8 @@
/// Range within which stair indicators will appear for approaching mobs
#define STAIR_INDICATOR_RANGE 3
+/// Minimum tile spacing between stair minimap blips
+#define STAIR_BLIP_MIN_DISTANCE 2
// dir determines the direction of travel to go upwards
// stairs require /turf/open/openspace as the tile above them to work, unless your stairs have 'force_open_above' set to TRUE
@@ -27,6 +29,8 @@
VAR_FINAL/turf/directly_above
/// If TRUE, we have left/middle/right sprites.
var/has_merged_sprites = TRUE
+ /// Current atoms used as this stair's minimap blip targets.
+ var/list/minimap_blip_targets
/// Lazyassoc list of weakef to mob viewing stair indicators to their images
VAR_PRIVATE/list/mob_to_image
@@ -63,6 +67,7 @@
force_open_above()
build_signal_listener()
update_surrounding()
+ update_minimap_blip()
var/static/list/exit_connections = list(
COMSIG_ATOM_EXIT = PROC_REF(on_exit_stairs),
@@ -78,6 +83,7 @@
/obj/structure/stairs/Destroy()
+ clear_minimap_blips()
if(directly_above)
UnregisterSignal(directly_above, COMSIG_TURF_MULTIZ_NEW)
directly_above = null
@@ -104,6 +110,47 @@
for(var/obj/structure/stairs/stair in get_step(src, turn(dir, -90)))
stair.update_appearance()
+ update_minimap_blip()
+
+/obj/structure/stairs/proc/update_minimap_blip()
+ var/bottom_state = isTerminator() ? "stairs_up" : "stairs_down"
+ var/top_state = (bottom_state == "stairs_up") ? "stairs_down" : "stairs_up"
+ var/turf/current_turf = get_turf(src)
+
+ clear_minimap_blips()
+ if(isnull(current_turf))
+ return
+
+ add_minimap_blip_if_valid(current_turf, bottom_state)
+ add_minimap_blip_if_valid(get_step_multiz(current_turf, UP), top_state)
+
+/obj/structure/stairs/proc/clear_minimap_blips()
+ if(!islist(minimap_blip_targets))
+ return
+ for(var/atom/target as anything in minimap_blip_targets)
+ remove_minimap_blip(MINIMAP_STAIR_BLIP, target)
+ LAZYCLEARLIST(minimap_blip_targets)
+
+/obj/structure/stairs/proc/add_minimap_blip_if_valid(atom/target, state)
+ if(isnull(target))
+ return
+ if(!should_place_minimap_blip(target))
+ return
+
+ var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(MINIMAP_STAIR_BLIP, target)
+ if(!isnull(blip))
+ blip.icon_state = state
+ else
+ add_minimap_blip(target, MINIMAP_STAIR_BLIP, state)
+ LAZYADD(minimap_blip_targets, target)
+
+/obj/structure/stairs/proc/should_place_minimap_blip(atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(isnull(target_turf))
+ return FALSE
+ if(length(get_minimap_blips_in_area(MINIMAP_STAIR_BLIP, target_turf, STAIR_BLIP_MIN_DISTANCE)))
+ return FALSE
+ return TRUE
/obj/structure/stairs/update_icon_state()
. = ..()
@@ -420,3 +467,4 @@
#undef STAIR_TERMINATOR_YES
#undef STAIR_INDICATOR_RANGE
+#undef STAIR_BLIP_MIN_DISTANCE
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index cd54bd380f2f..6ca6b22836c9 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -18,6 +18,7 @@
flags_ricochet = RICOCHET_HARD
receive_ricochet_chance_mod = 0.5
custom_materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT)
+ tacmap_color = TACMAP_WINDOW
var/state = WINDOW_OUT_OF_FRAME
var/reinf = FALSE
var/heat_resistance = 800
diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm
index 92b2e6f19e9b..6f1423dd9afe 100644
--- a/code/game/sound/sound.dm
+++ b/code/game/sound/sound.dm
@@ -14,8 +14,9 @@
* * 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.
* * volume_preference - Optional: Will be checked to modify the volume of the sound for each listener.
+ * * min_volume - minimum volume the sound can reach at max_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, datum/preference/numeric/volume/volume_preference = null)
+/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, datum/preference/numeric/volume/volume_preference = null, min_volume = 3)
if(isarea(source))
CRASH("playsound(): source is an area")
@@ -25,8 +26,6 @@
if(!soundin)
CRASH("playsound(): no soundin passed")
- if(vol < SOUND_AUDIBLE_VOLUME_MIN) // never let sound go below SOUND_AUDIBLE_VOLUME_MIN or bad things will happen
- CRASH("playsound(): volume below SOUND_AUDIBLE_VOLUME_MIN. [vol] < [SOUND_AUDIBLE_VOLUME_MIN]")
var/turf/turf_source = get_turf(source)
if (!turf_source)
@@ -50,30 +49,28 @@
var/turf/above_turf = GET_TURF_ABOVE(turf_source)
var/turf/below_turf = GET_TURF_BELOW(turf_source)
- var/audible_distance = CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(vol, maxdistance, falloff_distance, falloff_exponent)
-
if(ignore_walls)
- listeners = get_hearers_in_range(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners = get_hearers_in_range(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(above_turf && istransparentturf(above_turf))
- listeners += get_hearers_in_range(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_range(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(below_turf && istransparentturf(turf_source))
- listeners += get_hearers_in_range(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_range(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
else //these sounds don't carry through walls
- listeners = get_hearers_in_view(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners = get_hearers_in_view(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(above_turf && istransparentturf(above_turf))
- listeners += get_hearers_in_view(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_view(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
if(below_turf && istransparentturf(turf_source))
- listeners += get_hearers_in_view(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS)
+ listeners += get_hearers_in_view(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE)
for(var/mob/listening_ghost as anything in SSmobs.dead_players_by_zlevel[source_z])
- if(get_dist(listening_ghost, turf_source) <= audible_distance)
- listeners += listening_ghost
+ listeners += listening_ghost
for(var/mob/listening_mob in listeners)//had nulls sneak in here, hence the typecheck
- listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference)
+ if(get_dist_euclidean(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, volume_preference)
return listeners
@@ -96,8 +93,9 @@
* * distance_multiplier - Default 1, multiplies the maximum distance of our sound
* * use_reverb - bool default TRUE, determines if our sound has reverb
* * volume_preference - Optional: Will be checked to modify the volume of the sound.
+ * * min_volume - minimum volume the sound can reach at max_range.
*/
-/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, datum/preference/numeric/volume/volume_preference = null)
+/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, datum/preference/numeric/volume/volume_preference = null, min_volume = 5)
if(!client || HAS_TRAIT(src, TRAIT_DEAF))
return
@@ -120,10 +118,10 @@
var/turf/turf_loc = get_turf(src)
//sound volume falloff with distance
- distance = get_dist(turf_loc, turf_source) * distance_multiplier
+ distance = get_dist_euclidean(turf_loc, turf_source) * distance_multiplier
if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff.
- sound_to_use.volume -= CALCULATE_SOUND_VOLUME(vol, distance, max_distance, falloff_distance, falloff_exponent)
+ sound_to_use.volume -= CALCULATE_SOUND_VOLUME_RATIO(vol, distance, max_distance, falloff_distance, falloff_exponent) * (vol - min_volume)
if(pressure_affected)
//Atmosphere affects sound
@@ -144,9 +142,6 @@
sound_to_use.volume *= pressure_factor
//End Atmosphere affecting sound
- if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
- return FALSE
-
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
@@ -174,7 +169,7 @@
if(ispath(volume_preference) && client.prefs)
var/client_volume_modifier = client.prefs.read_preference(volume_preference)
sound_to_use.volume *= (client_volume_modifier / 100)
- if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
+ if(sound_to_use.volume < 0.1)
return FALSE
if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED))
diff --git a/code/game/turfs/closed/_closed.dm b/code/game/turfs/closed/_closed.dm
index c29772afc5b4..09bd513be8e3 100644
--- a/code/game/turfs/closed/_closed.dm
+++ b/code/game/turfs/closed/_closed.dm
@@ -9,6 +9,7 @@
init_air = FALSE
rad_insulation = RAD_MEDIUM_INSULATION
pass_flags_self = PASSCLOSEDTURF
+ tacmap_color = TACMAP_BLACK
/turf/closed/AfterChange()
. = ..()
diff --git a/code/game/turfs/closed/minerals.dm b/code/game/turfs/closed/minerals.dm
index 37575ccffa85..65ad140aa368 100644
--- a/code/game/turfs/closed/minerals.dm
+++ b/code/game/turfs/closed/minerals.dm
@@ -429,8 +429,8 @@
color = COLOR_BLUE
else
color = BlendRGB(COLOR_GREEN, COLOR_RED, clamp((open_turf_distance - 1) / 5, 0, 0.99))
- maptext_x = 4
- maptext_y = 4
+ maptext_x = -transform.c
+ maptext_y = -transform.f
maptext = MAPTEXT_TINY_UNICODE("[open_turf_distance]")
#endif
diff --git a/code/game/turfs/open/asteroid.dm b/code/game/turfs/open/asteroid.dm
index c441871caa82..b8128820f8ec 100644
--- a/code/game/turfs/open/asteroid.dm
+++ b/code/game/turfs/open/asteroid.dm
@@ -182,6 +182,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
initial_gas_mix = LAVALAND_DEFAULT_ATMOS
planetary_atmos = TRUE
baseturfs = /turf/open/lava/smooth/lava_land_surface
+ skip_minimap_rendering = TRUE
/// Used for the lavaland icemoon ruin.
/turf/open/misc/asteroid/basalt/lava_land_surface/no_ruins
@@ -393,6 +394,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
baseturfs = /turf/open/openspace/icemoon
initial_gas_mix = ICEMOON_DEFAULT_ATMOS
slowdown = 0
+ skip_minimap_rendering = TRUE
/// Exact subtype as parent, just used in ruins to prevent other ruins/chasms from spawning on top of it.
/turf/open/misc/asteroid/snow/icemoon/do_not_chasm
diff --git a/code/game/turfs/open/floor/iron_floor.dm b/code/game/turfs/open/floor/iron_floor.dm
index 6f0489acea5e..7f7a5fa28c9b 100644
--- a/code/game/turfs/open/floor/iron_floor.dm
+++ b/code/game/turfs/open/floor/iron_floor.dm
@@ -414,6 +414,7 @@
icon_state = "solarpanel"
base_icon_state = "solarpanel"
floor_tile = /obj/item/stack/tile/iron/solarpanel
+ skip_minimap_rendering = TRUE
/turf/open/floor/iron/solarpanel/airless
initial_gas_mix = AIRLESS_ATMOS
diff --git a/code/game/turfs/open/floor/misc_floor.dm b/code/game/turfs/open/floor/misc_floor.dm
index 61f2c76bc797..ae5ae6c3e9dd 100644
--- a/code/game/turfs/open/floor/misc_floor.dm
+++ b/code/game/turfs/open/floor/misc_floor.dm
@@ -75,6 +75,9 @@
icon_state = "bcircuitoff"
always_off = TRUE
+/turf/open/floor/circuit/no_light
+ always_off = TRUE
+
/turf/open/floor/circuit/airless
initial_gas_mix = AIRLESS_ATMOS
@@ -120,6 +123,9 @@
icon_state = "rcircuitoff"
always_off = TRUE
+/turf/open/floor/circuit/red/no_power
+ always_off = TRUE
+
/turf/open/floor/circuit/red/anim
icon_state = "rcircuitanim"
floor_tile = /obj/item/stack/tile/circuit/red/anim
diff --git a/code/game/turfs/open/ice.dm b/code/game/turfs/open/ice.dm
index a966e8e4d7e6..1a227d2a0340 100644
--- a/code/game/turfs/open/ice.dm
+++ b/code/game/turfs/open/ice.dm
@@ -82,6 +82,7 @@
baseturfs = /turf/open/openspace/icemoon
initial_gas_mix = ICEMOON_DEFAULT_ATMOS
slowdown = 0
+ skip_minimap_rendering = TRUE
/turf/open/misc/ice/icemoon/no_planet_atmos
planetary_atmos = FALSE
diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm
index dcc735819f3b..e4c447008da7 100644
--- a/code/game/turfs/open/space/space.dm
+++ b/code/game/turfs/open/space/space.dm
@@ -71,6 +71,7 @@ GLOBAL_LIST_EMPTY(starlight)
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
+ skip_minimap_rendering = TRUE
/turf/open/space/basic
icon_state = MAP_SWITCH("space", "space_basic_map")
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index b6a0b8c38678..4a98f911a040 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -113,6 +113,9 @@ GLOBAL_LIST_EMPTY(station_turfs)
///The typepath we use for lazy fishing on turfs, to save on world init time.
var/fish_source
+ /// If TRUE, then this turf will be skipped entirely by minimap rendering.
+ var/skip_minimap_rendering = FALSE
+
/turf/vv_edit_var(var_name, new_value)
var/static/list/banned_edits = list(NAMEOF_STATIC(src, x), NAMEOF_STATIC(src, y), NAMEOF_STATIC(src, z))
diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm
index 8721a1aabb25..290e52e5ee7c 100644
--- a/code/modules/admin/verbs/adminweather.dm
+++ b/code/modules/admin/verbs/adminweather.dm
@@ -2,7 +2,7 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather
var/list/weather_choices = list()
if(!length(weather_choices))
- for(var/datum/weather/weather_type as anything in subtypesof(/datum/weather))
+ for(var/datum/weather/weather_type as anything in valid_subtypesof(/datum/weather))
weather_choices[initial(weather_type.type)] = weather_type
var/datum/weather/weather_choice = tgui_input_list(user, "Choose a weather to run", "Weather", weather_choices)
diff --git a/code/modules/admin/view_variables/reference_tracking.dm b/code/modules/admin/view_variables/reference_tracking.dm
index cccb97f6c9bb..d7123e844844 100644
--- a/code/modules/admin/view_variables/reference_tracking.dm
+++ b/code/modules/admin/view_variables/reference_tracking.dm
@@ -205,7 +205,7 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b)
DoSearchVar(element_in_list, "[container_name] -> [element_in_list] (list)", search_time, recursion_count + 1)
//Check normal entrys
else if(element_in_list == src)
- #ifdef REFERENCE_TRACKING_DEBUG
+#ifdef REFERENCE_TRACKING_DEBUG
if(SSgarbage.should_save_refs)
if(!found_refs)
found_refs = list()
@@ -213,9 +213,9 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b)
continue
else
log_reftracker("Found [type] [text_ref(src)] in list [container_name].")
- #else
+#else
log_reftracker("Found [type] [text_ref(src)] in list [container_name].")
- #endif
+#endif
// This is dumb as hell I'm sorry
// I don't want the garbage subsystem to count as a ref for the purposes of this number
diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm
index 527196c51c3e..178ec2d59a16 100644
--- a/code/modules/antagonists/_common/antag_team.dm
+++ b/code/modules/antagonists/_common/antag_team.dm
@@ -29,6 +29,9 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
/datum/team/Destroy(force)
GLOB.antagonist_teams -= src
members = null
+ for(var/datum/objective/objective as anything in objectives)
+ if(objective.team == src)
+ objective.team = null
objectives = null
return ..()
diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm
index bafcd778401a..b2cf3808eff1 100644
--- a/code/modules/antagonists/blob/powers.dm
+++ b/code/modules/antagonists/blob/powers.dm
@@ -12,23 +12,23 @@
/** Places the core itself */
/mob/eye/blob/proc/place_blob_core(placement_override = BLOB_NORMAL_PLACEMENT, pop_override = FALSE)
if(placed && placement_override != BLOB_FORCE_PLACEMENT)
- return TRUE
+ return
if(placement_override == BLOB_NORMAL_PLACEMENT)
if(!pop_override && !check_core_visibility())
- return FALSE
+ return
var/turf/placement = get_turf(src)
if(placement.density)
to_chat(src, span_warning("This spot is too dense to place a blob core on!"))
- return FALSE
+ return
if(!is_valid_turf(placement))
to_chat(src, span_warning("You cannot place your core here!"))
- return FALSE
+ return
if(!check_objects_tile(placement))
- return FALSE
+ return
if(!pop_override && world.time <= manualplace_min_time && world.time <= autoplace_max_time)
to_chat(src, span_warning("It is too early to place your blob core!"))
- return FALSE
+ return
else
if(placement_override == BLOB_RANDOM_PLACEMENT)
var/turf/force_tile = pick(GLOB.blobstart)
@@ -47,8 +47,6 @@
placed = TRUE
announcement_time = world.time + OVERMIND_ANNOUNCEMENT_MAX_TIME
- return TRUE
-
/** Checks proximity for mobs */
/mob/eye/blob/proc/check_core_visibility()
for(var/mob/living/player in range(7, src))
@@ -92,7 +90,7 @@
/** Jumps to a node */
/mob/eye/blob/proc/jump_to_node()
if(!length(GLOB.blob_nodes))
- return FALSE
+ return
var/list/nodes = list()
for(var/index in 1 to length(GLOB.blob_nodes))
@@ -101,7 +99,7 @@
var/node_name = tgui_input_list(src, "Choose a node to jump to", "Node Jump", nodes)
if(isnull(node_name) || isnull(nodes[node_name]))
- return FALSE
+ return
var/obj/structure/blob/special/node/chosen_node = nodes[node_name]
if(chosen_node)
@@ -115,31 +113,31 @@
if(!blob)
to_chat(src, span_warning("There is no blob here!"))
balloon_alert(src, "no blob here!")
- return FALSE
+ return
if(!istype(blob, /obj/structure/blob/normal))
to_chat(src, span_warning("Unable to use this blob, find a normal one."))
balloon_alert(src, "need normal blob!")
- return FALSE
+ return
if(needs_node)
var/area/area = get_area(src)
if(!(area.area_flags & BLOBS_ALLOWED)) //factory and resource blobs must be legit
to_chat(src, span_warning("This type of blob must be placed on the station!"))
balloon_alert(src, "can't place off-station!")
- return FALSE
+ return
if(nodes_required && !(locate(/obj/structure/blob/special/node) in orange(BLOB_NODE_PULSE_RANGE, tile)) && !(locate(/obj/structure/blob/special/core) in orange(BLOB_CORE_PULSE_RANGE, tile)))
to_chat(src, span_warning("You need to place this blob closer to a node or core!"))
balloon_alert(src, "too far from node or core!")
- return FALSE //handholdotron 2000
+ return
if(min_separation)
for(var/obj/structure/blob/other_blob in orange(min_separation, tile))
if(other_blob.type == blobstrain)
to_chat(src, span_warning("There is a similar blob nearby, move more than [min_separation] tiles away from it!"))
other_blob.balloon_alert(src, "too close!")
- return FALSE
+ return
if(!can_buy(price))
- return FALSE
- var/obj/structure/blob/node = blob.change_to(blobstrain, src)
- return node
+ return
+
+ return blob.change_to(blobstrain, src)
/** Toggles requiring nodes */
/mob/eye/blob/proc/toggle_node_req()
@@ -155,15 +153,18 @@
if(!shield)
shield = create_special(BLOB_UPGRADE_STRONG_COST, /obj/structure/blob/shield, 0, FALSE, tile)
shield?.balloon_alert(src, "upgraded to [shield.name]!")
- return FALSE
+ return
- if(!can_buy(BLOB_UPGRADE_REFLECTOR_COST))
- return FALSE
+ if(istype(shield, /obj/structure/blob/shield/reflective))
+ to_chat(src, span_warning("This shield blob is already as resilient as you can make it!"))
+ return
if(shield.get_integrity() < shield.max_integrity * 0.5)
- add_points(BLOB_UPGRADE_REFLECTOR_COST)
to_chat(src, span_warning("This shield blob is too damaged to be modified properly!"))
- return FALSE
+ return
+
+ if(!can_buy(BLOB_UPGRADE_REFLECTOR_COST))
+ return
to_chat(src, span_warning("You secrete a reflective ooze over the shield blob, allowing it to reflect projectiles at the cost of reduced integrity."))
shield = shield.change_to(/obj/structure/blob/shield/reflective, src)
@@ -175,15 +176,15 @@
var/obj/structure/blob/special/factory/factory = locate(/obj/structure/blob/special/factory) in current_turf
if(!factory)
to_chat(src, span_warning("You must be on a factory blob!"))
- return FALSE
+ return
if(factory.blobbernaut || factory.is_creating_blobbernaut) //if it already made or making a blobbernaut, it can't do it again
to_chat(src, span_warning("This factory blob is already sustaining a blobbernaut."))
- return FALSE
+ return
if(factory.get_integrity() < factory.max_integrity * 0.5)
to_chat(src, span_warning("This factory blob is too damaged to sustain a blobbernaut."))
- return FALSE
+ return
if(!can_buy(BLOBMOB_BLOBBERNAUT_RESOURCE_COST))
- return FALSE
+ return
factory.is_creating_blobbernaut = TRUE
to_chat(src, span_notice("You attempt to produce a blobbernaut."))
@@ -214,7 +215,7 @@
to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later."))
add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
factory.assign_blobbernaut(null)
- return FALSE
+ return
var/mob_type = /mob/living/basic/blob_minion/blobbernaut/minion
var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new mob_type(get_turf(factory), blob_borne = TRUE)
@@ -229,19 +230,19 @@
if(!blob)
to_chat(src, span_warning("You must be on a blob node!"))
- return FALSE
+ return
if(!blob_core)
to_chat(src, span_userdanger("You have no core and are about to die! May you rest in peace."))
- return FALSE
+ return
var/area/area = get_area(tile)
if(isspaceturf(tile) || area && !(area.area_flags & BLOBS_ALLOWED))
to_chat(src, span_warning("You cannot relocate your core here!"))
- return FALSE
+ return
if(!can_buy(BLOB_POWER_RELOCATE_COST))
- return FALSE
+ return
var/turf/old_turf = get_turf(blob_core)
var/old_dir = blob_core.dir
@@ -256,15 +257,15 @@
if(!blob)
to_chat(src, span_warning("There is no blob there!"))
- return FALSE
+ return
if(blob.point_return < 0)
to_chat(src, span_warning("Unable to remove this blob."))
- return FALSE
+ return
if(max_blob_points < blob.point_return + blob_points)
to_chat(src, span_warning("You have too many resources to remove this blob!"))
- return FALSE
+ return
if(blob.point_return)
add_points(blob.point_return)
@@ -273,12 +274,10 @@
qdel(blob)
- return TRUE
-
/** Expands to nearby tiles */
/mob/eye/blob/proc/expand_blob(turf/tile)
if(world.time < last_attack)
- return FALSE
+ return
var/list/possible_blobs = list()
for(var/obj/structure/blob/blob in range(tile, 1))
@@ -286,10 +285,10 @@
if(!length(possible_blobs))
to_chat(src, span_warning("There is no blob adjacent to the target tile!"))
- return FALSE
+ return
if(!can_buy(BLOB_EXPAND_COST))
- return FALSE
+ return
var/attack_success
for(var/mob/living/player in tile)
@@ -343,14 +342,14 @@
add_points(BLOB_ATTACK_REFUND)
else
add_points(BLOB_EXPAND_COST) //if we're attacking diagonally and didn't hit anything, refund
- return TRUE
+ return
/** Rally spores to a location */
/mob/eye/blob/proc/rally_spores(turf/tile)
to_chat(src, "You rally your spores.")
var/list/surrounding_turfs = TURF_NEIGHBORS(tile)
if(!length(surrounding_turfs))
- return FALSE
+ return
for(var/mob/living/basic/blob_mob as anything in blob_mobs)
if(!isturf(blob_mob.loc) || get_dist(blob_mob, tile) > 35 || blob_mob.key)
continue
@@ -361,7 +360,7 @@
/mob/eye/blob/proc/strain_reroll()
if (!free_strain_rerolls && blob_points < BLOB_POWER_REROLL_COST)
to_chat(src, span_warning("You need at least [BLOB_POWER_REROLL_COST] resources to reroll your strain again!"))
- return FALSE
+ return
open_reroll_menu()
diff --git a/code/modules/antagonists/blood_worm/blood_worm_mob.dm b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
index 7ac058417869..44b5f662f84a 100644
--- a/code/modules/antagonists/blood_worm/blood_worm_mob.dm
+++ b/code/modules/antagonists/blood_worm/blood_worm_mob.dm
@@ -24,11 +24,12 @@
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
- minimum_survivable_temperature = 0
+ minimum_survivable_temperature = ICEBOX_MIN_TEMPERATURE
maximum_survivable_temperature = T0C + 100
- unsuitable_cold_damage = 0
+ unsuitable_cold_damage = 1
+ unsuitable_atmos_damage = 2
- habitable_atmos = null
+ habitable_atmos = list("min_oxy" = 5, "max_oxy" = 0)
// A vivid red.
lighting_cutoff_red = 40
diff --git a/code/modules/antagonists/clown_ops/outfits.dm b/code/modules/antagonists/clown_ops/outfits.dm
index 18016da7c4a5..b66b2212445b 100644
--- a/code/modules/antagonists/clown_ops/outfits.dm
+++ b/code/modules/antagonists/clown_ops/outfits.dm
@@ -18,7 +18,7 @@
/obj/item/mod/skin_applier/honkerative = 1,
)
box = /obj/item/storage/box/survival/syndie
- implants = list(/obj/item/implant/sad_trombone)
+ implants = list(/obj/item/implant/sad_trombone, /obj/item/implant/tacmap/nuclear)
uplink_type = /obj/item/uplink/clownop
@@ -33,3 +33,4 @@
command_radio = TRUE
id_trim = /datum/id_trim/chameleon/operative/clown_leader
+ implants = list(/obj/item/implant/sad_trombone, /obj/item/implant/tacmap/nuclear/leader)
diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm
index 7e9aa464142e..06a95d809d00 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm
@@ -25,6 +25,7 @@
AddComponent(/datum/component/stationloving, !fake)
AddComponent(/datum/component/keep_me_secure, CALLBACK(src, PROC_REF(secured_process)), CALLBACK(src, PROC_REF(unsecured_process)), 10)
SSpoints_of_interest.make_point_of_interest(src)
+ add_minimap_blip(src, MINIMAP_NUKEDISK_BLIP, "green_disk_off", 'icons/ui_icons/minimap/map_blips_large.dmi', TRUE)
else
// Ensure fake disks still have examine text, but dont actually do anything
AddComponent(/datum/component/keep_me_secure)
diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
index 807f0e6e6905..a00956e7c801 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm
@@ -53,6 +53,8 @@ GLOBAL_VAR(station_nuke_source)
var/proper_bomb = TRUE //Please
/// A reference to the countdown that goes up over the nuke
var/obj/effect/countdown/nuclearbomb/countdown
+ /// is this nuke on the MINIMAP_BOMB_BLIP tag minimap?
+ var/is_on_minimap = TRUE
/obj/machinery/nuclearbomb/Initialize(mapload)
. = ..()
@@ -62,6 +64,7 @@ GLOBAL_VAR(station_nuke_source)
update_appearance()
SSpoints_of_interest.make_point_of_interest(src)
previous_level = SSsecurity_level.get_current_level_as_text()
+ update_minimap_blip()
/obj/machinery/nuclearbomb/Destroy()
safety = FALSE
@@ -523,6 +526,7 @@ GLOBAL_VAR(station_nuke_source)
arm_nuke(usr)
else
disarm_nuke(usr)
+ update_minimap_blip()
/// Arms the nuke, making it active and triggering all pinpointers to start counting down (+delta alert)
/obj/machinery/nuclearbomb/proc/arm_nuke(mob/armer)
@@ -565,6 +569,12 @@ GLOBAL_VAR(station_nuke_source)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NUKE_DEVICE_DISARMED, src)
update_appearance()
+/obj/machinery/nuclearbomb/proc/update_minimap_blip()
+ if(!is_on_minimap)
+ return
+ var/blip_icon = 'icons/ui_icons/minimap/map_blips_large.dmi'
+ add_minimap_blip(src, MINIMAP_BOMB_BLIP, "nuke_[timing ? "on" : "off"]", blip_icon, TRUE)
+
/// If the nuke is active, gets how much time is left until it detonates, in seconds.
/// If the nuke is not active, gets how much time the nuke is set for, in seconds.
/obj/machinery/nuclearbomb/proc/get_time_left()
diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm
index 66798ae798c6..7bef615c8067 100644
--- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm
+++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm
@@ -3,6 +3,7 @@
name = "\improper Nanotrasen-brand nuclear fission explosive"
desc = "One of the more successful achievements of the Nanotrasen Corporate Warfare Division, their nuclear fission explosives are renowned for being cheap to produce and devastatingly effective. Signs explain that though this particular device has been decommissioned, every Nanotrasen station is equipped with an equivalent one, just in case. All Captains carefully guard the disk needed to detonate them - at least, the sign says they do. There seems to be a tap on the back."
proper_bomb = FALSE
+ is_on_minimap = FALSE
/// The keg located within the beer nuke.
var/obj/structure/reagent_dispensers/beerkeg/keg
/// Reagent that is produced once the nuke detonates.
diff --git a/code/modules/antagonists/nukeop/outfits.dm b/code/modules/antagonists/nukeop/outfits.dm
index 74402a2cd066..ce7cadba7a5a 100644
--- a/code/modules/antagonists/nukeop/outfits.dm
+++ b/code/modules/antagonists/nukeop/outfits.dm
@@ -13,6 +13,11 @@
skillchips = list(/obj/item/skillchip/disk_verifier)
box = /obj/item/storage/box/survival/syndie
+ implants = list(
+ /obj/item/implant/weapons_auth,
+ /obj/item/implant/explosive,
+ /obj/item/implant/tacmap/nuclear,
+ )
/// Amount of TC to automatically store in this outfit's uplink.
var/tc = 25
/// Enables big voice on this outfit's headset, used for nukie leaders.
@@ -33,6 +38,12 @@
name = "Syndicate Leader - Basic"
command_radio = TRUE
+ implants = list(
+ /obj/item/implant/weapons_auth,
+ /obj/item/implant/explosive,
+ /obj/item/implant/tacmap/nuclear/leader,
+ )
+
id_trim = /datum/id_trim/chameleon/operative/nuke_leader
/datum/outfit/syndicate/leader/plasmaman
@@ -84,6 +95,11 @@
/datum/outfit/syndicate/full/loneop
name = "Syndicate Operative - Full Kit (Loneop)"
uplink_type = /obj/item/uplink/loneop
+ implants = list(
+ /obj/item/implant/weapons_auth,
+ /obj/item/implant/explosive,
+ /obj/item/implant/tacmap/nuclear/leader,
+ )
/datum/outfit/syndicate/full/plasmaman
name = "Syndicate Operative - Full Kit (Plasmaman)"
@@ -213,3 +229,8 @@
command_radio = TRUE
tc = 0
uplink_type = null
+ implants = list(
+ /obj/item/implant/weapons_auth,
+ /obj/item/implant/explosive,
+ /obj/item/implant/tacmap/nuclear/offbase,
+ )
diff --git a/code/modules/antagonists/traitor/contractor/contractor_items.dm b/code/modules/antagonists/traitor/contractor/contractor_items.dm
index 86bbaf4168c5..7375660f969d 100644
--- a/code/modules/antagonists/traitor/contractor/contractor_items.dm
+++ b/code/modules/antagonists/traitor/contractor/contractor_items.dm
@@ -4,7 +4,6 @@
icon_state = "pinpointer_syndicate"
worn_icon_state = "pinpointer_black"
minimum_range = 25
- has_owner = TRUE
ignore_suit_sensor_level = TRUE
/obj/item/paper/contractor_guide
diff --git a/code/modules/antagonists/voidwalker/voidwalker_window.dm b/code/modules/antagonists/voidwalker/voidwalker_window.dm
index b6dad0722dad..c62dbf68c528 100644
--- a/code/modules/antagonists/voidwalker/voidwalker_window.dm
+++ b/code/modules/antagonists/voidwalker/voidwalker_window.dm
@@ -26,7 +26,7 @@
velocity = list(0, 0.2, 0)
position = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND)
drift = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND)
- spin = generator(GEN_NUM, list(-5,5), NORMAL_RAND)
+ spin = generator(GEN_NUM, -5, 5, NORMAL_RAND)
friction = 0.5
gravity = list(0, 0)
grow = 0.05
diff --git a/code/modules/asset_cache/assets/chat.dm b/code/modules/asset_cache/assets/chat.dm
index 471e8ce85dc6..890f05afe39c 100644
--- a/code/modules/asset_cache/assets/chat.dm
+++ b/code/modules/asset_cache/assets/chat.dm
@@ -11,3 +11,7 @@
if (icon != 'icons/ui/chat/language.dmi')
var/icon_state = initial(L.icon_state)
insert_icon("language-[icon_state]", uni_icon(icon, icon_state))
+
+ var/datum/universal_icon/byond_member = uni_icon('icons/ui/chat/member_content.dmi', "blag")
+ byond_member.scale(16, 16)
+ insert_icon("byond_member", byond_member)
diff --git a/code/modules/asset_cache/assets/rdd.dm b/code/modules/asset_cache/assets/rdd.dm
new file mode 100644
index 000000000000..a5f422b420bb
--- /dev/null
+++ b/code/modules/asset_cache/assets/rdd.dm
@@ -0,0 +1,11 @@
+/datum/asset/spritesheet_batched/rdd
+ name = "rdd"
+
+/datum/asset/spritesheet_batched/rdd/create_spritesheets()
+ for(var/category in GLOB.rdd_designs)
+ for(var/list/design in GLOB.rdd_designs[category])
+ var/obj/structure/decoration/dec_path = design["path"]
+ var/sprite_name = sanitize_css_class_name(design["name"])
+ var/datum/universal_icon/icon = uni_icon(initial(dec_path.icon), initial(dec_path.icon_state))
+ icon.scale(32, 32)
+ insert_icon(sprite_name, icon)
diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 3a76be7b1844..4a6691545802 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -688,6 +688,7 @@
/// Opens/closes the canister valve
/obj/machinery/portable_atmospherics/canister/proc/toggle_valve(mob/user, wire_pulsed = FALSE)
valve_open = !valve_open
+ playsound(src, 'sound/effects/valve_opening.ogg', 50, TRUE)
if(!valve_open)
var/logmsg = "valve was closed by [key_name(user)] [wire_pulsed ? "via wire pulse" : ""], stopping the transfer into \the [holding || "air"].
"
investigate_log(logmsg, INVESTIGATE_ATMOS)
diff --git a/code/modules/capture_the_flag/ctf_controller.dm b/code/modules/capture_the_flag/ctf_controller.dm
index dbc152d6bf46..55becb73c5d8 100644
--- a/code/modules/capture_the_flag/ctf_controller.dm
+++ b/code/modules/capture_the_flag/ctf_controller.dm
@@ -233,10 +233,7 @@
///Creates a CTF game with the provided team ID then returns a reference to the new controller. If a controller already exists provides a reference to it.
/proc/create_ctf_game(game_id)
- if(GLOB.ctf_games[game_id])
- return GLOB.ctf_games[game_id]
- var/datum/ctf_controller/CTF = new(game_id)
- return CTF
+ return GLOB.ctf_games[game_id] || new /datum/ctf_controller(game_id)
#undef CTF_DEFAULT_RESPAWN
#undef CTF_INSTAGIB_RESPAWN
diff --git a/code/modules/capture_the_flag/ctf_game.dm b/code/modules/capture_the_flag/ctf_game.dm
index 2b164277abaa..e7316c12ec67 100644
--- a/code/modules/capture_the_flag/ctf_game.dm
+++ b/code/modules/capture_the_flag/ctf_game.dm
@@ -20,9 +20,7 @@
/obj/machinery/ctf/Initialize(mapload)
. = ..()
- ctf_game = GLOB.ctf_games[game_id]
- if(isnull(ctf_game))
- ctf_game = create_ctf_game(game_id)
+ ctf_game = create_ctf_game(game_id)
///A spawn point for CTF, ghosts can interact with this to vote for CTF or spawn in if a game is running.
/obj/machinery/ctf/spawner
@@ -447,10 +445,10 @@
/obj/effect/ctf/dead_barricade/Initialize(mapload)
. = ..()
ctf_game = GLOB.ctf_games[game_id]
- ctf_game.barricades += src
+ ctf_game?.barricades += src
/obj/effect/ctf/dead_barricade/Destroy()
- ctf_game.barricades -= src
+ ctf_game?.barricades -= src
return ..()
/obj/effect/ctf/dead_barricade/proc/respawn()
@@ -473,10 +471,8 @@
///Proc that handles toggling and unloading CTF.
/proc/toggle_id_ctf(user, activated_id, automated = FALSE, unload = FALSE, area/ctf_area = /area/centcom/ctf)
var/static/loading = CTF_LOADING_UNLOADED
- var/datum/ctf_controller/ctf_controller = GLOB.ctf_games[activated_id]
- if(isnull(ctf_controller))
- ctf_controller = create_ctf_game(activated_id)
- if(unload == TRUE)
+ var/datum/ctf_controller/ctf_controller = create_ctf_game(activated_id)
+ if(unload)
log_admin("[key_name_admin(user)] is attempting to unload CTF.")
message_admins("[key_name_admin(user)] is attempting to unload CTF.")
if(loading == CTF_LOADING_UNLOADED)
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index 88534ea4c718..05d9007d0439 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -81,12 +81,27 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
// massmeta edit addition end(bot_topic)
var/keyname = key
- if(prefs.unlock_content)
- if(prefs.toggles & MEMBER_PUBLIC)
- keyname = "[icon2html('icons/ui/chat/member_content.dmi', world, "blag")][keyname]"
+ var/list/key_tags
+ var/key_prefix = ""
+ var/visible_unlock = prefs.unlock_content && (prefs.toggles & MEMBER_PUBLIC)
+
+ // heart first lol
if(prefs.hearted)
- var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat)
- keyname = "[sheet.icon_tag("emoji-heart")][keyname]"
+ LAZYADD(key_tags, "emoji-heart")
+ if(visible_unlock)
+ LAZYADD(key_tags, "byond_member")
+
+ if(LAZYLEN(key_tags))
+ var/datum/asset/spritesheet_batched/chat/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat)
+ for(var/icon_name in key_tags)
+ key_prefix = "[key_prefix][sheet.icon_tag(icon_name)]"
+ key_prefix = "[key_prefix]"
+
+ keyname = "[key_prefix][keyname]"
+
+ if(visible_unlock)
+ keyname = "[keyname]"
+
//The linkify span classes and linkify=TRUE below make ooc text get clickable chat href links if you pass in something resembling a url
for(var/client/receiver as anything in GLOB.clients)
if(!receiver.prefs) // Client being created or deleted. Despite all, this can be null.
diff --git a/code/modules/events/wizard/magical_rain.dm b/code/modules/events/wizard/magical_rain.dm
index 203a960d36e6..0885c9830648 100644
--- a/code/modules/events/wizard/magical_rain.dm
+++ b/code/modules/events/wizard/magical_rain.dm
@@ -21,7 +21,7 @@
if(!started)
started = TRUE
- SSweather.run_weather(/datum/weather/rain_storm/wizard)
+ SSweather.run_weather(/datum/weather/particle/rain_storm/wizard)
/datum/round_event/wizard/magical_rain/end()
for(var/mob/living/wizard in GLOB.alive_mob_list)
diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm
index 8799735505ea..195e34f734bd 100644
--- a/code/modules/fishing/fish/_fish.dm
+++ b/code/modules/fishing/fish/_fish.dm
@@ -816,14 +816,14 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list(
/obj/item/fish/apply_material_effects()
. = ..()
//Either effects aren't applied of he materials are simply being increased/decreased along with the weight. Avoids recursion.
- if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) || material_weight_mult == 1)
+ if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) )
return
maximum_weight *= material_weight_mult
update_size_and_weight(size, (temp_weight || weight) * material_weight_mult, update_materials = FALSE)
/obj/item/fish/remove_material_effects(replace_mats = TRUE)
. = ..()
- if(replace_mats || !(material_flags & MATERIAL_EFFECTS) || material_weight_mult == 1)
+ if(replace_mats || !(material_flags & MATERIAL_EFFECTS) )
return
maximum_weight /= material_weight_mult
update_size_and_weight(size, weight / material_weight_mult)
diff --git a/code/modules/food_and_drinks/machinery/griddle.dm b/code/modules/food_and_drinks/machinery/griddle.dm
index 0698174d4a8c..5041259352a8 100644
--- a/code/modules/food_and_drinks/machinery/griddle.dm
+++ b/code/modules/food_and_drinks/machinery/griddle.dm
@@ -95,7 +95,7 @@
if(isnull(item.atom_storage))
return NONE
- if(length(contents) >= max_items)
+ if(length(griddled_objects) >= max_items)
balloon_alert(user, "it's full!")
return ITEM_INTERACT_BLOCKING
@@ -109,7 +109,7 @@
for(var/obj/tray_item in item)
if(!IS_EDIBLE(tray_item))
continue
- if(length(contents) >= max_items)
+ if(length(griddled_objects) >= max_items)
break
if(item.atom_storage.attempt_remove(tray_item, src))
loaded++
diff --git a/code/modules/food_and_drinks/recipes/soup_mixtures.dm b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
index ab9a2175a89a..e9f0aa096eaa 100644
--- a/code/modules/food_and_drinks/recipes/soup_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
@@ -1865,6 +1865,17 @@
resulting_food_path = /obj/item/food/spaghetti/boilednoodles
ingredient_reagent_multiplier = 0
+// Space Ramen
+/datum/chemical_reaction/food/soup/beef_ramen
+ required_reagents = list(
+ /datum/reagent/consumable/beef_flavour = 5
+ )
+ required_ingredients = list(
+ /obj/item/food/spaghetti/ramen_dry = 1
+ )
+ resulting_food_path = /obj/item/food/spaghetti/ramen_beef
+ ingredient_reagent_multiplier = 0
+
// Dashi Broth
/datum/reagent/consumable/nutriment/soup/dashi
name = "Dashi"
diff --git a/code/modules/hydroponics/grown/cotton.dm b/code/modules/hydroponics/grown/cotton.dm
index c2149b7a3300..ea160511b973 100644
--- a/code/modules/hydroponics/grown/cotton.dm
+++ b/code/modules/hydroponics/grown/cotton.dm
@@ -1,6 +1,6 @@
/obj/item/seeds/cotton
name = "cotton seed pack"
- desc = "A pack of seeds that'll grow into a cotton plant. Assistants make good free labor if neccesary."
+ desc = "A pack of seeds that'll grow into a cotton plant."
icon_state = "seed-cotton"
species = "cotton"
plantname = "Cotton"
diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm
index a35b938fcf66..1eb888dc1cd7 100644
--- a/code/modules/hydroponics/hydroitemdefines.dm
+++ b/code/modules/hydroponics/hydroitemdefines.dm
@@ -174,6 +174,7 @@
. = ..()
AddComponent(/datum/component/butchering, speed = 9 SECONDS, effectiveness = 105)
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
+ AddComponent(/datum/component/walking_aid)
/obj/item/scythe/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is beheading [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 2e9b24675f17..46dfeaea46c3 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -52,8 +52,6 @@
var/datum/weakref/lastuser
///If the tray generates nutrients and water on its own
var/self_sustaining = FALSE
- ///If the tray is currently able to self-sustain
- var/can_self_sustain = TRUE
///The icon state for the overlay used to represent that this tray is self-sustaining.
var/self_sustaining_overlay_icon_state = "gaia_blessing"
///Whether the plant is currently being pollinated or polinating the nearby plants
@@ -68,10 +66,8 @@
var/plant_offset_y = 0
///Suffix things
var/alt_tray = FALSE
- var/indicatorsuffix = ""
///Soil things
- var/obj/machinery/hydroponics/soil/current_soil = null
- var/current_soil_overlay = null
+ var/obj/machinery/hydroponics/soil/current_soil
/obj/machinery/hydroponics/Initialize(mapload)
//ALRIGHT YOU DEGENERATES. YOU HAD REAGENT HOLDERS FOR AT LEAST 4 YEARS AND NONE OF YOU MADE HYDROPONICS TRAYS HOLD NUTRIENT CHEMS INSTEAD OF USING "Points".
@@ -169,12 +165,10 @@
return NONE
/obj/machinery/hydroponics/constructable
- name = "hydroponics tray"
icon = 'icons/obj/service/hydroponics/equipment.dmi'
icon_state = "hydrotray3"
/obj/machinery/hydroponics/constructable/oldstyle
- name = "hydroponics tray"
icon = 'icons/obj/service/hydroponics/equipment.dmi'
icon_state = "hydrotray3-alt"
alt_tray = TRUE
@@ -191,9 +185,6 @@
AddComponent(/datum/component/usb_port, typecacheof(list(/obj/item/circuit_component/hydroponics), only_root_path = TRUE))
AddComponent(/datum/component/fishing_spot, /datum/fish_source/hydro_tray)
-/obj/machinery/hydroponics/constructable/on_deconstruction(disassembled)
- current_soil?.forceMove(drop_location())
-
/obj/machinery/hydroponics/constructable/RefreshParts()
. = ..()
var/tmp_capacity = 0
@@ -247,21 +238,27 @@
if(myseed)
QDEL_NULL(myseed)
remove_shared_particles(/particles/pollen)
+ QDEL_NULL(current_soil)
return ..()
/obj/machinery/hydroponics/Exited(atom/movable/gone)
. = ..()
if(!QDELETED(src) && gone == myseed)
set_seed(null, FALSE)
- if(!istype(gone, /obj/item/mob_holder/snail))
return
- var/obj/item/mob_holder/snail_object = gone
- if(snail_object.held_mob)
- UnregisterSignal(snail_object.held_mob, list(
- COMSIG_LIVING_DEATH,
- COMSIG_MOVABLE_ATTEMPTED_MOVE,
- ))
- QDEL_NULL(our_snail)
+ if(gone == current_soil)
+ current_soil = null
+ if(!QDELETED(src))
+ update_appearance()
+ return
+ if(istype(gone, /obj/item/mob_holder/snail))
+ var/obj/item/mob_holder/snail_object = gone
+ if(snail_object.held_mob)
+ UnregisterSignal(snail_object.held_mob, list(
+ COMSIG_LIVING_DEATH,
+ COMSIG_MOVABLE_ATTEMPTED_MOVE,
+ ))
+ QDEL_NULL(our_snail)
/obj/machinery/hydroponics/constructable/screwdriver_act(mob/living/user, obj/item/tool)
return default_deconstruction_screwdriver(user, tool)
@@ -474,8 +471,19 @@
/obj/machinery/hydroponics/update_name(updates)
. = ..()
- if(!GetComponent(/datum/component/rename) && myseed)
- name = "[initial(name)] ([myseed.plantname])"
+ if(GetComponent(/datum/component/rename))
+ return
+ name = current_soil ? "botanic tray" : initial(name)
+ if(myseed)
+ name += " ([myseed.plantname])"
+
+/obj/machinery/hydroponics/update_desc(updates)
+ . = ..()
+ if(GetComponent(/datum/component/rename))
+ return
+ desc = initial(desc)
+ if(current_soil)
+ desc += " Filled with [current_soil.name]."
/obj/machinery/hydroponics/update_overlays()
. = ..()
@@ -483,8 +491,9 @@
. += myseed.get_tray_overlay(age, plant_status, plant_offset_y)
. += update_status_light_overlays()
- if(current_soil && current_soil_overlay)
- . += mutable_appearance(icon, current_soil_overlay, OBJ_LAYER + 0.001)
+ if(current_soil)
+ var/soil_overlay = "[current_soil.icon_state]_tray"
+ . += mutable_appearance(icon, soil_overlay, OBJ_LAYER + 0.001)
if(self_sustaining && self_sustaining_overlay_icon_state)
. += mutable_appearance(icon, self_sustaining_overlay_icon_state, OBJ_LAYER + 0.002)
@@ -492,8 +501,7 @@
/obj/machinery/hydroponics/proc/update_status_light_overlays()
. = list()
- if(alt_tray)
- indicatorsuffix = "-alt"
+ var/indicatorsuffix = alt_tray ? "-alt" : ""
if(waterlevel <= 10)
. += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3[indicatorsuffix]")
. += emissive_appearance(icon, "over_lowwater3[indicatorsuffix]", src, alpha = src.alpha)
@@ -1101,28 +1109,21 @@
return
if(!isnull(current_soil))
- balloon_alert(user, "tray is full")
+ balloon_alert(user, "tray is full!")
return
balloon_alert(user, "filling the tray...")
if(!do_after(user, 2 SECONDS, src))
return
- if(!oursoil.stored_soil)
- balloon_alert(user, "sack is empty!")
- return
-
- current_soil = new oursoil.stored_soil(src)
+ oursoil.transfer_soil(src, inside_tray = TRUE)
RefreshParts()
tray_flags = current_soil.tray_flags
- current_soil_overlay = "[current_soil.icon_state]_tray"
- name = "botanic tray"
- desc = "A basin used to grow plants in. Filled with [current_soil.name]."
qdel(oursoil)
update_appearance()
- return
+ return TRUE
else
return ..()
@@ -1166,10 +1167,6 @@
update_use_power(NO_POWER_USE)
return CLICK_ACTION_BLOCKING
- if(!can_self_sustain)
- balloon_alert(user, "no self-sustain mode!")
- return CLICK_ACTION_BLOCKING
-
set_self_sustaining(!self_sustaining)
to_chat(user, span_notice("You [self_sustaining ? "activate" : "deactivated"] [src]'s autogrow function[self_sustaining ? ", maintaining the tray's health while using high amounts of power" : ""]."))
return CLICK_ACTION_SUCCESS
diff --git a/code/modules/hydroponics/soil.dm b/code/modules/hydroponics/soil.dm
index 315a773ca860..fb343bb29d08 100644
--- a/code/modules/hydroponics/soil.dm
+++ b/code/modules/hydroponics/soil.dm
@@ -150,6 +150,12 @@
animate(time = 100 MILLISECONDS, pixel_z = 0, easing = QUAD_EASING | EASE_IN)
animate(time = 250 MILLISECONDS, pixel_x = rand(-6, 6), pixel_y = rand(-4, 4), flags = ANIMATION_PARALLEL)
+/obj/item/soil_sack/Exited(atom/movable/gone)
+ . = ..()
+ if(gone == stored_soil)
+ stored_soil = null
+ qdel(src)
+
/obj/item/soil_sack/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!isopenturf(interacting_with) || isgroundlessturf(interacting_with))
return ..()
@@ -161,19 +167,24 @@
if(!do_after(user, 1 SECONDS, interacting_with))
return ITEM_INTERACT_BLOCKING
+ transfer_soil(interacting_with)
+ return ITEM_INTERACT_SUCCESS
+
+//Proc responsible for placing the soil inside track onto the turf or inside a hydroponic tray
+/obj/item/soil_sack/proc/transfer_soil(atom/target, inside_tray = FALSE)
if(ispath(stored_soil))
stored_soil = new stored_soil(src)
+ if(inside_tray)
+ STOP_PROCESSING(SSmachines, stored_soil)
stored_soil.reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, stored_soil.maxnutri / 2)
stored_soil.waterlevel = stored_soil.maxwater
- else
+ else if(!inside_tray)
START_PROCESSING(SSmachines, stored_soil)
-
- stored_soil.forceMove(interacting_with)
- playsound(stored_soil, placement_sound, 65, vary = TRUE)
- stored_soil.on_place()
- qdel(src)
- return ITEM_INTERACT_SUCCESS
+ playsound(target, placement_sound, 65, vary = TRUE)
+ if(!inside_tray)
+ stored_soil.on_place()
+ stored_soil.forceMove(target) //stored_soil is set to null at this point, and the soil sack is deleted when that happens
/obj/item/soil_sack/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
if(attack_type == OVERWHELMING_ATTACK)
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
index 5e67c8de35bf..f73ab5f588c6 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
@@ -340,6 +340,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
/// The icon which appears over the mob holding the item
var/shield_icon = "shield-red"
+/obj/item/nullrod/staff/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
/obj/item/nullrod/staff/worn_overlays(mutable_appearance/standing, isinhands)
. = ..()
if(isinhands)
@@ -629,6 +633,7 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
force_unwielded = 14, \
force_wielded = 18, \
)
+ AddComponent(/datum/component/walking_aid)
/obj/item/nullrod/bostaff/update_icon_state()
icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]"
@@ -703,6 +708,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
sharpness = SHARP_EDGED
menu_description = "A sharp pitchfork. Can be worn on the back."
+/obj/item/nullrod/pitchfork/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
// Egyptian Staff - Used as a tool for making mummy wraps.
/obj/item/nullrod/egyptian
@@ -720,6 +729,11 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
attack_verb_simple = list("bash", "smack", "whack")
menu_description = "A staff. Can be used as a tool to craft exclusive egyptian items. Easily stored. Can be worn on the back."
+
+/obj/item/nullrod/egyptian/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
// Hypertool - It does brain damage rather than normal damage.
/obj/item/nullrod/hypertool
@@ -760,6 +774,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
hitsound = 'sound/items/weapons/bladeslice.ogg'
menu_description = "A pointy spear which penetrates armor a little. Can be worn only on the belt."
+/obj/item/nullrod/spear/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
// Unholy version of above, since the gamemode is dead in the water
/obj/item/brass_spear
@@ -783,6 +801,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
attack_verb_simple = list("stab", "poke", "slash", "clock")
hitsound = 'sound/items/weapons/bladeslice.ogg'
+/obj/item/brass_spear/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
// Nullblade - For when you really want to feel like rolling dice during combat
/obj/item/nullrod/nullblade
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
index c06f8993b53f..e94ad0d9762f 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm
@@ -81,6 +81,7 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and
AddElement(/datum/element/nullrod_core, chaplain_spawnable = FALSE, rune_remove_line = "TO DUST WITH YE!! AWAY!!") // The implant is the actual item the chappie can select
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) //also good at killing plants
AddComponent(/datum/component/butchering, speed = 3 SECONDS, effectiveness = 125)
+ AddComponent(/datum/component/walking_aid)
/obj/item/vorpalscythe/attack(mob/living/target, mob/living/user, list/modifiers, list/attack_modifiers)
if(ismonkey(target) && !target.mind) //Don't empower from hitting monkeys. Hit a corgi or something, I don't know.
diff --git a/code/modules/library/book.dm b/code/modules/library/book.dm
index 97bd3d99d4fa..2f1bc39436df 100644
--- a/code/modules/library/book.dm
+++ b/code/modules/library/book.dm
@@ -123,6 +123,7 @@
if(!can_read_book(user))
return
+ playsound(user, 'sound/items/handling/paper_pickup.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
user.visible_message(span_notice("[user] opens a book titled \"[book_data.title]\" and begins reading intently."))
credit_book_to_reader(user)
display_content(user)
diff --git a/code/modules/logging/categories/log_category_game.dm b/code/modules/logging/categories/log_category_game.dm
index fced988c1fc4..a5be914e6d25 100644
--- a/code/modules/logging/categories/log_category_game.dm
+++ b/code/modules/logging/categories/log_category_game.dm
@@ -66,3 +66,7 @@
category = LOG_CATEGORY_GAME_BLOOD_WORM
config_flag = /datum/config_entry/flag/log_blood_worm
master_category = /datum/log_category/game
+
+/datum/log_category/minimap_drawing
+ category = LOG_CATEGORY_GAME_MINIMAP_DRAWING
+ master_category = /datum/log_category/game
diff --git a/code/modules/minimap/_minimap.dm b/code/modules/minimap/_minimap.dm
new file mode 100644
index 000000000000..e864b080f560
--- /dev/null
+++ b/code/modules/minimap/_minimap.dm
@@ -0,0 +1,45 @@
+GLOBAL_ALIST_EMPTY(minimap_blip_tags)
+GLOBAL_ALIST_EMPTY(minimap_annotations)
+GLOBAL_LIST_EMPTY(minimap_annotation_viewers)
+
+/// Create a minimap blip on the z-level in question, object is optional, and will tie the blip to the object in question, and will clean up itself if the object in question is deleted
+/proc/add_minimap_blip(atom/object, tag, icon_state, icon = 'icons/ui_icons/minimap/map_blips.dmi', large = FALSE, layer = 12)
+ if(!istype(object) || !tag || !icon_state)
+ CRASH("Invalid params passed in to add_minimap_blip")
+ var/atom/movable/screen/minimap_element/blip/new_blip = new(null, null, object, icon_state, icon, large, tag)
+ new_blip.layer = layer
+ LAZYADD(GLOB.minimap_blip_tags[tag], new_blip)
+ SEND_GLOBAL_SIGNAL(COMSIG_MINIMAP_ADD(tag), new_blip)
+
+/proc/get_minimap_blip(tag, atom/object)
+ if(!tag || !istype(object))
+ return
+ for(var/atom/movable/screen/minimap_element/blip/blip as anything in GLOB.minimap_blip_tags[tag])
+ if(blip.track_target == object)
+ return blip
+
+/proc/remove_minimap_blip(tag, atom/object)
+ var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(tag, object)
+ if(isnull(blip))
+ return
+ SEND_GLOBAL_SIGNAL(COMSIG_MINIMAP_REMOVE(tag), blip)
+ LAZYREMOVE(GLOB.minimap_blip_tags[tag], blip)
+ qdel(blip)
+
+/// Returns minimap blips matching a tag that are within `distance` tiles of `target_turf` on the same z-level.
+/proc/get_minimap_blips_in_area(tag, turf/target_turf, distance)
+ if(!tag || isnull(target_turf) || !isnum(distance) || !islist(GLOB.minimap_blip_tags[tag]))
+ return list()
+
+ var/list/nearby_blips = list()
+ for(var/atom/movable/screen/minimap_element/blip/blip as anything in GLOB.minimap_blip_tags[tag])
+ var/turf/blip_turf = get_turf(blip.track_target)
+ if(isnull(blip_turf))
+ continue
+ if(blip_turf.z != target_turf.z)
+ continue
+ if(get_dist(blip_turf, target_turf) > distance)
+ continue
+ nearby_blips += blip
+
+ return nearby_blips
diff --git a/code/modules/minimap/hud/minimal_z_level.dm b/code/modules/minimap/hud/minimal_z_level.dm
new file mode 100644
index 000000000000..af6071b49dc1
--- /dev/null
+++ b/code/modules/minimap/hud/minimal_z_level.dm
@@ -0,0 +1,68 @@
+/atom/movable/screen/minimap_z_indicator
+ icon = 'icons/hud/screen_ai.dmi'
+ icon_state = "zindicator"
+ screen_loc = "BOTTOM+4,RIGHT"
+
+/atom/movable/screen/minimap_z_indicator/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ if(!isnull(hud_owner.mymob))
+ RegisterSignal(hud_owner.mymob, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_level_change))
+ RegisterSignal(hud_owner.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, PROC_REF(on_minimap_change_request))
+ var/current_turf = get_turf(hud_owner.mymob)
+ on_z_level_change(null, current_turf, FALSE)
+
+///sets the currently indicated relative floor
+/atom/movable/screen/minimap_z_indicator/proc/on_z_level_change(turf/old_turf, turf/new_turf, same_z_layer)
+ SIGNAL_HANDLER
+ var/used_z = get_displayed_z_level(new_turf)
+ set_floor_text(used_z)
+
+/atom/movable/screen/minimap_z_indicator/proc/on_minimap_change_request(mob/hud_owner, new_z_change)
+ SIGNAL_HANDLER
+ var/atom/movable/screen/minimap_display/current_display = hud?.screen_objects[HUD_TAC_MINIMAP]
+ if(isnull(current_display))
+ var/current_turf = get_turf(hud_owner)
+ on_z_level_change(null, current_turf, FALSE)
+ return
+ var/current_z = current_display.get_viewed_z_level()
+ if(isnull(current_z))
+ return
+ var/requested_z = current_display.get_clamped_connected_z(current_z + new_z_change, current_z)
+ set_floor_text(requested_z)
+
+/atom/movable/screen/minimap_z_indicator/proc/get_displayed_z_level(turf/current_turf)
+ var/atom/movable/screen/minimap_display/current_display = hud?.screen_objects[HUD_TAC_MINIMAP]
+ if(!isnull(current_display?.fixed_z_level))
+ return current_display.minimap?.z
+ return current_turf?.z
+
+/atom/movable/screen/minimap_z_indicator/proc/set_floor_text(used_z)
+ if(isnull(used_z))
+ return
+ var/bottom_z = used_z
+ while(SSmapping.multiz_levels?[bottom_z]?[Z_LEVEL_DOWN]) // just keep going down
+ bottom_z--
+ var/text = "Floor
[(used_z - bottom_z) + 1]"
+ maptext = MAPTEXT_TINY_UNICODE("[text]
")
+
+/atom/movable/screen/minimap_z_up
+ name = "go up"
+ icon = 'icons/hud/screen_ai.dmi'
+ icon_state = "up"
+ mouse_over_pointer = MOUSE_HAND_POINTER
+ screen_loc = "BOTTOM+4,RIGHT-1"
+
+/atom/movable/screen/minimap_z_up/Click(location, control, params)
+ flick("uppressed", src)
+ SEND_SIGNAL(hud.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, 1) // +1 z level
+
+/atom/movable/screen/minimap_z_down
+ name = "go down"
+ icon = 'icons/hud/screen_ai.dmi'
+ icon_state = "down"
+ mouse_over_pointer = MOUSE_HAND_POINTER
+ screen_loc = "BOTTOM+4,RIGHT-1"
+
+/atom/movable/screen/minimap_z_down/Click(location, control, params)
+ flick("downpressed", src)
+ SEND_SIGNAL(hud.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, -1) // -1 z level
diff --git a/code/modules/minimap/hud/minimap_dimmer.dm b/code/modules/minimap/hud/minimap_dimmer.dm
new file mode 100644
index 000000000000..bdd759d86cc5
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_dimmer.dm
@@ -0,0 +1,7 @@
+/// Fullscreen dimmer used while the tactical minimap is open.
+/// Rendered below HUD planes so minimap and normal HUD icons remain undimmed.
+/atom/movable/screen/fullscreen/dimmer/minimap
+ alpha = 180
+ plane = FULLSCREEN_PLANE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ clear_with_screen = TRUE
diff --git a/code/modules/minimap/hud/minimap_display.dm b/code/modules/minimap/hud/minimap_display.dm
new file mode 100644
index 000000000000..611bb51f30f0
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_display.dm
@@ -0,0 +1,585 @@
+#define MINIMAP_LABEL_REMOVE_PIXEL_RANGE 5
+
+/// Screen object that renders a [/datum/minimap] base map icon on the HUD.
+/atom/movable/screen/minimap_display
+ name = "Minimap"
+ icon_state = ""
+ layer = MINIMAP_IMAGE_LAYER
+ screen_loc = "1,1"
+ var/list/origin_px
+ var/atom/movable/screen/minimap_element/drawing/drawing
+ /// Optional group tag. Displays with the same tag share drawings and labels.
+ var/annotation_share_tag
+ /// Unified list of currently visible minimap elements (drawings, labels, blips, screentip).
+ var/list/atom/movable/screen/minimap_element/visible_minimap_elements = list()
+ /// A reference to the minimap used for this display.
+ var/datum/minimap/minimap
+ /// Screentext in vis_contents used for the maptext.
+ var/atom/movable/screen/minimap_element/label/screentip
+ /// Named blips indexed by name (added via add_blip()). Tagged blips are rebuilt from globals on z-level change.
+ var/list/atom/movable/screen/minimap_element/blip/blips = list()
+ /// The list of minimap blip tags we're going to read from the globalist and listen for additions to
+ var/list/valid_minimap_blip_tags = list()
+ /// fixed z-level to stay on
+ var/fixed_z_level
+ /// Y-axis offset for drawing to account for mouse cursor icon positioning.
+ var/draw_offset_y = -3
+ /// Whether this minimap instance allows drawing and labels.
+ var/can_draw = TRUE
+ /// list of signals we want to keep tied on the hud owner mob
+ var/list/hud_signals = list(
+ COMSIG_MOVABLE_Z_CHANGED = PROC_REF(on_z_level_change),
+ COMSIG_MINIMAP_CHANGE_Z_LEVEL = PROC_REF(z_change_request)
+ )
+ /// Maps HUD key → button type path. Used to create/remove toolbar buttons via [/datum/hud].
+ var/static/list/toolbar_button_types = list(
+ HUD_TAC_MINIMAP_TOOL_RED = /atom/movable/screen/minimap_toolbar_button/draw/red,
+ HUD_TAC_MINIMAP_TOOL_YELLOW = /atom/movable/screen/minimap_toolbar_button/draw/yellow,
+ HUD_TAC_MINIMAP_TOOL_PURPLE = /atom/movable/screen/minimap_toolbar_button/draw/purple,
+ HUD_TAC_MINIMAP_TOOL_BLUE = /atom/movable/screen/minimap_toolbar_button/draw/blue,
+ HUD_TAC_MINIMAP_TOOL_ERASE = /atom/movable/screen/minimap_toolbar_button/erase,
+ HUD_TAC_MINIMAP_TOOL_LABEL = /atom/movable/screen/minimap_toolbar_button/label,
+ HUD_TAC_MINIMAP_TOOL_CLEAR = /atom/movable/screen/minimap_toolbar_button/clear,
+ )
+ /// Currently active toolbar button (the active tool).
+ var/atom/movable/screen/minimap_toolbar_button/active_button = null
+ /// string for the locator blip's tag
+ var/locator_blip_tag = "locator"
+
+/atom/movable/screen/minimap_display/Initialize(mapload, datum/hud/hud_owner, datum/minimap/minimap, list/minimap_blip_tags, initial_fixed_z_level, annotation_share_tag, can_draw = TRUE)
+ src.can_draw = can_draw
+ . = ..()
+ if(isnull(minimap))
+ CRASH("[type] created without a minimap reference!")
+ src.annotation_share_tag = isnull(annotation_share_tag) ? "[type]" : annotation_share_tag
+ LAZYOR(GLOB.minimap_annotation_viewers[src.annotation_share_tag], src)
+ if(!isnull(initial_fixed_z_level))
+ fixed_z_level = initial_fixed_z_level
+ INVOKE_ASYNC(src, PROC_REF(apply_fixed_z_minimap), initial_fixed_z_level)
+ if(length(minimap_blip_tags))
+ valid_minimap_blip_tags = minimap_blip_tags.Copy()
+ set_minimap(minimap)
+ screentip = new
+ show_minimap_element(screentip)
+ if(length(valid_minimap_blip_tags))
+ for(var/blip_tag in valid_minimap_blip_tags)
+ RegisterSignal(SSdcs, COMSIG_MINIMAP_ADD(blip_tag), PROC_REF(on_tagged_blip_add))
+ RegisterSignal(SSdcs, COMSIG_MINIMAP_REMOVE(blip_tag), PROC_REF(on_tagged_blip_remove))
+ on_z_level_change(hud_owner.mymob)
+ show_tagged_blips()
+
+/atom/movable/screen/minimap_display/proc/apply_fixed_z_minimap(target_z)
+ if(QDELETED(src) || isnull(target_z) || fixed_z_level != target_z)
+ return
+ var/datum/minimap/fixed_minimap = get_minimap_for_z(target_z)
+ if(QDELETED(src) || isnull(fixed_minimap) || fixed_z_level != target_z)
+ return
+ set_minimap(fixed_minimap)
+
+/atom/movable/screen/minimap_display/Destroy()
+ set_cursor_icon(null)
+ if(active_button)
+ active_button.on_deactivate()
+ active_button = null
+ var/mob/owner = get_mob()
+ if(hud)
+ for(var/key in toolbar_button_types)
+ hud.remove_screen_object(key, update = FALSE)
+ if(owner)
+ for(var/signal in hud_signals)
+ UnregisterSignal(owner, signal, hud_signals[signal])
+ if(owner?.client)
+ UnregisterSignal(owner.client, COMSIG_CLIENT_MOUSEUP)
+ if(length(GLOB.minimap_annotation_viewers[annotation_share_tag]))
+ GLOB.minimap_annotation_viewers[annotation_share_tag] -= src
+ minimap = null
+ drawing = null
+ visible_minimap_elements.Cut()
+ annotation_share_tag = null
+ QDEL_NULL(screentip)
+ if(length(valid_minimap_blip_tags))
+ for(var/blip_tag in valid_minimap_blip_tags)
+ UnregisterSignal(SSdcs, COMSIG_MINIMAP_ADD(blip_tag))
+ UnregisterSignal(SSdcs, COMSIG_MINIMAP_REMOVE(blip_tag))
+ remove_all_blips()
+
+ return ..()
+
+/atom/movable/screen/minimap_display/set_new_hud(datum/hud/hud_owner)
+ var/mob/old_owner = get_mob()
+ if(hud)
+ for(var/key in toolbar_button_types)
+ hud.remove_screen_object(key, update = FALSE)
+ if(old_owner)
+ for(var/signal in hud_signals)
+ UnregisterSignal(old_owner, signal, hud_signals[signal])
+ if(old_owner?.client)
+ UnregisterSignal(old_owner.client, COMSIG_CLIENT_MOUSEUP)
+ . = ..()
+ var/mob/new_owner = get_mob()
+ if(new_owner)
+ for(var/signal in hud_signals)
+ RegisterSignal(new_owner, signal, hud_signals[signal])
+ if(new_owner?.client)
+ RegisterSignal(new_owner.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(on_client_mouseup))
+ if(can_draw)
+ for(var/hud_key, hud_type in toolbar_button_types)
+ var/atom/movable/screen/minimap_toolbar_button/button = new hud_type(null, hud_owner, src)
+ button.tool_key = hud_key
+ hud_owner.add_screen_object(button, hud_key, HUD_GROUP_STATIC, update_screen = FALSE)
+ reposition_toolbar_buttons()
+ hud_owner.show_hud(hud_owner.hud_version)
+
+/atom/movable/screen/minimap_display/Click(location, control, params)
+ if(..() || usr != get_mob())
+ return
+ var/list/modifiers = params2list(params)
+ var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X))
+ var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y))
+ var/right_click = LAZYACCESS(modifiers, RIGHT_CLICK)
+
+ if(active_button && active_button.on_click(icon_x, icon_y, right_click))
+ return
+
+/atom/movable/screen/minimap_display/proc/remove_nearest_label(icon_x, icon_y, mob/user)
+ var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z)
+ if(!length(labels_for_z))
+ return
+ var/atom/movable/screen/minimap_element/label/nearest
+ var/nearest_dist_sq
+ for(var/atom/movable/screen/minimap_element/label/label as anything in labels_for_z)
+ var/dx = label.pixel_w - icon_x
+ var/dy = label.pixel_z - icon_y
+ var/dist_sq = (dx * dx) + (dy * dy)
+ if(dist_sq > (MINIMAP_LABEL_REMOVE_PIXEL_RANGE * MINIMAP_LABEL_REMOVE_PIXEL_RANGE))
+ continue
+ if(isnull(nearest_dist_sq) || dist_sq < nearest_dist_sq)
+ nearest_dist_sq = dist_sq
+ nearest = label
+ if(isnull(nearest))
+ return
+ labels_for_z -= nearest
+ hide_minimap_element(nearest)
+ qdel(nearest)
+ sync_visible_objects(minimap?.z)
+ log_minimap_drawing("[key_name(user)] removed a minimap label")
+
+/atom/movable/screen/minimap_display/MouseEntered(location, control, params)
+ MouseMove(location, control, params)
+
+/atom/movable/screen/minimap_display/MouseDrag(over_object, src_location, over_location, src_control, over_control, params)
+ if(usr != get_mob())
+ return
+ if(!active_button)
+ return
+ var/list/modifiers = params2list(params)
+ var/list/mouse_px = params2screenpixel(LAZYACCESS(modifiers, SCREEN_LOC))
+ if(length(mouse_px) != 2)
+ return
+ var/x = mouse_px[1] - origin_px[1] + 1
+ var/y = mouse_px[2] - origin_px[2] + 1
+ active_button.on_mouse_drag(x, y)
+
+/atom/movable/screen/minimap_display/proc/on_client_mouseup(client/source)
+ SIGNAL_HANDLER
+ if(active_button)
+ active_button.on_mouse_up()
+
+/atom/movable/screen/minimap_display/MouseMove(location, control, params)
+ if(usr != get_mob())
+ return
+ var/list/modifiers = params2list(params)
+ var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X))
+ var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y))
+
+ var/x = clamp(MINIMAP_ICON_TO_WORLD(icon_x, minimap.min_x), 1, world.maxx)
+ var/y = clamp(MINIMAP_ICON_TO_WORLD(icon_y, minimap.min_y), 1, world.maxy)
+ var/hover_text = get_hover_text(x, y)
+ screentip.maptext = MAPTEXT_TINY_UNICODE("[hover_text]")
+ screentip.pixel_w = icon_x
+ screentip.pixel_z = icon_y
+
+/atom/movable/screen/minimap_display/proc/get_hover_text(x, y)
+ var/closest_blip_name
+ var/closest_blip_distance
+ for(var/atom/movable/screen/minimap_element/blip/blip in visible_minimap_elements)
+ if(isnull(blip.track_target) || blip.track_target.z != minimap.z)
+ continue
+ if(!length(blip.name))
+ continue
+ var/hover_range = blip.large ? 2 : 1
+ var/x_distance = abs(blip.track_target.x - x)
+ var/y_distance = abs(blip.track_target.y - y)
+ if(x_distance > hover_range || y_distance > hover_range)
+ continue
+ var/distance = x_distance + y_distance
+ if(isnull(closest_blip_distance) || distance < closest_blip_distance)
+ closest_blip_distance = distance
+ closest_blip_name = blip.name
+
+ if(!isnull(closest_blip_name))
+ return closest_blip_name
+
+ var/area_name = minimap.map_position_to_name["[x]:[y]"]
+ if(isnull(area_name))
+ var/turf/hovered_loc = locate(x, y, minimap.z)
+ area_name = "[hovered_loc?.loc?.name]"
+ minimap.map_position_to_name["[x]:[y]"] = area_name
+ return area_name
+
+/atom/movable/screen/minimap_display/MouseExited(location, control, params)
+ if(usr != get_mob())
+ return
+ screentip.maptext = ""
+ if(active_button)
+ active_button.on_mouse_up()
+
+/atom/movable/screen/minimap_display/proc/on_z_level_change(mob/source)
+ SIGNAL_HANDLER
+ var/turf/mob_loc = get_turf(source)
+ if(!mob_loc || mob_loc.z != minimap.z)
+ if(isnull(fixed_z_level))
+ INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), mob_loc.z)
+ return
+ remove_blip(locator_blip_tag)
+ return
+ add_blip(locator_blip_tag, "locator", mob_loc.x, mob_loc.y, layer = 15)
+
+
+/atom/movable/screen/minimap_display/proc/resolve_and_change_z_level(new_z)
+ if(isnull(new_z))
+ return
+ var/datum/minimap/new_minimap = get_minimap_for_z(new_z)
+ if(QDELETED(src) || isnull(new_minimap))
+ return
+ change_z_level(new_z, new_minimap)
+
+/atom/movable/screen/minimap_display/proc/change_z_level(new_z, datum/minimap/new_minimap)
+ if(isnull(new_minimap))
+ return
+ if(!isnull(fixed_z_level))
+ fixed_z_level = new_z
+ set_minimap(new_minimap)
+
+/atom/movable/screen/minimap_display/proc/set_fixed_z_level(new_fixed_z, apply_immediately = FALSE)
+ fixed_z_level = new_fixed_z
+ if(apply_immediately)
+ INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), new_fixed_z)
+
+/atom/movable/screen/minimap_display/proc/show_tagged_blips()
+ if(!length(valid_minimap_blip_tags))
+ return
+ for(var/blip_flag in valid_minimap_blip_tags)
+ var/blip_list = GLOB.minimap_blip_tags[blip_flag]
+ if(!blip_list)
+ continue
+ for(var/atom/movable/screen/minimap_element/blip/blip as anything in blip_list)
+ on_tagged_blip_add(null, blip)
+
+/atom/movable/screen/minimap_display/proc/on_tagged_blip_add(datum/source, atom/movable/screen/minimap_element/blip/blip)
+ SIGNAL_HANDLER
+ if(isnull(blip) || QDELETED(blip) || isnull(minimap))
+ return
+ if(!(blip.blip_tag in valid_minimap_blip_tags))
+ return
+ if(isnull(blip.track_target))
+ return
+ var/turf/blip_turf = get_turf(blip.track_target)
+ if(blip_turf?.z != minimap.z)
+ return
+ blip.start_tracking_target()
+ show_minimap_element(blip)
+
+/atom/movable/screen/minimap_display/proc/on_tagged_blip_remove(datum/source, atom/movable/screen/minimap_element/blip/blip)
+ SIGNAL_HANDLER
+ if(isnull(blip))
+ return
+ hide_minimap_element(blip)
+
+/atom/movable/screen/minimap_display/proc/set_minimap(datum/minimap/minimap)
+ if(isnull(minimap))
+ return
+ icon = minimap.base_map
+ var/map_w = minimap.base_map.Width()
+ var/map_h = minimap.base_map.Height()
+ var/screen_size = get_screen_pixel_size()
+ var/map_offset_y = 32
+ screen_loc = "1:[screen_size / 2 - map_w / 2],1:[screen_size / 2 - map_h / 2 - map_offset_y]"
+ origin_px = params2screenpixel(screen_loc)
+ src.minimap = minimap
+ refresh_visible_annotations()
+ screentip?.maptext = ""
+ clear_tagged_blips()
+ show_tagged_blips()
+ reposition_toolbar_buttons()
+
+/// adds a local blip that's not added to the global list
+/atom/movable/screen/minimap_display/proc/add_blip(name, icon_state, x, y, large = FALSE, layer = 12)
+ if(blips[name])
+ return
+ var/atom/movable/screen/minimap_element/blip/new_blip = new(null, null, get_mob(), icon_state, large)
+ new_blip.layer = layer
+ new_blip.start_tracking_target()
+ blips[name] = new_blip
+ show_minimap_element(new_blip)
+
+/atom/movable/screen/minimap_display/proc/update_blip(name, x, y)
+ var/atom/movable/screen/minimap_element/blip/blip = blips[name]
+ if(!blip)
+ return
+ var/half_size = blip.large ? 5 : 3
+ blip.pixel_w = MINIMAP_WORLD_TO_PIXEL(x, minimap.min_x, half_size)
+ blip.pixel_z = MINIMAP_WORLD_TO_PIXEL(y, minimap.min_y, half_size)
+
+/atom/movable/screen/minimap_display/proc/remove_blip(name)
+ var/atom/movable/screen/minimap_element/blip/blip = blips[name]
+ if(!blip)
+ return
+ blips -= name
+ hide_minimap_element(blip)
+ qdel(blip)
+
+/atom/movable/screen/minimap_display/proc/remove_all_blips()
+ for(var/blip_name in blips)
+ var/atom/movable/screen/minimap_element/blip/blip = blips[blip_name]
+ hide_minimap_element(blip)
+ qdel(blip)
+ blips.Cut()
+ hide_visible_elements_by_type(/atom/movable/screen/minimap_element/blip)
+
+/atom/movable/screen/minimap_display/proc/z_change_request(mob/hud_owner, new_z_change)
+ SIGNAL_HANDLER
+ var/current_z = get_viewed_z_level()
+ var/new_z = get_clamped_connected_z(current_z + new_z_change, current_z)
+ INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), new_z)
+
+/atom/movable/screen/minimap_display/proc/get_viewed_z_level()
+ if(!isnull(fixed_z_level))
+ return fixed_z_level
+ return minimap?.z
+
+/atom/movable/screen/minimap_display/proc/get_clamped_connected_z(requested_z, source_z)
+ if(isnull(source_z))
+ return requested_z
+ var/list/connected_levels = SSmapping.get_connected_levels(source_z)
+ if(!length(connected_levels))
+ return requested_z
+ if(requested_z in connected_levels)
+ return requested_z
+ var/closest_z = connected_levels[1]
+ var/closest_distance = abs(closest_z - requested_z)
+ for(var/connected_z in connected_levels)
+ var/current_distance = abs(connected_z - requested_z)
+ if(current_distance < closest_distance)
+ closest_z = connected_z
+ closest_distance = current_distance
+ return closest_z
+
+/// Activates a toolbar button as the active tool.
+/atom/movable/screen/minimap_display/proc/activate_button(atom/movable/screen/minimap_toolbar_button/button)
+ if(!button)
+ return
+
+ // Deselect if clicking the same button
+ if(active_button == button)
+ deactivate_button()
+ return
+
+ deactivate_button()
+
+ active_button = button
+ button.on_activate()
+ update_toolbar_button_states()
+
+/// Deactivates the current tool button.
+/atom/movable/screen/minimap_display/proc/deactivate_button()
+ if(active_button)
+ active_button.on_deactivate()
+ active_button = null
+ update_toolbar_button_states()
+
+/// Sets the mouse cursor icon for the HUD client. Pass null to reset to default.
+/atom/movable/screen/minimap_display/proc/set_cursor_icon(icon/cursor_icon)
+ var/mob/owner = get_mob()
+ if(owner?.client)
+ owner.client.mouse_pointer_icon = cursor_icon
+
+/// Calculates the actual screen pixel size based on the client's view
+/atom/movable/screen/minimap_display/proc/get_screen_pixel_size()
+ var/mob/owner = get_mob()
+ if(!owner?.client)
+ return SCREEN_PIXEL_SIZE // fallback to constant if no client
+ var/list/view_pixels = view_to_pixels(owner.client.view_size.getView())
+ // Return the maximum dimension (typically square, but handle non-square views)
+ return max(view_pixels[1], view_pixels[2])
+
+/atom/movable/screen/minimap_display/proc/update_toolbar_button_states()
+ if(!hud)
+ return
+ for(var/hud_key in toolbar_button_types)
+ var/atom/movable/screen/minimap_toolbar_button/button = hud.screen_objects[hud_key]
+ if(button)
+ button.update_active_state()
+
+/atom/movable/screen/minimap_display/proc/reposition_toolbar_buttons()
+ if(!hud || !minimap)
+ return
+ // origin_px[1] is the minimap's left edge; place toolbar one icon-width to its left.
+ var/btn_x = origin_px[1] - ICON_SIZE_X - 4
+ if(btn_x < 0)
+ btn_x = origin_px[1] + minimap.base_map.Width() + 4 // fall back to right side
+ // Toolbar is vertically centered relative to the minimap's current position.
+ var/screen_size = get_screen_pixel_size()
+ var/toolbar_h = length(toolbar_button_types) * ICON_SIZE_Y
+ var/map_center_y = origin_px[2] + minimap.base_map.Height() / 2
+ var/btn_top_y = clamp(map_center_y + toolbar_h / 2, toolbar_h, screen_size)
+ for(var/key in toolbar_button_types)
+ var/atom/movable/screen/minimap_toolbar_button/button = hud.screen_objects[key]
+ if(button)
+ button.screen_loc = "1:[btn_x],1:[btn_top_y - ICON_SIZE_Y - button.button_slot * ICON_SIZE_Y]"
+
+/atom/movable/screen/minimap_display/proc/clear_canvas(mob/user)
+ if(!can_draw)
+ return
+ drawing.clear_canvas(minimap?.base_map)
+ log_minimap_drawing("[key_name(user)] cleared the minimap canvas on z-level [minimap?.z]")
+ to_chat(user, span_warning("Cleared all minimap drawings."))
+
+/atom/movable/screen/minimap_display/proc/clear_all_annotations(mob/user, annotation_type = /atom/movable/screen/minimap_element/label, annotation_type_name = "label")
+ var/alist/annotation_store = GLOB.minimap_annotations[annotation_share_tag]
+ var/alist/items_by_z = annotation_store?[annotation_type]
+ if(isnull(items_by_z))
+ return
+ var/current_z = get_viewed_z_level()
+ var/list/items = items_by_z[current_z]
+ if(!length(items))
+ return
+ QDEL_LIST(items)
+ items_by_z[current_z] = list()
+ refresh_visible_annotations()
+ sync_visible_objects(current_z)
+ var/user_name = user ? key_name(user) : "System"
+ to_chat(user, span_warning("Cleared all [annotation_type_name] annotations on z-level [current_z]."))
+ log_minimap_drawing("[user_name] has cleared all [annotation_type_name] annotations on z-level [current_z]")
+
+/atom/movable/screen/minimap_display/proc/async_place_label(mob/user, icon_x, icon_y)
+ var/x = clamp(MINIMAP_ICON_TO_WORLD(icon_x, minimap.min_x), 1, world.maxx)
+ var/y = clamp(MINIMAP_ICON_TO_WORLD(icon_y, minimap.min_y), 1, world.maxy)
+ var/area_name = minimap.map_position_to_name["[x]:[y]"]
+ if(isnull(area_name))
+ var/turf/hovered_loc = locate(x, y, minimap.z)
+ area_name = "[hovered_loc?.loc?.name]"
+ minimap.map_position_to_name["[x]:[y]"] = area_name
+ var/label_text = tgui_input_text(user, "What would you like the label at [area_name] to say?", "Add Label", max_length = 25)
+ if(!label_text || QDELETED(src))
+ return
+ var/list/filter_result = is_ic_filtered(label_text)
+ if(filter_result)
+ to_chat(user, span_warning("That label contained a word prohibited in IC chat! Consider reviewing the server rules.\n\"[label_text]\""))
+ SSblackbox.record_feedback("tally", "ic_blocked_words", 1, LOWER_TEXT(config.ic_filter_regex.match))
+ REPORT_CHAT_FILTER_TO_USER(src, filter_result)
+ log_filter("IC", label_text, filter_result)
+ return
+ var/atom/movable/screen/minimap_element/label/new_label = new
+ new_label.icon = 'icons/ui_icons/minimap/map_blips.dmi'
+ new_label.icon_state = "label"
+ new_label.maptext_x = 5
+ new_label.maptext_y = 5
+ new_label.maptext_width = 64
+ new_label.maptext = MAPTEXT_TINY_UNICODE("[label_text]")
+ var/icon/label_icon = icon(new_label.icon, new_label.icon_state)
+ var/half_width = round(label_icon.Width() / 2)
+ var/half_height = round(label_icon.Height() / 2)
+ new_label.pixel_w = MINIMAP_WORLD_TO_PIXEL(x, minimap.min_x, half_width)
+ new_label.pixel_z = MINIMAP_WORLD_TO_PIXEL(y, minimap.min_y, half_height)
+ var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z)
+ labels_for_z += new_label
+ sync_visible_objects(minimap?.z)
+ log_minimap_drawing("[key_name(user)] placed label '[label_text]' at [area_name]")
+
+/atom/movable/screen/minimap_display/proc/get_or_create_annotation_list(annotation_type, z_level)
+ var/alist/annotation_store = GLOB.minimap_annotations[annotation_share_tag]
+ if(isnull(annotation_store))
+ annotation_store = alist()
+ GLOB.minimap_annotations[annotation_share_tag] = annotation_store
+ var/alist/by_z = annotation_store[annotation_type]
+ if(isnull(by_z))
+ by_z = alist()
+ annotation_store[annotation_type] = by_z
+ var/list/items = by_z[z_level]
+ if(isnull(items))
+ items = list()
+ by_z[z_level] = items
+ return items
+
+/atom/movable/screen/minimap_display/proc/refresh_visible_annotations()
+ if(isnull(minimap))
+ return
+ var/list/drawings = get_or_create_annotation_list(/atom/movable/screen/minimap_element/drawing, minimap.z)
+ var/atom/movable/screen/minimap_element/drawing/target_drawing = length(drawings) ? drawings[1] : null
+ if(isnull(target_drawing))
+ target_drawing = new
+ target_drawing.clear_canvas(minimap.base_map)
+ drawings += target_drawing
+ if(drawing != target_drawing)
+ if(!isnull(drawing))
+ hide_minimap_element(drawing)
+ drawing = target_drawing
+ show_minimap_element(drawing)
+
+ hide_visible_elements_by_type(/atom/movable/screen/minimap_element/label)
+ var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z)
+ if(length(labels_for_z))
+ show_minimap_elements(labels_for_z)
+ show_minimap_element(screentip)
+
+/atom/movable/screen/minimap_display/proc/sync_visible_objects(z_level)
+ if(isnull(z_level))
+ return
+ var/list/displays_for_tag = GLOB.minimap_annotation_viewers[annotation_share_tag]
+ if(!length(displays_for_tag))
+ return
+ for(var/atom/movable/screen/minimap_display/display as anything in displays_for_tag)
+ if(QDELETED(display) || display.minimap?.z != z_level)
+ continue
+ display.refresh_visible_annotations()
+
+/atom/movable/screen/minimap_display/proc/show_minimap_element(atom/movable/screen/minimap_element/element)
+ if(isnull(element))
+ return
+ if(!(element in visible_minimap_elements))
+ visible_minimap_elements += element
+ vis_contents |= element
+
+/atom/movable/screen/minimap_display/proc/hide_minimap_element(atom/movable/screen/minimap_element/element)
+ if(isnull(element))
+ return
+ visible_minimap_elements -= element
+ vis_contents -= element
+
+/atom/movable/screen/minimap_display/proc/show_minimap_elements(list/elements)
+ for(var/atom/movable/screen/minimap_element/element as anything in elements)
+ show_minimap_element(element)
+
+/atom/movable/screen/minimap_display/proc/hide_minimap_elements(list/elements)
+ for(var/atom/movable/screen/minimap_element/element as anything in elements)
+ hide_minimap_element(element)
+
+/atom/movable/screen/minimap_display/proc/hide_visible_elements_by_type(element_type)
+ if(isnull(element_type))
+ return
+ for(var/atom/movable/screen/minimap_element/element as anything in visible_minimap_elements.Copy())
+ if(istype(element, element_type))
+ hide_minimap_element(element)
+
+/atom/movable/screen/minimap_display/proc/clear_tagged_blips()
+ for(var/atom/movable/screen/minimap_element/blip/blip as anything in visible_minimap_elements.Copy())
+ if(!length(blip.blip_tag))
+ continue
+ hide_minimap_element(blip)
+
+/atom/movable/screen/minimap_display/nuclear
+ annotation_share_tag = MINIMAP_ANNOTATION_TAG_NUCLEAR
+ valid_minimap_blip_tags = list(MINIMAP_BOMB_BLIP, MINIMAP_NUKEDISK_BLIP, MINIMAP_NUKEOP_BLIP, MINIMAP_NUKEOP_BORG_BLIP, MINIMAP_SYNDICATE_MECH_BLIP, MINIMAP_SYNDIE_TURRET_BLIP, MINIMAP_LADDER_BLIP, MINIMAP_STAIR_BLIP)
+
+#undef MINIMAP_LABEL_REMOVE_PIXEL_RANGE
diff --git a/code/modules/minimap/hud/minimap_drawing.dm b/code/modules/minimap/hud/minimap_drawing.dm
new file mode 100644
index 000000000000..11c81089c85a
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_drawing.dm
@@ -0,0 +1,59 @@
+
+/atom/movable/screen/minimap_element/drawing
+ icon = 'icons/ui_icons/minimap/minimap.dmi'
+
+/atom/movable/screen/minimap_element/drawing/proc/clear_canvas(icon/base_map)
+ var/icon/new_icon = icon('icons/ui_icons/minimap/minimap.dmi')
+ if(base_map)
+ new_icon.Scale(base_map.Width(), base_map.Height())
+ icon = new_icon
+
+/atom/movable/screen/minimap_element/drawing/proc/draw_box(box_color, start_x, start_y, end_x, end_y, erase_pixel_range = 0, erase_padding_multiplier = 0)
+ var/icon/new_icon = icon(src.icon)
+ if(!isnull(box_color) || !erase_padding_multiplier)
+ new_icon.DrawBox(box_color, start_x, start_y, end_x, end_y)
+ else
+ var/padding = erase_pixel_range * erase_padding_multiplier
+ new_icon.DrawBox(box_color, start_x - padding, start_y - padding, end_x + padding, end_y + padding)
+ icon = new_icon
+
+// Unapologetically yoinked from /proc/get_line()
+/atom/movable/screen/minimap_element/drawing/proc/draw_line(line_color, x0, y0, x1, y1, erase_pixel_range = 0, erase_padding_multiplier = 0)
+ var/icon/canvas = icon(src.icon)
+ var/current_x = x0
+ var/current_y = y0
+
+ var/x_distance = x1 - current_x
+ var/y_distance = y1 - current_y
+
+ var/abs_x_distance = abs(x_distance)
+ var/abs_y_distance = abs(y_distance)
+
+ var/x_distance_sign = sign(x_distance)
+ var/y_distance_sign = sign(y_distance)
+
+ var/cx = abs_x_distance >> 1
+ var/cy = abs_y_distance >> 1
+
+ // draw the start pixel
+ var/padding = isnull(line_color) && erase_padding_multiplier ? erase_pixel_range * erase_padding_multiplier : 0
+ canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding)
+
+ if(abs_x_distance >= abs_y_distance)
+ for(var/i in 0 to (abs_x_distance - 1))
+ cy += abs_y_distance
+ if(cy >= abs_x_distance)
+ cy -= abs_x_distance
+ current_y += y_distance_sign
+ current_x += x_distance_sign
+ canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding)
+ else
+ for(var/i in 0 to (abs_y_distance - 1))
+ cx += abs_x_distance
+ if(cx >= abs_y_distance)
+ cx -= abs_y_distance
+ current_x += x_distance_sign
+ current_y += y_distance_sign
+ canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding)
+
+ src.icon = canvas
diff --git a/code/modules/minimap/hud/minimap_element.dm b/code/modules/minimap/hud/minimap_element.dm
new file mode 100644
index 000000000000..f146f61bf348
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_element.dm
@@ -0,0 +1,12 @@
+/atom/movable/screen/minimap_element
+ name = "unknown"
+ layer = MINIMAP_LABELS_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ vis_flags = VIS_INHERIT_PLANE
+ /// the tag this blip is associated via in it's stored globalist
+ var/blip_tag = ""
+
+/atom/movable/screen/minimap_element/Destroy()
+ if(blip_tag)
+ LAZYREMOVE(GLOB.minimap_blip_tags[blip_tag], src)
+ return ..()
diff --git a/code/modules/minimap/hud/minimap_label.dm b/code/modules/minimap/hud/minimap_label.dm
new file mode 100644
index 000000000000..e7515ab0c882
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_label.dm
@@ -0,0 +1,6 @@
+
+/atom/movable/screen/minimap_element/label
+ name = "label"
+ maptext = ""
+ maptext_width = 96
+ maptext_height = 96
diff --git a/code/modules/minimap/hud/minimap_toolbar.dm b/code/modules/minimap/hud/minimap_toolbar.dm
new file mode 100644
index 000000000000..c32ecb3979cf
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_toolbar.dm
@@ -0,0 +1,198 @@
+#define MINIMAP_TOOLBAR_ERASE_RANGE 5
+
+/atom/movable/screen/minimap_toolbar_button
+ icon = 'icons/ui_icons/minimap/minimap_buttons.dmi'
+ icon_state = "draw"
+ mouse_over_pointer = MOUSE_HAND_POINTER
+ /// The minimap display this button controls.
+ var/atom/movable/screen/minimap_display/display
+ /// Vertical slot index (0 = topmost). Used by reposition_toolbar_buttons to calculate screen_loc.
+ var/button_slot = 0
+ /// Mouse cursor icon set when this button's tool is active. null = default cursor.
+ var/icon/mouse_icon = null
+ /// Draw color this button represents. null = erase mode.
+ var/draw_color = null
+ /// HUD key for this tool button. Used to track selection state.
+ var/tool_key = null
+ /// Coordinates of the last drag position during drawing.
+ var/last_drag_x
+ var/last_drag_y
+
+/atom/movable/screen/minimap_toolbar_button/Initialize(mapload, datum/hud/hud_owner, atom/movable/screen/minimap_display/minimap_display)
+ . = ..()
+ display = minimap_display
+
+/atom/movable/screen/minimap_toolbar_button/Destroy()
+ display = null
+ return ..()
+
+/atom/movable/screen/minimap_toolbar_button/MouseEntered(location, control, params)
+ add_filter("mouseover", 1, outline_filter(1, COLOR_LIME))
+ if(desc)
+ openToolTip(usr, tip_src = display || src, params = params, title = name, content = desc)
+
+/atom/movable/screen/minimap_toolbar_button/MouseExited(location, control, params)
+ remove_filter("mouseover")
+ if(desc)
+ closeToolTip(usr)
+
+/// Returns TRUE if this button should be shown as active.
+/atom/movable/screen/minimap_toolbar_button/proc/is_active()
+ return display && display.active_button == src
+
+/// Updates the button's visual state to show if it's active.
+/atom/movable/screen/minimap_toolbar_button/proc/update_active_state()
+ if(is_active())
+ add_filter("active", 1, outline_filter(2, COLOR_YELLOW))
+ else
+ remove_filter("active")
+
+/// Called when this button is activated as the active tool.
+/atom/movable/screen/minimap_toolbar_button/proc/on_activate()
+ display?.set_cursor_icon(mouse_icon)
+
+/// Called when this button is deactivated.
+/atom/movable/screen/minimap_toolbar_button/proc/on_deactivate()
+ display?.set_cursor_icon(null)
+ last_drag_x = null
+ last_drag_y = null
+
+/// Called during mouse drag on the map. Override to implement tool behavior.
+/atom/movable/screen/minimap_toolbar_button/proc/on_mouse_drag(x, y)
+ return FALSE
+
+/// Called on mouse up. Override to clean up tool state.
+/atom/movable/screen/minimap_toolbar_button/proc/on_mouse_up()
+ last_drag_x = null
+ last_drag_y = null
+
+/// Called on click. Override to implement click behavior like label placement.
+/atom/movable/screen/minimap_toolbar_button/proc/on_click(icon_x, icon_y, right_click)
+ return FALSE
+
+/atom/movable/screen/minimap_toolbar_button/draw
+ desc = "Left-drag on the map to draw."
+
+/atom/movable/screen/minimap_toolbar_button/draw/on_mouse_drag(x, y)
+ if(isnull(display) || isnull(display.drawing))
+ return FALSE
+ var/icon_width = display.minimap?.base_map?.Width()
+ var/icon_height = display.minimap?.base_map?.Height()
+ if(isnull(icon_width) || isnull(icon_height))
+ return FALSE
+ if(!ISINRANGE(x, 1, icon_width) || !ISINRANGE(y, 1, icon_height))
+ last_drag_x = null
+ last_drag_y = null
+ return FALSE
+
+ if(last_drag_x && last_drag_y)
+ display.drawing.draw_line(draw_color, last_drag_x, last_drag_y + display.draw_offset_y, x, y + display.draw_offset_y, 0, 1)
+ last_drag_x = x
+ last_drag_y = y
+ else
+ display.drawing.draw_box(draw_color, x, y + display.draw_offset_y, x + 1, y + 1 + display.draw_offset_y, 0, 1)
+ last_drag_x = x
+ last_drag_y = y
+ return TRUE
+
+/atom/movable/screen/minimap_toolbar_button/draw/Click(location, control, params)
+ if(usr == get_mob())
+ display?.activate_button(src)
+
+/atom/movable/screen/minimap_toolbar_button/draw/red
+ button_slot = 0
+ color = TACMAP_DRAWING_RED
+ draw_color = TACMAP_DRAWING_RED
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_red.dmi'
+
+/atom/movable/screen/minimap_toolbar_button/draw/yellow
+ button_slot = 1
+ color = TACMAP_DRAWING_YELLOW
+ draw_color = TACMAP_DRAWING_YELLOW
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi'
+
+/atom/movable/screen/minimap_toolbar_button/draw/purple
+ button_slot = 2
+ color = TACMAP_DRAWING_PURPLE
+ draw_color = TACMAP_DRAWING_PURPLE
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi'
+
+/atom/movable/screen/minimap_toolbar_button/draw/blue
+ button_slot = 3
+ color = TACMAP_DRAWING_BLUE
+ draw_color = TACMAP_DRAWING_BLUE
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi'
+
+/// Erase tool — left-drag on the map to erase a line.
+/atom/movable/screen/minimap_toolbar_button/erase
+ icon_state = "erase"
+ button_slot = 4
+ desc = "Left-drag on the map to erase."
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi'
+
+/atom/movable/screen/minimap_toolbar_button/erase/on_mouse_drag(x, y)
+ if(isnull(display) || isnull(display.drawing))
+ return FALSE
+ var/icon_width = display.minimap?.base_map?.Width()
+ var/icon_height = display.minimap?.base_map?.Height()
+ if(isnull(icon_width) || isnull(icon_height))
+ return FALSE
+ if(!ISINRANGE(x, 1, icon_width) || !ISINRANGE(y, 1, icon_height))
+ last_drag_x = null
+ last_drag_y = null
+ return FALSE
+
+ if(last_drag_x && last_drag_y)
+ display.drawing.draw_line(null, last_drag_x, last_drag_y + display.draw_offset_y, x, y + display.draw_offset_y, MINIMAP_TOOLBAR_ERASE_RANGE, 1)
+ last_drag_x = x
+ last_drag_y = y
+ else
+ display.drawing.draw_box(null, x, y + display.draw_offset_y, x + 1, y + 1 + display.draw_offset_y, MINIMAP_TOOLBAR_ERASE_RANGE, 1)
+ last_drag_x = x
+ last_drag_y = y
+ return TRUE
+
+/atom/movable/screen/minimap_toolbar_button/erase/Click(location, control, params)
+ if(usr == get_mob())
+ display?.activate_button(src)
+
+/// Clear tool — removes all drawings and labels.
+/atom/movable/screen/minimap_toolbar_button/clear
+ icon_state = "clear"
+ button_slot = 5
+ desc = "Clear all drawings."
+
+/atom/movable/screen/minimap_toolbar_button/clear/is_active()
+ return FALSE
+
+/atom/movable/screen/minimap_toolbar_button/clear/Click(location, control, params)
+ if(usr == get_mob())
+ display?.clear_canvas(usr)
+
+/// Label tool — click the map to place labels, right-click a label to remove it, right-click this button to clear all labels.
+/atom/movable/screen/minimap_toolbar_button/label
+ icon_state = "label"
+ button_slot = 6
+ desc = "Toggle label mode. Click the map to place a label. Right-click a nearby label on the map to remove it. Right-click this button to clear all labels."
+ mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/label.dmi'
+
+/atom/movable/screen/minimap_toolbar_button/label/on_click(icon_x, icon_y, right_click)
+ if(isnull(display))
+ return FALSE
+ if(right_click)
+ display.remove_nearest_label(icon_x, icon_y, usr)
+ return TRUE
+ // Place label asynchronously
+ INVOKE_ASYNC(display, /atom/movable/screen/minimap_display/proc/async_place_label, usr, icon_x, icon_y)
+ return TRUE
+
+/atom/movable/screen/minimap_toolbar_button/label/Click(location, control, params)
+ if(usr != get_mob())
+ return
+ var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, RIGHT_CLICK))
+ display.clear_all_annotations(usr, /atom/movable/screen/minimap_element/label)
+ else
+ display?.activate_button(src)
+
+#undef MINIMAP_TOOLBAR_ERASE_RANGE
diff --git a/code/modules/minimap/hud/minimap_tracking_blip.dm b/code/modules/minimap/hud/minimap_tracking_blip.dm
new file mode 100644
index 000000000000..d7c2d19433c4
--- /dev/null
+++ b/code/modules/minimap/hud/minimap_tracking_blip.dm
@@ -0,0 +1,90 @@
+/atom/movable/screen/minimap_element/blip
+ icon = 'icons/ui_icons/minimap/map_blips.dmi'
+ /// Is this a large blip? causes different pixel offsets to be applied
+ var/large = FALSE
+ /// Minimap datum for the current z-level this blip is on
+ var/datum/minimap/minimap
+ /// If we are tracking our target or not, to ensure we do not re-register multiple times
+ var/tracking = FALSE
+ /// what target we're essentially owned by, and will cause this blip to cleanup if it gets deleted
+ var/atom/track_target
+ /// Weak reference to connect_containers so container movement updates this blip.
+ var/datum/weakref/connect_ref
+
+/atom/movable/screen/minimap_element/blip/Initialize(mapload, datum/hud/hud_owner, atom/track_target, icon_state, icon, large = FALSE, blip_tag)
+ . = ..()
+ src.icon_state = icon_state
+ src.large = large
+ if(icon)
+ src.icon = icon
+ if(track_target)
+ register_target(track_target)
+ if(blip_tag)
+ src.blip_tag = blip_tag
+
+/atom/movable/screen/minimap_element/blip/Destroy()
+ clear_tracking_signals()
+ return ..()
+
+/atom/movable/screen/minimap_element/blip/proc/register_target(atom/target)
+ if(!isnull(track_target))
+ CRASH("[type] attempted to register [target] while already tracking [track_target].")
+ if(isnull(target))
+ CRASH("[type] attempted to register a null track target.")
+ RegisterSignal(target, COMSIG_QDELETING, TYPE_PROC_REF(/datum, selfdelete), override = TRUE)
+ track_target = target
+ name = target.name
+
+/atom/movable/screen/minimap_element/blip/proc/clear_tracking_signals()
+ tracking = FALSE
+ if(track_target)
+ UnregisterSignal(track_target, list(COMSIG_QDELETING, COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOVABLE_MOVED))
+ track_target.maptext = null
+ track_target = null
+ QDEL_NULL(connect_ref)
+
+/atom/movable/screen/minimap_element/blip/proc/start_tracking_target()
+ if(tracking)
+ return
+ if(isnull(track_target))
+ CRASH("[type] cannot start tracking without a registered target.")
+ RegisterSignal(track_target, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_target_z_changed))
+ RegisterSignal(track_target, COMSIG_MOVABLE_MOVED, PROC_REF(on_tracked_or_container_moved))
+ if(ismovable(track_target))
+ var/static/list/container_connections = list(
+ COMSIG_MOVABLE_MOVED = PROC_REF(on_tracked_or_container_moved),
+ COMSIG_MOVABLE_Z_CHANGED = PROC_REF(on_target_z_changed),
+ )
+ connect_ref = WEAKREF(AddComponent(/datum/component/connect_containers, track_target, container_connections))
+ tracking = TRUE
+ INVOKE_ASYNC(src, PROC_REF(update_minimap))
+
+/atom/movable/screen/minimap_element/blip/proc/update_minimap()
+ var/turf/target_turf = get_turf(track_target)
+ minimap = get_minimap_for_z(target_turf.z)
+ update_blip()
+
+/atom/movable/screen/minimap_element/blip/proc/on_target_z_changed(atom/movable/source, turf/old_turf, turf/new_turf, same_z_layer)
+ SIGNAL_HANDLER
+ if(isnull(track_target))
+ return
+ var/turf/target_turf = get_turf(track_target)
+ if(isnull(minimap) || minimap.z != target_turf?.z)
+ INVOKE_ASYNC(src, PROC_REF(update_minimap))
+ else
+ update_blip()
+
+/atom/movable/screen/minimap_element/blip/proc/on_tracked_or_container_moved(atom/movable/source, atom/old_loc)
+ SIGNAL_HANDLER
+ update_blip()
+
+/atom/movable/screen/minimap_element/blip/proc/update_blip()
+ SIGNAL_HANDLER
+ if(isnull(track_target) || isnull(minimap))
+ return
+ name = track_target.name
+ var/turf/target_turf = get_turf(track_target)
+ var/half_size = large ? 5 : 3
+ pixel_w = MINIMAP_WORLD_TO_PIXEL(target_turf.x, minimap.min_x, half_size)
+ pixel_z = MINIMAP_WORLD_TO_PIXEL(target_turf.y, minimap.min_y, half_size)
+
diff --git a/code/modules/minimap/minimap.dm b/code/modules/minimap/minimap.dm
new file mode 100644
index 000000000000..a893ae13d38a
--- /dev/null
+++ b/code/modules/minimap/minimap.dm
@@ -0,0 +1,84 @@
+/// Assoc list of z-levels to `/datum/minimap` instances.
+GLOBAL_ALIST_EMPTY(minimaps)
+
+/// Represents a minimap for a single Z-level.
+/datum/minimap
+ /// The Z-level this minimap was made for.
+ VAR_FINAL/z
+ /// The icon of the base map itself.
+ var/icon/base_map
+ /// Mapping of x/y coords to area names.
+ var/alist/map_position_to_name = alist()
+ /// Minimum world X coordinate included in the cropped map icon.
+ var/min_x = 1
+ /// Minimum world Y coordinate included in the cropped map icon.
+ var/min_y = 1
+
+/datum/minimap/proc/load_z(z)
+ . = FALSE
+ if(!isnum(z) || z > length(SSmapping.z_list))
+ CRASH("Tried to create minimap for invalid Z-level ([z])")
+
+ base_map = icon('icons/ui_icons/minimap/minimap.dmi')
+ src.z = z
+
+ var/min_x = world.maxx
+ var/min_y = world.maxy
+ var/max_x = 1
+ var/max_y = 1
+
+ map_position_to_name.Cut()
+ for(var/turf/location as anything in Z_TURFS(z))
+ if(location.skip_minimap_rendering || isshuttleturf(location))
+ continue
+ var/area/arealoc = location.loc
+ if(arealoc.skip_minimap_rendering)
+ continue
+ var/x = location.x
+ var/y = location.y
+ min_x = min(min_x, x)
+ min_y = min(min_y, y)
+ max_x = max(max_x, x)
+ max_y = max(max_y, y)
+ if(location.density)
+ base_map.DrawBox(location.tacmap_color, x, y)
+ continue
+ var/atom/movable/alttarget = (locate(/obj/machinery/door) in location) || (locate(/obj/structure/window) in location) || (locate(/obj/structure/fence) in location)
+ if(alttarget)
+ base_map.DrawBox(alttarget.tacmap_color, x, y)
+ continue
+ if(arealoc.tacmap_color)
+ base_map.DrawBox(BlendRGB(location.tacmap_color, arealoc.tacmap_color, 0.5), x, y)
+ continue
+ if(istype(location, /turf/open/floor/engine/hull))
+ var/turf/turf_below = GET_TURF_BELOW(location)
+ var/area/below_area = turf_below?.loc
+ // we'll draw the below area's color but transparent
+ if(below_area?.tacmap_color)
+ var/list/below_color = rgb2num(below_area.tacmap_color)
+ base_map.DrawBox(rgb(below_color[1], below_color[2], below_color[3], 64), x, y)
+ continue
+ base_map.DrawBox(location.tacmap_color, x, y)
+
+ src.min_x = min_x
+ src.min_y = min_y
+
+ base_map.Crop(min_x, min_y, max_x, max_y)
+ base_map.Scale(base_map.Width() * MINIMAP_PIXEL_MULTIPLIER, base_map.Height() * MINIMAP_PIXEL_MULTIPLIER)
+
+ return TRUE
+
+/// Gets the `/datum/minimap` for a Z-level - generating it if it hasn't been yet.
+/proc/get_minimap_for_z(z) as /datum/minimap
+ var/static/generating_minimap = FALSE
+ UNTIL(!generating_minimap)
+
+ if(GLOB.minimaps[z])
+ return GLOB.minimaps[z]
+
+ generating_minimap = TRUE
+ var/datum/minimap/minimap = new
+ if(minimap.load_z(z))
+ GLOB.minimaps[z] = minimap
+ . = minimap
+ generating_minimap = FALSE
diff --git a/code/modules/minimap/minimap_action.dm b/code/modules/minimap/minimap_action.dm
new file mode 100644
index 000000000000..9c25d7bc553e
--- /dev/null
+++ b/code/modules/minimap/minimap_action.dm
@@ -0,0 +1,141 @@
+
+/datum/action/minimap
+ name = "Toggle Minimap"
+ button_icon = 'icons/hud/implants.dmi'
+ button_icon_state = "minimap"
+ /// Mob currently bound to z-change signal handling for auto-hide behavior.
+ var/mob/tracked_owner
+ /// Optional fixed z-level that anchors this action's minimap stack and permission checks.
+ var/fixed_z_level
+ /// Optional minimap blip tags to render on the rewrite minimap display.
+ var/list/minimap_blip_tags = list()
+ /// Optional annotation sharing tag for minimap drawings/labels.
+ var/annotation_share_tag
+ /// Whether this action's minimap display should allow drawing tools.
+ var/can_draw = FALSE
+ /// list of hud elements we add and remove and check for when this action is triggered
+ var/list/huds = list(
+ HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap,
+ HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display,
+ HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator,
+ HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up,
+ HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down
+ )
+
+/datum/action/minimap/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ var/datum/hud/hud = clicker.hud_used
+ // Toggle off if already visible.
+ if(has_minimap_huds(hud))
+ remove_huds(hud)
+ to_chat(clicker, span_notice("Minimap hidden."))
+ return
+
+ if(SEND_SIGNAL(clicker, COMSIG_MINIMAP_ACTION_TRIGGER) & COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL)
+ return
+
+ var/anchor_z = get_anchor_z_level(clicker?.z)
+ if(is_forbidden_minimap_z(anchor_z))
+ to_chat(clicker, span_warning("The minimap cannot be used on this z-level."))
+ clicker.balloon_alert(clicker, "invalid z-level!")
+ return
+ var/display_z = get_opening_display_z_level(anchor_z, clicker?.z)
+
+ var/datum/minimap/minimap = get_minimap_for_z(display_z)
+ if(isnull(minimap))
+ clicker.balloon_alert(clicker, "no minimap generated")
+ return
+ add_huds(hud, minimap, isnull(fixed_z_level) ? null : display_z)
+ to_chat(clicker, span_notice("Minimap shown."))
+
+/datum/action/minimap/Grant(mob/grant_to)
+ . = ..()
+ set_tracked_owner(grant_to)
+
+/datum/action/minimap/Remove(mob/remove_from)
+ set_tracked_owner(null)
+ return ..()
+
+/datum/action/minimap/proc/has_minimap_huds(datum/hud/hud)
+ for(var/element in huds)
+ if(hud.screen_objects[element])
+ return TRUE
+ return FALSE
+
+/datum/action/minimap/proc/get_open_minimap_display(datum/hud/hud)
+ if(isnull(hud) || !has_minimap_huds(hud))
+ return null
+ return hud.screen_objects[HUD_TAC_MINIMAP]
+
+/datum/action/minimap/proc/add_huds(datum/hud/hud, datum/minimap/minimap, initial_display_z_level)
+ for(var/element in huds)
+ var/hud_element_type = huds[element]
+ var/instanced = new hud_element_type(null, hud, minimap, minimap_blip_tags, initial_display_z_level, annotation_share_tag, can_draw)
+ hud.add_screen_object(instanced, element, HUD_GROUP_STATIC, update_screen = TRUE)
+
+/datum/action/minimap/proc/set_tracked_owner(mob/new_owner)
+ if(tracked_owner == new_owner)
+ return
+ if(!isnull(tracked_owner))
+ UnregisterSignal(tracked_owner, COMSIG_MOVABLE_Z_CHANGED)
+ tracked_owner = new_owner
+ if(!isnull(tracked_owner))
+ RegisterSignal(tracked_owner, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_owner_z_changed))
+
+/datum/action/minimap/proc/get_anchor_z_level(current_z_level)
+ return isnull(fixed_z_level) ? current_z_level : fixed_z_level
+
+
+/datum/action/minimap/proc/get_opening_display_z_level(anchor_z_level, current_z_level)
+ var/list/connected_levels = SSmapping.get_connected_levels(anchor_z_level)
+ if(length(connected_levels) && connected_levels.Find(current_z_level))
+ return current_z_level
+ return (length(connected_levels) ? connected_levels[1] : anchor_z_level)
+
+/datum/action/minimap/proc/is_forbidden_minimap_z(z_level)
+ if(isnull(z_level))
+ return FALSE
+ return is_centcom_level(z_level) || is_reserved_level(z_level)
+
+/datum/action/minimap/proc/on_owner_z_changed(atom/movable/source, turf/old_turf, turf/new_turf, same_z_layer)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(handle_owner_z_changed), source, new_turf?.z)
+
+/datum/action/minimap/proc/handle_owner_z_changed(mob/owner_mob, new_z_level)
+ var/datum/hud/owner_hud = owner_mob?.hud_used
+ if(isnull(get_open_minimap_display(owner_hud)))
+ return
+
+ var/anchor_z = get_anchor_z_level(new_z_level)
+ if(is_forbidden_minimap_z(anchor_z))
+ remove_huds(owner_hud)
+ to_chat(owner_mob, span_warning("The minimap closes on this z-level."))
+ return
+ if(isnull(fixed_z_level))
+ return
+
+ var/display_z = get_opening_display_z_level(anchor_z, new_z_level)
+ var/atom/movable/screen/minimap_display/display = get_open_minimap_display(owner_hud)
+ if(isnull(display) || display.get_viewed_z_level() == display_z)
+ return
+ var/datum/minimap/minimap = get_minimap_for_z(display_z)
+ if(QDELETED(src) || isnull(minimap))
+ return
+ display = get_open_minimap_display(owner_hud)
+ if(isnull(display) || display.get_viewed_z_level() == display_z)
+ return
+ display.change_z_level(display_z, minimap)
+
+/datum/action/minimap/nuclear
+ annotation_share_tag = MINIMAP_ANNOTATION_TAG_NUCLEAR
+ huds = list(
+ HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap,
+ HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display/nuclear,
+ HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator,
+ HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up,
+ HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down
+ )
+
+/datum/action/minimap/proc/remove_huds(datum/hud/hud)
+ for(var/element in huds)
+ hud.remove_screen_object(element)
diff --git a/code/modules/minimap/minimap_table.dm b/code/modules/minimap/minimap_table.dm
new file mode 100644
index 000000000000..3af72ea76c47
--- /dev/null
+++ b/code/modules/minimap/minimap_table.dm
@@ -0,0 +1,225 @@
+
+/obj/machinery/minimap_table
+ name = "reconnaissance platform"
+ desc = "This holographic projector displays a constant stream of tactical information, enabling cinematic planning and strategizing. \
+ Currently, it appears to be monitoring a nearby station and tracking key targets for a clandestine operation."
+ icon = 'icons/obj/machines/minimap_table.dmi'
+ icon_state = "off"
+ processing_flags = START_PROCESSING_MANUALLY
+ light_range = 4
+ light_power = 2
+ light_color = LIGHT_COLOR_INTENSE_RED
+ light_system = OVERLAY_LIGHT
+ density = TRUE
+ bound_width = 64
+ SET_BASE_PIXEL(0, 2)
+ /// Hologram icon sheet used for startup/idle/closing projection effects.
+ var/hologram_icon_file = 'icons/obj/machines/minimap_table_hologram.dmi'
+ /// Z-trait used to resolve the minimap level this table displays.
+ var/target_z_trait = ZTRAIT_STATION
+ /// Cached minimap datum currently displayed by this table.
+ var/datum/minimap/minimap
+ /// Users currently viewing the table-projected minimap HUD.
+ var/list/mob/viewers = list()
+ /// Whether the table is currently active and projecting.
+ var/active = FALSE
+ /// Whether startup animation/state is currently in progress.
+ var/startup = FALSE
+ /// Pixel X offset for projected hologram overlays.
+ var/animation_x = -15
+ /// Pixel Y offset for projected hologram overlays.
+ var/animation_y = 18
+ /// Startup/closing animation duration.
+ var/animation_duration = 5.1 SECONDS
+ /// Max distance at which users can interact with the table.
+ var/interactivity_range = 3
+ /// Proximity monitor used to close viewers leaving interaction range.
+ var/datum/proximity_monitor/proximity
+ /// Light helper used for pulsing hologram illumination.
+ var/datum/light_middleman/middleman
+ /// Lowest alpha value used by the pulsing light animation.
+ var/flicker_min_alpha = 150
+ /// HUD elements used by the holotable minimap view.
+ var/list/table_huds = list(
+ HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap,
+ HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display/nuclear,
+ HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator,
+ HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up,
+ HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down,
+ )
+
+/obj/machinery/minimap_table/Initialize(mapload)
+ . = ..()
+ proximity = new(src, interactivity_range)
+
+ if(IS_OVERLAY_LIGHT_SYSTEM(light_system))
+ middleman = new(src, "holotable")
+ RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_pulsate))
+ middleman.being_overriding_light()
+
+/obj/machinery/minimap_table/Destroy(force)
+ for(var/mob/viewer as anything in viewers)
+ remove_table_huds(viewer.hud_used)
+ viewers = null
+ QDEL_NULL(proximity)
+ QDEL_NULL(middleman)
+ minimap = null
+ return ..()
+
+/obj/machinery/minimap_table/post_machine_initialize()
+ . = ..()
+ INVOKE_ASYNC(src, PROC_REF(set_minimap))
+
+/obj/machinery/minimap_table/RangedAttackOn(mob/attacker, list/modifiers)
+ if(get_dist(src, attacker) > interactivity_range)
+ return
+ interact(attacker)
+
+/obj/machinery/minimap_table/interact(mob/user)
+ . = ..()
+ if(!is_operational || isnull(minimap) || isnull(user.hud_used))
+ return FALSE
+ if(!isnull(user.hud_used.screen_objects[HUD_TAC_MINIMAP]))
+ hide_minimap(user)
+ return TRUE
+ if(active)
+ show_minimap(user)
+ return TRUE
+
+ addtimer(CALLBACK(src, PROC_REF(activate), user), animation_duration, TIMER_UNIQUE | TIMER_CLIENT_TIME)
+
+ startup = TRUE
+ play_animation("startup")
+ return TRUE
+
+/obj/machinery/minimap_table/proc/play_animation(icon_state = "startup", duration = animation_duration)
+ var/image/img = image(hologram_icon_file, src, icon_state, ABOVE_MOB_LAYER, dir, animation_x, animation_y)
+ var/image/emissive_img = image(hologram_icon_file, src, icon_state, ABOVE_MOB_LAYER, dir, animation_x, animation_y)
+ emissive_img.plane = EMISSIVE_PLANE
+ emissive_img.color = _EMISSIVE_COLOR_NO_BLOOM(1)
+
+ flick_overlay_global(img, GLOB.clients, duration)
+ flick_overlay_global(emissive_img, GLOB.clients, duration)
+
+/obj/machinery/minimap_table/proc/activate(mob/activator)
+ if(active || !is_operational)
+ return
+ startup = FALSE
+ active = TRUE
+ if(activator && get_dist(src, activator) <= interactivity_range)
+ show_minimap(activator)
+ update_appearance(UPDATE_OVERLAYS)
+ light_pulsate()
+
+/obj/machinery/minimap_table/proc/deactivate()
+ if(!active)
+ return
+ active = FALSE
+ update_appearance(UPDATE_OVERLAYS)
+ var/obj/effect/abstract/main_light = middleman.primary_intercept
+ animate(main_light, time = 1 SECONDS)
+ play_animation("closing")
+
+/obj/machinery/minimap_table/proc/light_pulsate()
+ SIGNAL_HANDLER
+ var/obj/effect/abstract/main_light = middleman.primary_intercept
+ var/matrix/center = matrix()
+ // center it since we're a 2x2 machine
+ center.Translate(16, 0)
+ main_light.transform = center
+ if(!active)
+ return
+
+
+ var/matrix/bigTransform = matrix()
+ var/matrix/smallTransform = matrix()
+ smallTransform.Add(center)
+ bigTransform.Add(center)
+
+ bigTransform.Scale(1.25)
+ smallTransform.Scale(0.75)
+
+ animate(main_light, alpha = flicker_min_alpha, time = 2 SECONDS, loop = -1, easing = SINE_EASING)
+ animate(alpha = 255, time = 2 SECONDS)
+ animate(transform = bigTransform, time = 3 SECONDS, loop = -1, flags = ANIMATION_PARALLEL, easing = SINE_EASING)
+ animate(transform = smallTransform, time = 3 SECONDS)
+
+/obj/machinery/minimap_table/proc/deactive_without_viewers()
+ if(!length(viewers))
+ deactivate()
+
+/obj/machinery/minimap_table/proc/show_minimap(mob/user)
+ add_table_huds(user.hud_used)
+ viewers |= user
+
+/obj/machinery/minimap_table/proc/hide_minimap(mob/user)
+ remove_table_huds(user.hud_used)
+ viewers -= user
+ if(!length(viewers))
+ addtimer(CALLBACK(src, PROC_REF(deactive_without_viewers)), 10 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE)
+
+/obj/machinery/minimap_table/proc/add_table_huds(datum/hud/hud)
+ var/target_z = resolve_target_z()
+ var/allow_draw = can_user_draw(hud?.mymob)
+ for(var/element in table_huds)
+ var/hud_element_type = table_huds[element]
+ var/instanced = new hud_element_type(null, hud, minimap, null, target_z, MINIMAP_ANNOTATION_TAG_NUCLEAR, allow_draw)
+ hud.add_screen_object(instanced, element, HUD_GROUP_STATIC, update_screen = TRUE)
+
+/obj/machinery/minimap_table/proc/can_user_draw(mob/user)
+ return HAS_TRAIT(user, TRAIT_MINIMAP_TABLE_DRAW)
+
+/obj/machinery/minimap_table/proc/remove_table_huds(datum/hud/hud)
+ for(var/element in table_huds)
+ hud.remove_screen_object(element)
+
+/obj/machinery/minimap_table/proc/resolve_target_z()
+ if(isnull(target_z_trait))
+ return null
+ var/list/trait_levels = SSmapping.levels_by_trait(target_z_trait)
+ if(length(trait_levels))
+ var/top_level
+ for(var/level in trait_levels)
+ if(isnull(top_level) || level > top_level)
+ top_level = level
+ return top_level
+ return null
+
+/obj/machinery/minimap_table/proc/set_minimap()
+ var/target_z = resolve_target_z()
+ minimap = get_minimap_for_z(target_z)
+
+/obj/machinery/minimap_table/on_set_is_operational()
+ update_appearance()
+ set_light_on(is_operational)
+
+ if(!is_operational)
+ deactivate()
+
+/obj/machinery/minimap_table/update_overlays()
+ . = ..()
+ if(!is_operational)
+ return
+ . += mutable_appearance(icon, "idle")
+ . += emissive_appearance(icon, "idle", src)
+ if(active)
+ var/holo_state = "idle"
+ var/mutable_appearance/idle = mutable_appearance(hologram_icon_file, holo_state, ABOVE_MOB_LAYER)
+ idle.pixel_x = animation_x
+ idle.pixel_y = animation_y
+ . += idle
+
+ var/mutable_appearance/emissive = emissive_appearance(hologram_icon_file, holo_state, src, ABOVE_MOB_LAYER, effect_type = EMISSIVE_NO_BLOOM)
+ emissive.pixel_x = animation_x
+ emissive.pixel_y = animation_y
+ . += emissive
+
+/obj/machinery/minimap_table/OnProximityExit(atom/movable/gone)
+ if(!active || !ismob(gone))
+ return
+ var/mob/mob_gone = gone
+ var/list/adjacent = orange(interactivity_range, src)
+ if(mob_gone in adjacent)
+ return
+ if(mob_gone in viewers)
+ hide_minimap(gone)
diff --git a/code/modules/mining/equipment/vent_pointer.dm b/code/modules/mining/equipment/vent_pointer.dm
index 4edab185597b..5c276b746ee0 100644
--- a/code/modules/mining/equipment/vent_pointer.dm
+++ b/code/modules/mining/equipment/vent_pointer.dm
@@ -2,7 +2,9 @@
name = "ventpointer"
desc = "A handheld tracking device. It will locate and point to nearby vents. A bit unreliable though."
icon_state = "pinpointer_vent"
- minimum_range = 14 //gotta use them eyes
+ minimum_range = 8 //gotta use them eyes
+ close_range = 12
+ medium_range = 20
/obj/item/pinpointer/vent/scan_for_target()
var/closest_dist = INFINITY
diff --git a/code/modules/mob/living/basic/bots/medbot/medbot.dm b/code/modules/mob/living/basic/bots/medbot/medbot.dm
index 1fc8b9873c7d..31b96e8ebe3c 100644
--- a/code/modules/mob/living/basic/bots/medbot/medbot.dm
+++ b/code/modules/mob/living/basic/bots/medbot/medbot.dm
@@ -488,6 +488,9 @@
/mob/living/basic/bot/medbot/nukie/Initialize(mapload, new_skin)
. = ..()
+ var/datum/action/minimap/nuclear/tacmap_action = new
+ tacmap_action.Grant(src)
+ add_minimap_blip(src, MINIMAP_NUKEOP_BLIP, "mediborg")
RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_DISARMED, PROC_REF(nuke_disarm))
RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_ARMED, PROC_REF(nuke_arm))
RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_DETONATING, PROC_REF(nuke_detonate))
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
index b31e8a982a73..5167982defc4 100644
--- a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
@@ -16,8 +16,8 @@ GLOBAL_LIST_INIT(tendrils, list())
mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING
basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS
mob_size = MOB_SIZE_HUGE
- maxHealth = 1200
- health = 1200
+ maxHealth = 800
+ health = 800
friendly_verb_continuous = "flails at"
friendly_verb_simple = "flail at"
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
index 1a2272fd85b7..1750372e250a 100644
--- a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
@@ -7,7 +7,7 @@
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = FALSE
- cooldown_time = 8 SECONDS
+ cooldown_time = 12 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
projectile_type = /obj/projectile/tentacle_lash
@@ -24,12 +24,12 @@
telegraph.dir = swipe_dir
line_turf = get_step(line_turf, swipe_dir)
- SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+ SLEEP_CHECK_DEATH(1.2 SECONDS, owner)
for (var/swipe_dir in GLOB.cardinals)
shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
- SLEEP_CHECK_DEATH(1.6 SECONDS, owner)
+ SLEEP_CHECK_DEATH(1.8 SECONDS, owner)
for (var/swipe_dir in GLOB.diagonals)
var/turf/open/line_turf = get_step(owner, swipe_dir)
@@ -40,7 +40,7 @@
telegraph.dir = swipe_dir
line_turf = get_step(line_turf, swipe_dir)
- SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+ SLEEP_CHECK_DEATH(1.2 SECONDS, owner)
for (var/swipe_dir in GLOB.diagonals)
shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
@@ -55,7 +55,8 @@
name = "tentacle spike"
icon_state = "tentacle_spike"
pass_flags = PASSTABLE
- damage = 5 // +10 from the grab
+ damage = 10 // +10 from the grab
+ speed = 1
armor_flag = MELEE
range = 7
hit_prone_targets = TRUE
@@ -69,6 +70,8 @@
var/duration = 1.2 SECONDS
/// Damage dealt to targets who get snatched from entering the beam or being hit directly
var/snatch_damage = 10
+ /// How much faster do we retract
+ var/retract_speed = 2.5
/obj/projectile/tentacle_lash/stab
damage = 15
@@ -86,6 +89,21 @@
/obj/projectile/tentacle_lash/Destroy()
QDEL_NULL(tentacle_beam)
+ if (QDELETED(firer) || !isturf(loc))
+ return ..()
+ // Animate tentacle retraction for visual flair
+ var/turf/our_turf = loc
+ var/turf/firer_turf = get_turf(firer)
+ var/obj/effect/abstract/holder = new(firer_turf)
+ holder.icon = icon
+ holder.icon_state = icon_state
+ holder.transform = transform
+ holder.pixel_x = (firer_turf.x - our_turf.x) * ICON_SIZE_X + pixel_x
+ holder.pixel_y = (firer_turf.y - our_turf.y) * ICON_SIZE_Y + pixel_y
+ holder.Beam(firer, "goliath_tentacle", emissive = FALSE)
+ var/anim_time = sqrt((holder.pixel_x / ICON_SIZE_X) ** 2 + (holder.pixel_y / ICON_SIZE_Y) ** 2) / (speed * SSprojectiles.wait * retract_speed)
+ animate(holder, pixel_x = 0, pixel_y = 0, time = anim_time)
+ QDEL_IN_CLIENT_TIME(holder, anim_time)
return ..()
// Don't range out, stop and persist until we're done
@@ -143,13 +161,13 @@
button_icon_state = "spikes_stabbing"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
- cooldown_time = 8 SECONDS
+ cooldown_time = 12 SECONDS
click_to_activate = TRUE
shared_cooldown = NONE
/// Lazy list of references to spike trails
var/list/active_chasers
/// Health percentage threshold at which we send out wide charsers after the main target
- var/wide_chaser_threshold = 0.7
+ var/wide_chaser_threshold = 0.6
/datum/action/cooldown/mob_cooldown/tendril_chaser/Grant(mob/granted_to)
. = ..()
@@ -176,7 +194,7 @@
ResetCooldown()
/datum/action/cooldown/mob_cooldown/tendril_chaser/Activate(atom/target)
- . = ..()
+ disable_cooldown_actions()
var/primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser
if (isliving(owner))
var/mob/living/as_living = owner
@@ -187,6 +205,9 @@
LAZYADD(active_chasers, WEAKREF(chaser))
RegisterSignal(chaser, COMSIG_QDELETING, PROC_REF(on_chaser_destroyed))
playsound(owner, 'sound/effects/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+ SLEEP_CHECK_DEATH(0.6 SECONDS, owner)
+ StartCooldown()
+ enable_cooldown_actions()
/// Remove a spike trail from our list of active trails
/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_chaser_destroyed(atom/chaser)
@@ -195,8 +216,8 @@
/obj/effect/temp_visual/effect_trail/tendril_chaser
duration = 10 SECONDS
- move_speed = 4
- spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril
+ move_speed = 5
+ spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril/chaser
/// Do we spawn spikes around ourselves as well or only on our own turf?
var/area_spawn = FALSE
@@ -211,7 +232,7 @@
if (!area_spawn)
var/turf/spawn_turf = get_turf(src)
if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_turf) && isopenturf(spawn_turf))
- new spawned_effect(spawn_turf)
+ new spawned_effect(spawn_turf, src)
return
for (var/spawn_dir in GLOB.cardinals)
@@ -219,31 +240,31 @@
continue
var/turf/spawn_loc = get_step(src, spawn_dir)
if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_loc) && isopenturf(spawn_loc))
- new spawned_effect(spawn_loc)
+ new spawned_effect(spawn_loc, src)
/obj/effect/temp_visual/emerging_ground_spike/tendril
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "spikes_stabbing"
- duration = 0.7 SECONDS
+ duration = 0.8 SECONDS
position_variance = 3
- impale_damage = 10
+ impale_damage = 15
damage_blacklist_typecache = list(
/mob/living/basic/mining/tendril,
)
impale_wound_bonus = CANT_WOUND
- // Have we hit someone yet?
+ /// Have we hit someone yet?
var/hit_loser = FALSE
-
-/obj/effect/temp_visual/emerging_ground_spike/tendril/single
- icon_state = "spike"
- duration = 1 SECONDS
- harm_delay = 0.25 SECONDS
- position_variance = 5
+ /// For how long after emerging can we hit someone after harm_delay?
+ var/hit_entry = 0.35 SECONDS
/obj/effect/temp_visual/emerging_ground_spike/tendril/impale()
. = ..()
hit_loser |= .
RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+ addtimer(CALLBACK(src, PROC_REF(stop_impaling)), hit_entry)
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/stop_impaling()
+ UnregisterSignal(loc, COMSIG_ATOM_ENTERED)
/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
@@ -255,6 +276,39 @@
playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
hit_loser = TRUE
+/obj/effect/temp_visual/emerging_ground_spike/tendril/single
+ icon_state = "spike"
+ duration = 1.2 SECONDS
+ harm_delay = 0.3 SECONDS
+ hit_entry = 0.45 SECONDS
+ position_variance = 5
+
+/// Subtype which deletes the chaser once it hits the target
+/obj/effect/temp_visual/emerging_ground_spike/tendril/chaser
+ /// Chaser that spawned us
+ var/obj/effect/temp_visual/effect_trail/tendril_chaser/spawner
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/chaser/Initialize(mapload, spawner)
+ . = ..()
+ src.spawner = spawner
+ RegisterSignal(spawner, COMSIG_QDELETING, PROC_REF(on_spawner_destroy))
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/chaser/proc/on_spawner_destroy(datum/source)
+ SIGNAL_HANDLER
+ spawner = null
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/chaser/Destroy()
+ spawner = null
+ return ..()
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/chaser/harm_mob(mob/living/victim)
+ // Don't multihit mobs by spikes from the same chaser
+ if (!spawner)
+ return FALSE
+ . = ..()
+ if (.)
+ qdel(spawner)
+
/datum/action/cooldown/mob_cooldown/tendril_cross_spikes
name = "Cross Spikes"
desc = "Create a wave of spikes around yourself, impaling anyone caught in it."
@@ -263,7 +317,7 @@
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = FALSE
- cooldown_time = 10 SECONDS
+ cooldown_time = 12 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
/// Range in which we create spikes
diff --git a/code/modules/mob/living/basic/pets/dog/corgi.dm b/code/modules/mob/living/basic/pets/dog/corgi.dm
index 447e6b32e34d..0e514005aa5c 100644
--- a/code/modules/mob/living/basic/pets/dog/corgi.dm
+++ b/code/modules/mob/living/basic/pets/dog/corgi.dm
@@ -270,7 +270,8 @@
///Handler for COMSIG_MOB_RETRIEVE_ACCESS
/mob/living/basic/pet/dog/corgi/proc/retrieve_access(mob/accessor, list/player_access)
SIGNAL_HANDLER
- player_access += access_card.GetAccess()
+ if(access_card)
+ player_access += access_card.GetAccess()
///Handles updating any existing overlays for the corgi (such as fashion items) when it changes how it appears, as in, dead or alive.
/mob/living/basic/pet/dog/corgi/proc/on_appearance_change()
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
index d114b6d42a1e..f04fa3aea99d 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
@@ -252,6 +252,8 @@
var/datum/callback/got_disk = CALLBACK(src, PROC_REF(got_disk))
var/datum/callback/display_disk = CALLBACK(src, PROC_REF(display_disk))
AddComponent(/datum/component/nuclear_bomb_operator, got_disk, display_disk)
+ var/obj/item/implant/implanter = SSwardrobe.provide_type(/obj/item/implant/tacmap/nuclear/cayenne, src)
+ implanter.implant(src, null, TRUE)
/mob/living/basic/carp/pet/cayenne/apply_colour()
if (prob(RARE_CAYENNE_CHANCE))
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
index 60288e9c5e74..4e6701bea5bc 100644
--- a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
@@ -87,4 +87,5 @@
var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, wound_bonus = impale_wound_bonus, sharpness = SHARP_POINTY)
+ to_chat(victim, span_userdanger("You've been impaled by \a [src]!"))
return TRUE
diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm
index 292eda9c6ace..478037ccf33e 100644
--- a/code/modules/mob/living/carbon/death.dm
+++ b/code/modules/mob/living/carbon/death.dm
@@ -73,7 +73,7 @@
qdel(organ)
-/mob/living/carbon/spread_bodyparts(drop_bitflags=NONE)
+/mob/living/carbon/spread_bodyparts(drop_bitflags = NONE, gibbed = FALSE)
for(var/obj/item/bodypart/part as anything in get_bodyparts())
if(part.body_zone == BODY_ZONE_CHEST)
continue // never drop this
@@ -84,12 +84,16 @@
for(var/obj/item/organ/leftover in part)
leftover_organs += leftover
- part.drop_limb(TRUE)
+ part.drop_limb(special = gibbed, dismembered = !gibbed)
part.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), rand(1,3), 5)
// any organs that weren't throw out already about need to follow the bodypart out
for(var/obj/item/organ/leftover as anything in leftover_organs)
- leftover.Remove(src, TRUE)
- leftover.bodypart_insert(part)
+ // depending on whether gibbed flag was set changes how the organs are removed,
+ // so just let's be... very careful here and double check everything
+ if(leftover.owner == src)
+ leftover.Remove(src, gibbed)
+ if(leftover.loc != part)
+ leftover.bodypart_insert(part)
/mob/living/carbon/set_suicide(suicide_state) //you thought that box trick was pretty clever, didn't you? well now hardmode is on, boyo.
. = ..()
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 7624d9d11918..49f80dd3319c 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1045,7 +1045,7 @@
/mob/living/carbon/human/species/set_species(datum/species/mrace, icon_update, pref_load, replace_missing)
. = ..()
if(use_random_name)
- fully_replace_character_name(real_name, generate_random_mob_name())
+ fully_replace_character_name(newname = generate_random_mob_name())
///Proc used to make monkey roles able to function like crew, but not be able to shift into humans easily.
/mob/living/carbon/human/proc/crewlike_monkify()
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index a7de4804b76c..022bfc314e82 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -23,7 +23,7 @@
spill_organs(drop_bitflags)
if(drop_bitflags & DROP_BODYPARTS)
- spread_bodyparts(drop_bitflags)
+ spread_bodyparts(drop_bitflags, gibbed = TRUE)
// failsafe for if we fuck up and leave our brain behind. (other organs are replaceable so we can ignore them.)
var/obj/item/organ/brain/brain = get_organ_slot(ORGAN_SLOT_BRAIN)
@@ -83,7 +83,7 @@
* drop_bitflags: (see code/__DEFINES/blood.dm)
* * DROP_BRAIN - Detaches the head from the mob and launches it away from the body
**/
-/mob/living/proc/spread_bodyparts(drop_bitflags=NONE)
+/mob/living/proc/spread_bodyparts(drop_bitflags = NONE, gibbed = FALSE)
return
/// Length of the animation in dust_animation.dmi
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 83561f37f9f4..0b91518a0094 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -374,8 +374,9 @@
now_pushing = FALSE
/mob/living/start_pulling(atom/movable/AM, state, force = pull_force, supress_message = FALSE)
- if(!AM || !src)
+ if(!src)
return FALSE
+ ASSERT(ismovable(AM), "[src] attempted to pull [AM ? "[AM], a nonmovable atom" : "a null object"]")
if(!(AM.can_be_pulled(src, force)))
return FALSE
if(throwing || !(mobility_flags & MOBILITY_PULL))
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index af9b1e6d49fa..1c755a020781 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -920,6 +920,9 @@
..()
var/mob/living/silicon/robot/cyborg = loc
cyborg.remove_faction(FACTION_SILICON) //ai turrets
+ add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "combatborg")
+ var/datum/action/minimap/nuclear/tacmap_action = new
+ tacmap_action.Grant(cyborg)
/obj/item/robot_model/syndicate/remove_module(obj/item/removed_module)
..()
@@ -952,6 +955,13 @@
model_traits = list(TRAIT_PUSHIMMUNE)
hat_offset = list("north" = list(0, 3), "south" = list(0, 3), "east" = list(-1, 3), "west" = list(1, 3))
+/obj/item/robot_model/syndicate_medical/rebuild_modules()
+ ..()
+ var/mob/living/silicon/robot/cyborg = loc
+ add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "mediborg")
+ var/datum/action/minimap/nuclear/tacmap_action = new
+ tacmap_action.Grant(cyborg)
+
/obj/item/robot_model/saboteur
name = "Syndicate Saboteur"
basic_modules = list(
@@ -982,6 +992,13 @@
canDispose = TRUE
var/datum/weakref/thermal_vision_ref
+/obj/item/robot_model/saboteur/rebuild_modules()
+ ..()
+ var/mob/living/silicon/robot/cyborg = loc
+ add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "engiborg")
+ var/datum/action/minimap/nuclear/tacmap_action = new
+ tacmap_action.Grant(cyborg)
+
/datum/action/cooldown/borg_thermal
name = "Toggle Thermal Night Vision"
button_icon = 'icons/mob/actions/actions_mecha.dmi'
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 541734ff8e4a..6c32c7a0e790 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -131,8 +131,8 @@
SEND_SIGNAL(client, COMSIG_CLIENT_MOB_LOGIN, src)
client.init_verbs()
- AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
- AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
diff --git a/code/modules/mod/modules/modules_supply.dm b/code/modules/mod/modules/modules_supply.dm
index 9fe1b8a137bb..976111bd8495 100644
--- a/code/modules/mod/modules/modules_supply.dm
+++ b/code/modules/mod/modules/modules_supply.dm
@@ -576,8 +576,8 @@
/obj/item/mod/module/sphere_transform
name = "MOD sphere transform module"
- desc = "A module able to move the suit's parts around, turning it and the user into a sphere. \
- The sphere can move quickly, even through lava, and launch mining micromissile to decimate terrain and fauna alike."
+ desc = "A module able to move the suit's parts around, turning it and the user into a sphere. If the modsuit is insulated with bileworm skin, the user will be protected from lava while active. \
+ The sphere can move quickly, even through lava, and launch mining micromissiles to decimate terrain and fauna alike."
icon_state = "sphere"
module_type = MODULE_ACTIVE
removable = FALSE
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index 3f9afa610799..9e6230200731 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -39,7 +39,8 @@
/datum/computer_file/program/science/ui_assets(mob/user)
return list(
- get_asset_datum(/datum/asset/spritesheet_batched/research_designs)
+ get_asset_datum(/datum/asset/spritesheet_batched/sheetmaterials),
+ get_asset_datum(/datum/asset/spritesheet_batched/research_designs),
)
// heavy data from this proc should be moved to static data when possible
@@ -168,8 +169,17 @@
var/datum/design/design = SSresearch.techweb_designs[design_id] || SSresearch.error_design
var/compressed_id = "[compress_id(design.id)]"
var/size = spritesheet.icon_size_id(design.id)
+
+ var/cost = list()
+ var/list/materials = design.materials
+ for(var/datum/material/mat in materials)
+ cost[mat.name] = OPTIMAL_COST(materials[mat])
+
design_cache[compressed_id] = list(
design.name,
+ cost,
+ design.build_type,
+ design.departmental_flags,
"[size == size32x32 ? "" : "[size] "][design.id]"
)
@@ -178,10 +188,23 @@
for (var/id in id_cache)
flat_id_cache += id
+ var/list/department_flags = list()
+ for (var/datum/job_department/department as anything in subtypesof(/datum/job_department))
+ if (department::department_bitflags)
+ department_flags["[department::department_bitflags]"] = department::department_name
+
+ // Don't pass away flags as those are irrelevant to the station
+ var/list/build_types = GLOB.build_types_to_string.Copy()
+ build_types -= "[AWAY_IMPRINTER]"
+ build_types -= "[AWAY_LATHE]"
+
.["static_data"] = list(
"node_cache" = node_cache,
"design_cache" = design_cache,
- "id_cache" = flat_id_cache
+ "id_cache" = flat_id_cache,
+ "SHEET_MATERIAL_AMOUNT" = SHEET_MATERIAL_AMOUNT,
+ "build_types" = build_types,
+ "department_flags" = department_flags,
)
/**
diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm
index 3615616f4f39..ccaed1a6996d 100644
--- a/code/modules/projectiles/guns/magic/staff.dm
+++ b/code/modules/projectiles/guns/magic/staff.dm
@@ -11,6 +11,10 @@
/// If FALSE, only wizards or survivalists can use the staff to its full potential - If TRUE, anyone can
var/allow_intruder_use = FALSE
+/obj/item/gun/magic/staff/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/walking_aid)
+
/obj/item/gun/magic/staff/proc/is_wizard_or_friend(mob/user)
if(!HAS_MIND_TRAIT(user, TRAIT_MAGICALLY_GIFTED) && !allow_intruder_use)
return FALSE
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 8d9f0989c1c6..7484372eafb5 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -1368,3 +1368,13 @@
nutriment_factor = 0.5
randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+/datum/reagent/consumable/beef_flavour
+ name = "Beef Space Ramen Flavouring"
+ description = "Powdered beef flavouring with enough salt to preserve a corpse."
+ nutriment_factor = 5
+ color = "#5f3e00" // rgb: 115, 16, 8
+ taste_description = "beef"
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS
+ default_container = /obj/item/reagent_containers/condiment/pack/beef_flavour
+
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 9e73a0be3613..b83cf6ded835 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -1501,7 +1501,7 @@
toxpwr = 0
liver_tolerance_multiplier = 0
silent_toxin = TRUE
- remove_paralysis()
+ remove_paralysis(affected_mob)
need_mob_update += affected_mob.adjust_oxy_loss(-3.5 * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
need_mob_update = affected_mob.adjust_tox_loss(-3.25 * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)
need_mob_update += affected_mob.adjust_brute_loss(-6 * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
diff --git a/code/modules/reagents/reagent_containers/condiment.dm b/code/modules/reagents/reagent_containers/condiment.dm
index 6d015f43760b..e6fd128b3eaf 100644
--- a/code/modules/reagents/reagent_containers/condiment.dm
+++ b/code/modules/reagents/reagent_containers/condiment.dm
@@ -493,7 +493,7 @@
desc = temp_list[3]
else
icon_state = "condi_mixed"
- desc = "A small condiment pack. The label says it contains [originalname]"
+ desc = "A small condiment pack. The label says it contains [originalname]."
//Ketchup
/obj/item/reagent_containers/condiment/pack/ketchup
@@ -541,3 +541,9 @@
originalname = "mayonnaise"
volume = 5
list_reagents = list(/datum/reagent/consumable/mayonnaise = 5)
+
+/obj/item/reagent_containers/condiment/pack/beef_flavour
+ name = "beef space ramen flavouring"
+ originalname = "beef flavour"
+ volume = 5
+ list_reagents = list(/datum/reagent/consumable/beef_flavour = 5)
diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm
index 784cc5688b87..316feffcd93d 100644
--- a/code/modules/research/designs/autolathe/service_designs.dm
+++ b/code/modules/research/designs/autolathe/service_designs.dm
@@ -661,3 +661,20 @@
RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE,
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
+/datum/design/rdd
+ name = "Rapid Decoration Device (RDD)"
+ id = "rdd"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 8,
+ /datum/material/plastic = SHEET_MATERIAL_AMOUNT * 4,
+ /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2,
+ )
+ build_path = /obj/item/construction/rdd/loaded
+ category = list(
+ RND_CATEGORY_INITIAL,
+ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE,
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm
index 60fa59075563..1e2e0de6f02f 100644
--- a/code/modules/research/rdconsole.dm
+++ b/code/modules/research/rdconsole.dm
@@ -189,6 +189,7 @@ Nothing else in the console has ID requirements.
/obj/machinery/computer/rdconsole/ui_assets(mob/user)
return list(
+ get_asset_datum(/datum/asset/spritesheet_batched/sheetmaterials),
get_asset_datum(/datum/asset/spritesheet_batched/research_designs),
)
@@ -317,8 +318,17 @@ Nothing else in the console has ID requirements.
var/datum/design/design = SSresearch.techweb_designs[design_id] || SSresearch.error_design
var/compressed_id = "[compress_id(design.id)]"
var/size = spritesheet.icon_size_id(design.id)
+
+ var/cost = list()
+ var/list/materials = design.materials
+ for(var/datum/material/mat in materials)
+ cost[mat.name] = OPTIMAL_COST(materials[mat])
+
design_cache[compressed_id] = list(
design.name,
+ cost,
+ design.build_type,
+ design.departmental_flags,
"[size == size32x32 ? "" : "[size] "][design.id]"
)
@@ -327,10 +337,23 @@ Nothing else in the console has ID requirements.
for (var/id in id_cache)
flat_id_cache += id
+ var/list/department_flags = list()
+ for (var/datum/job_department/department as anything in subtypesof(/datum/job_department))
+ if (department::department_bitflags)
+ department_flags["[department::department_bitflags]"] = department::department_name
+
+ // Don't pass away flags as those are irrelevant to the station
+ var/list/build_types = GLOB.build_types_to_string.Copy()
+ build_types -= "[AWAY_IMPRINTER]"
+ build_types -= "[AWAY_LATHE]"
+
.["static_data"] = list(
"node_cache" = node_cache,
"design_cache" = design_cache,
"id_cache" = flat_id_cache,
+ "SHEET_MATERIAL_AMOUNT" = SHEET_MATERIAL_AMOUNT,
+ "build_types" = build_types,
+ "department_flags" = department_flags,
)
/obj/machinery/computer/rdconsole/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
diff --git a/code/modules/research/techweb/nodes/service_nodes.dm b/code/modules/research/techweb/nodes/service_nodes.dm
index 8de42207928b..24fe204c7c70 100644
--- a/code/modules/research/techweb/nodes/service_nodes.dm
+++ b/code/modules/research/techweb/nodes/service_nodes.dm
@@ -38,7 +38,11 @@
"water_balloon",
"ticket_machine",
"radio_entertainment",
+<<<<<<< HEAD
"manual_cell_recharger", // MASSMETA EDIT (more_cell_interactions)
+=======
+ "rdd",
+>>>>>>> upstream/master
"photocopier",
)
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index 594094deefd6..79a47f4d2440 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -822,6 +822,8 @@ GLOBAL_LIST_INIT(slime_extract_auto_activate_reactions, init_slime_auto_activate
var/obj/item/implant/radio/syndicate/imp = new(src)
imp.implant(smart_mob, user)
smart_mob.AddComponent(/datum/component/simple_access, list(ACCESS_SYNDICATE, ACCESS_MAINT_TUNNELS))
+ var/obj/item/implant/implanter = SSwardrobe.provide_type(/obj/item/implant/tacmap/nuclear/cayenne, src)
+ implanter.implant(src, null, TRUE)
/obj/item/slimepotion/sentience/nuclear/dangerous_horse
name = "dangerous pony potion"
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 041fe3a0e805..6ed37204e1aa 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -754,7 +754,7 @@
check_wounding(wounding_type, wounding_dmg, wound_bonus, exposed_wound_bonus, attack_direction, damage_source = damage_source, wound_clothing = wound_clothing)
for(var/datum/wound/iter_wound as anything in wounds)
- iter_wound.receive_damage(wounding_type, wounding_dmg, wound_bonus, damage_source)
+ iter_wound.receive_damage(wounding_type, wounding_dmg, wound_bonus, attack_direction, damage_source)
/*
// END WOUND HANDLING
diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm
index c7b0bcce0f61..86eecc8e41af 100644
--- a/code/modules/surgery/organs/internal/heart/_heart.dm
+++ b/code/modules/surgery/organs/internal/heart/_heart.dm
@@ -48,13 +48,11 @@
/obj/item/organ/heart/Remove(mob/living/carbon/heartless, special, movement_flags)
. = ..()
if(!special)
- addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 12 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 12 SECONDS, TIMER_DELETE_ME)
beat = BEAT_NONE
owner?.stop_sound_channel(CHANNEL_HEARTBEAT)
/obj/item/organ/heart/proc/stop_if_unowned()
- if(QDELETED(src))
- return
if(IS_ROBOTIC_ORGAN(src))
return
if(isnull(owner))
diff --git a/code/modules/surgery/surgery_tools.dm b/code/modules/surgery/surgery_tools.dm
index f07c53b81179..7dde72e5fd4a 100644
--- a/code/modules/surgery/surgery_tools.dm
+++ b/code/modules/surgery/surgery_tools.dm
@@ -160,7 +160,8 @@
inhand_icon_state = "drill"
lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
- hitsound = 'sound/items/weapons/circsawhit.ogg'
+ hitsound = 'sound/items/tools/drill_hit.ogg'
+ usesound = 'sound/items/tools/drill_use.ogg'
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT*5, /datum/material/glass = SHEET_MATERIAL_AMOUNT*3)
obj_flags = CONDUCTS_ELECTRICITY
item_flags = SURGICAL_TOOL
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index eed3af70bcc5..b37264b59451 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -39,7 +39,7 @@
/// *Only* run the test provided within the parentheses
/// This is useful for debugging when you want to reduce noise, but should never be pushed
/// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)`
-#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; }
+#define TEST_FOCUS(test_path) ##test_path { test_flags = UNIT_TEST_FOCUS; }
/// Run the test provided within the parentheses run_count times
/// Useful for debugging flaky tests that only fail sometimes
@@ -67,6 +67,16 @@
*/
#define TEST_AFTER_CREATE_AND_DESTROY INFINITY
+// Unit test bitflags
+
+/// If any unit test has this bitflag, only unit tests with UNIT_TEST_FOCUS will run.
+#define UNIT_TEST_FOCUS (1<<0)
+/// This unit test only runs on specially designated unit test maps (Should only ever be one).
+#define UNIT_TEST_DEBUG_MAP_ONLY (1<<1)
+
+#define UNIT_TEST_BASIC (UNIT_TEST_DEBUG_MAP_ONLY)
+#define UNIT_TEST_MAP_TEST (NONE)
+
/// Change color to red on ANSI terminal output, if enabled with -DANSICOLORS.
#ifdef ANSICOLORS
#define TEST_OUTPUT_RED(text) "\x1B\x5B1;31m[text]\x1B\x5B0m"
@@ -212,6 +222,7 @@
#include "keybinding_init.dm"
#include "kinetic_crusher.dm"
#include "knockoff_component.dm"
+#include "language_key_conflicts.dm"
#include "language_transfer.dm"
#include "leash.dm"
#include "lesserform.dm"
diff --git a/code/modules/unit_tests/area_contents.dm b/code/modules/unit_tests/area_contents.dm
index dea3c0a30574..0b5c792561db 100644
--- a/code/modules/unit_tests/area_contents.dm
+++ b/code/modules/unit_tests/area_contents.dm
@@ -1,6 +1,7 @@
/// Verifies that an area's perception of their "turfs" is correct, and no other area overlaps with them
/// Quite slow, but needed
/datum/unit_test/maptest_area_contents
+ test_flags = UNIT_TEST_MAP_TEST
priority = TEST_LONGER
/datum/unit_test/maptest_area_contents/Run()
diff --git a/code/modules/unit_tests/atmospherics_sanity.dm b/code/modules/unit_tests/atmospherics_sanity.dm
index d7348a096d37..f3efb4d0a137 100644
--- a/code/modules/unit_tests/atmospherics_sanity.dm
+++ b/code/modules/unit_tests/atmospherics_sanity.dm
@@ -2,6 +2,7 @@
* This test checks that all areas are connected to their distribution loops
*/
/datum/unit_test/atmospherics_sanity
+ test_flags = UNIT_TEST_MAP_TEST
priority = TEST_LONGER // we iterate over all atmospherics devices on the starting networks
/// List of areas to start crawling from
diff --git a/code/modules/unit_tests/cable_powernets.dm b/code/modules/unit_tests/cable_powernets.dm
index f462dcf5bc96..24da321f5851 100644
--- a/code/modules/unit_tests/cable_powernets.dm
+++ b/code/modules/unit_tests/cable_powernets.dm
@@ -1,5 +1,6 @@
///Checking all powernets to see if they are properly connected and powered.
/datum/unit_test/cable_powernets
+ test_flags = UNIT_TEST_MAP_TEST
/datum/unit_test/cable_powernets/Run()
for(var/datum/powernet/powernets as anything in SSmachines.powernets)
diff --git a/code/modules/unit_tests/cargo_dep_order_locations.dm b/code/modules/unit_tests/cargo_dep_order_locations.dm
index 106a0eb19a76..aa262628d2d7 100644
--- a/code/modules/unit_tests/cargo_dep_order_locations.dm
+++ b/code/modules/unit_tests/cargo_dep_order_locations.dm
@@ -1,4 +1,5 @@
/datum/unit_test/cargo_dep_order_locations
+ test_flags = UNIT_TEST_MAP_TEST
/datum/unit_test/cargo_dep_order_locations/Run()
for(var/datum/job_department/department as anything in SSjob.joinable_departments)
diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm
index 366ebc29c138..76feb7b8b01c 100644
--- a/code/modules/unit_tests/create_and_destroy.dm
+++ b/code/modules/unit_tests/create_and_destroy.dm
@@ -1,5 +1,7 @@
///Delete one of every type, sleep a while, then check to see if anything has gone fucky
/datum/unit_test/create_and_destroy
+ // Since this unit test takes so damn long, we split it up across all runners
+ test_flags = parent_type::test_flags & ~UNIT_TEST_DEBUG_MAP_ONLY
//You absolutely must run after (almost) everything else
priority = TEST_CREATE_AND_DESTROY
@@ -15,6 +17,30 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE)
GLOB.running_create_and_destroy = TRUE
var/list/type_paths_to_check = (valid_typesof(/atom/movable) + valid_typesof(/turf)) - uncreatables // No areas please
+
+ // This code is responsible for splitting up create & destroy across multiple integration tests.
+ var/total_amount_to_check = length(type_paths_to_check)
+ var/runner_count = length(config.maplist)
+
+ var/split_up_amount = floor(total_amount_to_check / runner_count)
+
+ var/what_map_index_are_we = 1
+ for(var/map_name, _map_config in config.maplist)
+ var/datum/map_config/map_config = _map_config
+ if(SSmapping.current_map.map_name == map_config.map_name)
+ break
+ what_map_index_are_we++
+
+ var/start_index = (what_map_index_are_we - 1) * split_up_amount
+ // Instead of super trying to make it an equal split, we just give the remainder tests to the final runner
+ var/end_index = (what_map_index_are_we == runner_count) ? total_amount_to_check : start_index + split_up_amount
+
+ // +1 because byond's list.Copy() implementation is weird
+ type_paths_to_check = type_paths_to_check.Copy(start_index, end_index + 1)
+
+ log_world("Running create and destroy on [length(type_paths_to_check)] atoms out of the [total_amount_to_check] total")
+ log_world("([start_index] [type_paths_to_check[1]]) - ([end_index] [type_paths_to_check[length(type_paths_to_check)]])")
+
for(var/type_path in type_paths_to_check)
if(ispath(type_path, /turf))
spawn_at.ChangeTurf(type_path)
@@ -28,6 +54,8 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE)
else
var/atom/creation = new type_path(spawn_at)
if(QDELETED(creation))
+ // Same as below
+ creation = null
continue
//Go all in
qdel(creation, force = TRUE)
@@ -50,7 +78,7 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE)
var/list/queues_we_care_about = list()
// All of em, I want hard deletes too, since we rely on the debug info from them
- for(var/i in 1 to GC_QUEUE_HARDDELETE)
+ for(var/i in GC_QUEUE_FILTER to GC_QUEUE_HARDDELETE)
queues_we_care_about += i
//Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about
diff --git a/code/modules/unit_tests/dcs_check_list_arguments.dm b/code/modules/unit_tests/dcs_check_list_arguments.dm
index 769574cf95f2..0358ecfc2b90 100644
--- a/code/modules/unit_tests/dcs_check_list_arguments.dm
+++ b/code/modules/unit_tests/dcs_check_list_arguments.dm
@@ -25,6 +25,7 @@
* This unit test requires every (unless ignored) atom to have been created at least once
* for a more accurate search, which is why it's run after create_and_destroy is done running.
*/
+ test_flags = parent_type::test_flags & ~UNIT_TEST_DEBUG_MAP_ONLY
priority = TEST_AFTER_CREATE_AND_DESTROY
/datum/unit_test/dcs_check_list_arguments/Run()
diff --git a/code/modules/unit_tests/firedoor_regions.dm b/code/modules/unit_tests/firedoor_regions.dm
index 6b64a4a72608..0e2dbfc32efe 100644
--- a/code/modules/unit_tests/firedoor_regions.dm
+++ b/code/modules/unit_tests/firedoor_regions.dm
@@ -7,6 +7,7 @@
* Then, checks if every non-ignored region has a fire alarm in it
*/
/datum/unit_test/firedoor_regions
+ test_flags = UNIT_TEST_MAP_TEST
priority = TEST_LONGER
/datum/unit_test/firedoor_regions/Run()
diff --git a/code/modules/unit_tests/language_key_conflicts.dm b/code/modules/unit_tests/language_key_conflicts.dm
new file mode 100644
index 000000000000..8e0af55b122b
--- /dev/null
+++ b/code/modules/unit_tests/language_key_conflicts.dm
@@ -0,0 +1,15 @@
+/// This test ensures that multiple languages aren't mapped to the same prefix key.
+/datum/unit_test/language_key_conflicts
+
+/datum/unit_test/language_key_conflicts/Run()
+ var/list/used_keys = list()
+ for(var/datum/language/language as anything in subtypesof(/datum/language))
+ var/name = language::name
+ var/key = language::key
+ if(!key)
+ TEST_FAIL("[name] ([language]) does not have a prefix!")
+ else if(used_keys[key])
+ var/datum/language/conflicting_language = used_keys[key]
+ TEST_FAIL("[name] ([language]) uses the '[key]' prefix, which is also used by [conflicting_language::name] ([conflicting_language])!")
+ else
+ used_keys[key] = language
diff --git a/code/modules/unit_tests/map_landmarks.dm b/code/modules/unit_tests/map_landmarks.dm
index f81b67aa6672..a454ed447fc4 100644
--- a/code/modules/unit_tests/map_landmarks.dm
+++ b/code/modules/unit_tests/map_landmarks.dm
@@ -1,5 +1,6 @@
/// Tests that [/datum/job/proc/get_default_roundstart_spawn_point] returns a landmark from all joinable jobs.
/datum/unit_test/maptest_job_roundstart_spawnpoints
+ test_flags = UNIT_TEST_MAP_TEST
/datum/unit_test/maptest_job_roundstart_spawnpoints/Run()
for(var/datum/job/job as anything in SSjob.joinable_occupations)
diff --git a/code/modules/unit_tests/mapload_space_verification.dm b/code/modules/unit_tests/mapload_space_verification.dm
index c7c9845956aa..c51ac0ba549d 100644
--- a/code/modules/unit_tests/mapload_space_verification.dm
+++ b/code/modules/unit_tests/mapload_space_verification.dm
@@ -1,6 +1,7 @@
/// Verifies that there are no space turfs inside a station area, or on any planetary z-level. Sometimes, these are introduced during the load of the map and are not present in the DMM itself.
/// Let's just make sure that we have a stop-gap measure in place to catch these if they pop up so we don't put it onto production servers should something errant come up.
/datum/unit_test/maptest_mapload_space_verification
+ test_flags = UNIT_TEST_MAP_TEST
// This test is quite taxing time-wise, so let's run it later than other faster tests.
priority = TEST_LONGER
diff --git a/code/modules/unit_tests/mapping.dm b/code/modules/unit_tests/mapping.dm
index 6b1509ba64e2..ffdd66e2f678 100644
--- a/code/modules/unit_tests/mapping.dm
+++ b/code/modules/unit_tests/mapping.dm
@@ -1,5 +1,6 @@
/// Conveys all log_mapping messages as unit test failures, as they all indicate mapping problems.
/datum/unit_test/maptest_log_mapping
+ test_flags = UNIT_TEST_MAP_TEST
// Happen before all other tests, to make sure we only capture normal mapping logs.
priority = TEST_PRE
diff --git a/code/modules/unit_tests/mapping_nearstation_test.dm b/code/modules/unit_tests/mapping_nearstation_test.dm
index 39898432fa03..6cdfe9be0783 100644
--- a/code/modules/unit_tests/mapping_nearstation_test.dm
+++ b/code/modules/unit_tests/mapping_nearstation_test.dm
@@ -1,5 +1,6 @@
///Detects movables that may have been accidentally placed in space, as well as movables which do not have the proper nearspace area (meaning they aren't lit properly.)
/datum/unit_test/maptest_mapping_nearstation_test
+ test_flags = UNIT_TEST_MAP_TEST
priority = TEST_PRE
/datum/unit_test/maptest_mapping_nearstation_test/Run()
diff --git a/code/modules/unit_tests/orphaned_genturf.dm b/code/modules/unit_tests/orphaned_genturf.dm
index 289b883d2def..ce1d97faec3f 100644
--- a/code/modules/unit_tests/orphaned_genturf.dm
+++ b/code/modules/unit_tests/orphaned_genturf.dm
@@ -1,6 +1,7 @@
/// Ensures we do not leave genturfs sitting around post work
/// They serve as notice to the mapper and have no functionality, but it's good to make note of it here
/datum/unit_test/orphaned_genturf
+ test_flags = UNIT_TEST_MAP_TEST
/datum/unit_test/orphaned_genturf/Run()
for(var/turf/open/genturf/orphaned in ALL_TURFS())
diff --git a/code/modules/unit_tests/required_map_items.dm b/code/modules/unit_tests/required_map_items.dm
index a1b1f516d7f6..4d65d7651e18 100644
--- a/code/modules/unit_tests/required_map_items.dm
+++ b/code/modules/unit_tests/required_map_items.dm
@@ -6,6 +6,7 @@
* - In the type's initialize, REGISTER_REQUIRED_MAP_ITEM() a minimum and maximum
*/
/datum/unit_test/maptest_required_map_items
+ test_flags = UNIT_TEST_MAP_TEST
/// A list of all typepaths that we expect to be in the required items list
var/list/expected_types = list()
diff --git a/code/modules/unit_tests/subsystem_init.dm b/code/modules/unit_tests/subsystem_init.dm
index f5168079a907..3f49b5069e09 100644
--- a/code/modules/unit_tests/subsystem_init.dm
+++ b/code/modules/unit_tests/subsystem_init.dm
@@ -1,5 +1,6 @@
/// Tests that all subsystems that need to properly initialize.
/datum/unit_test/subsystem_init
+ test_flags = UNIT_TEST_MAP_TEST
/datum/unit_test/subsystem_init/Run()
for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems)
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 74f41bf10884..0b922c77e011 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -27,35 +27,37 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/proc/focused_tests()
var/list/focused_tests = list()
for (var/datum/unit_test/unit_test as anything in subtypesof(/datum/unit_test))
- if (initial(unit_test.focus))
+ if (unit_test::test_flags & UNIT_TEST_FOCUS)
focused_tests += unit_test
- return focused_tests.len > 0 ? focused_tests : null
+ return length(focused_tests) ? focused_tests : null
/datum/unit_test
- /// Do not instantiate if type matches this
abstract_type = /datum/unit_test
- //Bit of metadata for the future maybe
- var/list/procs_tested
+ /// Behavior flags for this unit test
+ var/test_flags = UNIT_TEST_BASIC
+ /// The priority of the test, the larger it is the later it fires
+ var/priority = TEST_DEFAULT
+ /// How many times this unit test will run. Use the TEST_REPEAT() macro
+ var/times_to_run = 1
+ // internal shit
+ /// If this test has passed or not
+ var/succeeded = TRUE
/// The bottom left floor turf of the testing zone
var/turf/run_loc_floor_bottom_left
-
/// The top right floor turf of the testing zone
var/turf/run_loc_floor_top_right
- ///The priority of the test, the larger it is the later it fires
- var/priority = TEST_DEFAULT
- //internal shit
- var/focus = FALSE
- var/succeeded = TRUE
+ /// A list of instances created by this unit test. Use allocate()
var/list/allocated
+ /// Lazy list of why this unit test failed.
var/list/fail_reasons
- var/times_to_run = 1
- /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. Stored on the base datum for usability in other relevant tests that need this data.
+ /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy.
+ /// Stored on the base datum for usability in other relevant tests that need this data.
var/static/list/uncreatables = null
-
+ /// Reference to the blank z-level containing our testing enviroment
var/static/datum/space_level/reservation
/proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b)
@@ -66,10 +68,9 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
var/datum/map_template/unit_tests/template = new
reservation = template.load_new_z()
- if (isnull(uncreatables))
- uncreatables = build_list_of_uncreatables()
+ uncreatables ||= build_list_of_uncreatables()
- allocated = new
+ allocated = list()
run_loc_floor_bottom_left = get_turf(locate(/obj/effect/landmark/unit_test_bottom_left) in GLOB.landmarks_list)
run_loc_floor_top_right = get_turf(locate(/obj/effect/landmark/unit_test_top_right) in GLOB.landmarks_list)
@@ -367,15 +368,37 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/proc/RunUnitTests()
CHECK_TICK
- var/list/tests_to_run = subtypesof(/datum/unit_test)
+ // Find our primary unit test map & find out if we are the secondary
+ var/datum/map_config/primary_unit_test_map
+ var/is_secondary_unit_test_map = FALSE
+ var/found_secondary_unit_test_map = FALSE
+ for(var/map_name, _map_config in config.maplist)
+ var/datum/map_config/map_config = _map_config
+ if(map_config.is_unit_test_map)
+ primary_unit_test_map = map_config
+ if(!LAZYLEN(map_config.skipped_tests) && !found_secondary_unit_test_map)
+ found_secondary_unit_test_map = TRUE
+ if(SSmapping.current_map.map_name == map_config.map_name)
+ is_secondary_unit_test_map = TRUE
+
+ var/list/tests_to_run = list()
var/list/focused_tests = list()
- for (var/_test_to_run in tests_to_run)
- var/datum/unit_test/test_to_run = _test_to_run
- if (initial(test_to_run.focus))
- focused_tests += test_to_run
+ for (var/datum/unit_test/potential_test as anything in subtypesof(/datum/unit_test))
+ // If the test has [UNIT_TEST_DEBUG_MAP_ONLY] and we aren't the primary unit test map, skip it.
+ // HOWEVER, some unit tests are incompatible with the primary testing map, so we must offload them a secondary one with no blacklisted tests.
+ if((potential_test::test_flags & UNIT_TEST_DEBUG_MAP_ONLY) && !SSmapping.current_map.is_unit_test_map && \
+ !(primary_unit_test_map.skipped_tests?.Find(potential_test) && is_secondary_unit_test_map) \
+ )
+ continue
+ if (potential_test::test_flags & UNIT_TEST_FOCUS)
+ focused_tests += potential_test
+ continue
+ tests_to_run += potential_test
if(length(focused_tests))
tests_to_run = focused_tests
+ primary_unit_test_map = null // I'm paranoid
+
sortTim(tests_to_run, GLOBAL_PROC_REF(cmp_unit_test_priority))
var/list/test_results = list()
diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm
index 265c43dce651..e008dbca98d2 100644
--- a/code/modules/uplink/uplink_items/nukeops.dm
+++ b/code/modules/uplink/uplink_items/nukeops.dm
@@ -654,7 +654,7 @@
operative team's body-cams. They can also pilot the shuttle remotely and view the station's camera net. \
If you're a meathead who's just here to kill people and don't care about strategising or intel, you'll still have someone to bear witness to your murder-spree!"
item = /obj/item/antag_spawner/nuke_ops/overwatch
- cost = 12
+ cost = 10
purchasable_from = UPLINK_FIREBASE_OPS
// ~~ Disposable Sentry Gun ~~
diff --git a/code/modules/vehicles/mecha/combat/gygax.dm b/code/modules/vehicles/mecha/combat/gygax.dm
index 974ec17818a3..951035fdaa91 100644
--- a/code/modules/vehicles/mecha/combat/gygax.dm
+++ b/code/modules/vehicles/mecha/combat/gygax.dm
@@ -73,6 +73,10 @@
fire = 100
acid = 100
+/obj/vehicle/sealed/mecha/gygax/dark/Initialize(mapload)
+ . = ..()
+ add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech")
+
/obj/vehicle/sealed/mecha/gygax/dark/loaded/Initialize(mapload)
. = ..()
max_ammo()
diff --git a/code/modules/vehicles/mecha/combat/honker.dm b/code/modules/vehicles/mecha/combat/honker.dm
index f5ad579cda19..9c63dc718983 100644
--- a/code/modules/vehicles/mecha/combat/honker.dm
+++ b/code/modules/vehicles/mecha/combat/honker.dm
@@ -60,6 +60,10 @@
MECHA_ARMOR = 2,
)
+/obj/vehicle/sealed/mecha/honker/dark/Initialize(mapload, built_manually)
+ . = ..()
+ add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech")
+
/obj/vehicle/sealed/mecha/honker/dark/loaded
equip_by_category = list(
MECHA_L_ARM = /obj/item/mecha_parts/mecha_equipment/weapon/honker,
diff --git a/code/modules/vehicles/mecha/combat/marauder.dm b/code/modules/vehicles/mecha/combat/marauder.dm
index e0e214242aa3..e316de70bf7b 100644
--- a/code/modules/vehicles/mecha/combat/marauder.dm
+++ b/code/modules/vehicles/mecha/combat/marauder.dm
@@ -161,6 +161,10 @@
fire = 100
acid = 100
+/obj/vehicle/sealed/mecha/marauder/mauler/Initialize(mapload)
+ . = ..()
+ add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech")
+
/obj/vehicle/sealed/mecha/marauder/mauler/loaded
equip_by_category = list(
MECHA_L_ARM = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/lmg,
diff --git a/config/logging.txt b/config/logging.txt
index cd9b38b62039..c116110bd464 100644
--- a/config/logging.txt
+++ b/config/logging.txt
@@ -50,6 +50,9 @@ LOG_MECHA
## log OOC channel
LOG_OOC
+## log minimap drawing
+LOG_MINIMAP_DRAWING
+
## log pda messages
LOG_PDA
diff --git a/config/maps.txt b/config/maps.txt
index f7c597a29122..640244b43977 100644
--- a/config/maps.txt
+++ b/config/maps.txt
@@ -15,6 +15,8 @@ Format:
webmap_url (link to the a webmap to see the map in the user's browser)
endmap
+# When adding or removing maps be sure to also add/remove from ci_config_maps.txt
+
# Production-level maps.
map birdshot
@@ -98,3 +100,6 @@ endmap
map runtimestation
endmap
+
+map runtimestation_minimal
+endmap
diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml
index ca7ab1a64ccc..24ae7d3e2172 100644
--- a/html/changelogs/archive/2026-06.yml
+++ b/html/changelogs/archive/2026-06.yml
@@ -175,3 +175,114 @@
by ghosts or players with science goggles.
lelandkemble:
- bugfix: Pinpointers stop tracking a shunted ai when that ai is no longer shunted
+2026-06-07:
+ ArcaneMusic:
+ - balance: The mining ventpointer has been adjusted so that it can now more accurately
+ show the distance to the nearest vent, and will now claim you've arrived if
+ you're within 8 tiles away from a vent, down from 14.
+ - code_imp: Several backend improvements to the pinpointer functionality and adjustability.
+ CabinetOnFire:
+ - sound: Sound now makes use of euclidean distance for falloff
+ - code_imp: max_range on playsound now ensures the sound is audible up to that range.
+ SmArtKar:
+ - bugfix: Fixed an issue where Stop-All-Active-Weather verb would not remove ash
+ storm sounds
+ - refactor: Rain and ash storms have been refactored to use particles rather than
+ overlays
+ - image: Made volcanic pores more noticeable
+ UvvU:
+ - bugfix: fixed improper chat caching.
+ soulware1:
+ - bugfix: Parthenogenesis no longer makes your material fish have null weights
+2026-06-08:
+ Absolucy:
+ - bugfix: Made the BYOND member OOC icon better aligned to the text.
+ Melbert:
+ - bugfix: Fixed a bug where diseases wouldn't trigger their regular effects when
+ applied in certain contexts
+ - bugfix: Fixed a bug where airborne diseases wouldn't spread via breathing when
+ applied in certain contexts
+ - balance: Blood worms are no longer fully immune to extreme pressures and cold,
+ such as the vacuum of space
+ mcbalaam:
+ - rscadd: Added fake plastic plants to be used for decoration
+ - rscadd: 'RDD: Rapid Decoration Device is now available for the service department'
+ soulware1:
+ - balance: Carving blocks can be made out of any material now!
+ timothymtorres:
+ - rscadd: Pole objects like mops, brooms, staves, scythes, and spears can now be
+ held in a hand to act as walking aids, mitigating limping from fractures and
+ reducing missing-leg slowdowns.
+ - balance: Walking aids must be held in the hand on the same side as the injured
+ or missing leg to provide support, and lose their effectiveness if actively
+ wielded with two hands.
+ - balance: Objects with the walking aid reduce limbless slowdown by 40%, while dedicated
+ medical crutches reduce it by 60%.
+ - refactor: Refactor cane and crutch code logic into a modular walking aid component
+2026-06-09:
+ EnterTheJake:
+ - rscadd: Tactical Maps have been added to the game!
+ - rscadd: Nuclear Operatives implants now grant the ability to open a Minimap, this
+ map will display and show the names of various areas of the station, and the
+ positioning of the Nukeops team,borgs,mechs,Cayenne and the nuke disk in real
+ time!
+ - rscadd: Nukie leaders and OW agents can also draw and apply labels to the map.
+ - rscadd: the "Recoinassance Platform" has been added to the Nukie Base, it allows
+ displaying of the Tactical maps while on the Syndicate Base Z level.
+ - map: The briefing room in the NukeOps base has been remapped to better accomodate
+ the new Holographic table.
+ - map: Changed the front shutters on the Syndicate Infiltrator to the more fitting
+ Syndicate Shutters.
+ mcbalaam:
+ - bugfix: Fixed some of the Big Manipulator behavior such as not working in strict
+ mode, putting items inside itself and incorrectly targeting items due to misconfigured
+ filters.
+2026-06-10:
+ Ghommie:
+ - balance: You should now be able to walk around while eating about any vending
+ machine snack, no longer just chips and a couple others.
+ SmArtKar:
+ - qol: RND Techweb now displays where a design can be printed and how much it'd
+ cost to print
+ - balance: Tendrils have had their health reduced and attack cooldowns, delays and
+ telegraph durations increased. Damage of some attacks has been slightly increased
+ (by 5) but they can no longer multihit.
+ - bugfix: Tendril spikes now animate properly
+ - image: Tendril shots now have a fancy retraction animation
+ Wisemonster:
+ - spellcheck: The sphere module now clarifies that the modsuit need to be insulated
+ with bileworm skin in order to wade through lava safely.
+ - spellcheck: Corrects sphere modules micromissle to micromissles
+ btcbuster:
+ - balance: Baseball bats do more damage and fit on the back slot now.
+ cacogen:
+ - rscadd: Adds packets of beef ramen as an uncommon maintenance spawn
+ georgebothways:
+ - bugfix: Trays can now unload eight items onto griddles properly.
+ timothymtorres:
+ - sound: Books now make paper sound when opened
+2026-06-11:
+ Melbert:
+ - bugfix: Losing your limbs via the cursed quirk now gives you limb stumps (so you
+ can get your limbs back afterwards)
+ SmArtKar:
+ - bugfix: Fixed debug overlays on new biome rocks on lavaland
+ TealSeer:
+ - refactor: security camera construction/item interaction has been rewritten, report
+ any weirdness
+ Y0SH1M4S73R:
+ - bugfix: Changing the species of most monkeys, whose names were changed to those
+ of individuals on the manifest, will no longer cause the names on those individuals'
+ records to be changed to the name the monkey ends up with.
+ cool20141:
+ - bugfix: fixed TTX healing
+ lelandkemble:
+ - bugfix: fixed a runtime when trying to pull a turf
+ - bugfix: Blood spraying from hitting existing wounds works
+ - bugfix: Corgis no longer runtime when trying to open doors without an ID
+ oranges:
+ - bugfix: fixed arguments for wound recieve damage
+ timothymtorres:
+ - sound: Add valve sound to portable canisters
+ - sound: Add keypad sounds to secure safes
+ - sound: Add drill sound to surgical drill
diff --git a/icons/effects/weather_overlay.dmi b/icons/effects/weather_overlay.dmi
new file mode 100644
index 000000000000..7dc15fbe40c7
Binary files /dev/null and b/icons/effects/weather_overlay.dmi differ
diff --git a/icons/hud/actions.dmi b/icons/hud/actions.dmi
index 6d51cc55534a..e8385331596b 100644
Binary files a/icons/hud/actions.dmi and b/icons/hud/actions.dmi differ
diff --git a/icons/hud/implants.dmi b/icons/hud/implants.dmi
index b85ab4573ff2..64ba66dc4068 100644
Binary files a/icons/hud/implants.dmi and b/icons/hud/implants.dmi differ
diff --git a/icons/mob/inhands/equipment/tools_lefthand.dmi b/icons/mob/inhands/equipment/tools_lefthand.dmi
index adaedb6483f7..5c0cf513166b 100644
Binary files a/icons/mob/inhands/equipment/tools_lefthand.dmi and b/icons/mob/inhands/equipment/tools_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/tools_righthand.dmi b/icons/mob/inhands/equipment/tools_righthand.dmi
index 454a225c82a7..e96f9fa959ba 100644
Binary files a/icons/mob/inhands/equipment/tools_righthand.dmi and b/icons/mob/inhands/equipment/tools_righthand.dmi differ
diff --git a/icons/mob/simple/lavaland/bileworm_old.dmi b/icons/mob/simple/lavaland/bileworm_old.dmi
deleted file mode 100644
index 8337f6cc4f18..000000000000
Binary files a/icons/mob/simple/lavaland/bileworm_old.dmi and /dev/null differ
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index f338fab310ae..1b7be6f3f091 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/obj/fluff/flora/rocks.dmi b/icons/obj/fluff/flora/rocks.dmi
index 098f0b03fbb0..c68ddaef7d06 100644
Binary files a/icons/obj/fluff/flora/rocks.dmi and b/icons/obj/fluff/flora/rocks.dmi differ
diff --git a/icons/obj/food/martian.dmi b/icons/obj/food/martian.dmi
index fb5528818a19..f2baab585066 100644
Binary files a/icons/obj/food/martian.dmi and b/icons/obj/food/martian.dmi differ
diff --git a/icons/obj/machines/minimap_table.dmi b/icons/obj/machines/minimap_table.dmi
new file mode 100644
index 000000000000..bb68f2da612c
Binary files /dev/null and b/icons/obj/machines/minimap_table.dmi differ
diff --git a/icons/obj/machines/minimap_table_hologram.dmi b/icons/obj/machines/minimap_table_hologram.dmi
new file mode 100644
index 000000000000..3e0fe4d2e700
Binary files /dev/null and b/icons/obj/machines/minimap_table_hologram.dmi differ
diff --git a/icons/obj/storage/box.dmi b/icons/obj/storage/box.dmi
index 4101697250c3..e59cb1924120 100644
Binary files a/icons/obj/storage/box.dmi and b/icons/obj/storage/box.dmi differ
diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi
index 0f1f57eb860a..4c8209021996 100644
Binary files a/icons/obj/tools.dmi and b/icons/obj/tools.dmi differ
diff --git a/icons/ui_icons/minimap/draw_tools.dmi b/icons/ui_icons/minimap/draw_tools.dmi
new file mode 100644
index 000000000000..ba72a74475e4
Binary files /dev/null and b/icons/ui_icons/minimap/draw_tools.dmi differ
diff --git a/icons/ui_icons/minimap/map_blips.dmi b/icons/ui_icons/minimap/map_blips.dmi
new file mode 100644
index 000000000000..ce7c1e579c9a
Binary files /dev/null and b/icons/ui_icons/minimap/map_blips.dmi differ
diff --git a/icons/ui_icons/minimap/map_blips_large.dmi b/icons/ui_icons/minimap/map_blips_large.dmi
new file mode 100644
index 000000000000..420551877aef
Binary files /dev/null and b/icons/ui_icons/minimap/map_blips_large.dmi differ
diff --git a/icons/ui_icons/minimap/minimap.dmi b/icons/ui_icons/minimap/minimap.dmi
new file mode 100644
index 000000000000..7eeb5680b90f
Binary files /dev/null and b/icons/ui_icons/minimap/minimap.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_buttons.dmi b/icons/ui_icons/minimap/minimap_buttons.dmi
new file mode 100644
index 000000000000..5f8655815619
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_buttons.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi
new file mode 100644
index 000000000000..bae80afea3c2
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi
new file mode 100644
index 000000000000..1290b9e55598
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi
new file mode 100644
index 000000000000..690bd390167d
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_red.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_red.dmi
new file mode 100644
index 000000000000..83d06fcd1ac0
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/draw_red.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi
new file mode 100644
index 000000000000..5489f87ce420
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi differ
diff --git a/icons/ui_icons/minimap/minimap_mouse/label.dmi b/icons/ui_icons/minimap/minimap_mouse/label.dmi
new file mode 100644
index 000000000000..ad619da8e83f
Binary files /dev/null and b/icons/ui_icons/minimap/minimap_mouse/label.dmi differ
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000000..51f0906c4811
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,200 @@
+{
+ "name": "TG-STATION",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.16",
+ "prettier": "^3.8.3"
+ }
+ },
+ "node_modules/@biomejs/biome": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz",
+ "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "2.4.16",
+ "@biomejs/cli-darwin-x64": "2.4.16",
+ "@biomejs/cli-linux-arm64": "2.4.16",
+ "@biomejs/cli-linux-arm64-musl": "2.4.16",
+ "@biomejs/cli-linux-x64": "2.4.16",
+ "@biomejs/cli-linux-x64-musl": "2.4.16",
+ "@biomejs/cli-win32-arm64": "2.4.16",
+ "@biomejs/cli-win32-x64": "2.4.16"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz",
+ "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz",
+ "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz",
+ "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz",
+ "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz",
+ "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz",
+ "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "2.4.16",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz",
+ "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "2.4.16",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 68493d466f4a..9f494dce393d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"devDependencies": {
- "@biomejs/biome": "^2.3.10",
- "prettier": "^3.7.4"
+ "@biomejs/biome": "^2.4.16",
+ "prettier": "^3.8.3"
},
"scripts": {
"tgui:fix": "biome check --write --unsafe tgui",
diff --git a/tgstation.dme b/tgstation.dme
index 58f475443e64..3963035c2edc 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -163,6 +163,7 @@
#include "code\__DEFINES\melee.dm"
#include "code\__DEFINES\memory_defines.dm"
#include "code\__DEFINES\mergers.dm"
+#include "code\__DEFINES\minimap.dm"
#include "code\__DEFINES\mining.dm"
#include "code\__DEFINES\mob_spawn.dm"
#include "code\__DEFINES\mobfactions.dm"
@@ -896,7 +897,6 @@
#include "code\datums\signals.dm"
#include "code\datums\sound_token.dm"
#include "code\datums\spawners_menu.dm"
-#include "code\datums\sprite_accessories.dm"
#include "code\datums\station_alert.dm"
#include "code\datums\station_integrity.dm"
#include "code\datums\stock_market_events.dm"
@@ -1400,6 +1400,7 @@
#include "code\datums\components\usb_port.dm"
#include "code\datums\components\vacuum.dm"
#include "code\datums\components\vision_hurting.dm"
+#include "code\datums\components\walking_aid.dm"
#include "code\datums\components\wearertargeting.dm"
#include "code\datums\components\weatherannouncer.dm"
#include "code\datums\components\wet_floor.dm"
@@ -2052,6 +2053,20 @@
#include "code\datums\skills\fishing.dm"
#include "code\datums\skills\gaming.dm"
#include "code\datums\skills\mining.dm"
+#include "code\datums\sprite_accessories\_sprite_accessory.dm"
+#include "code\datums\sprite_accessories\clothing.dm"
+#include "code\datums\sprite_accessories\ears.dm"
+#include "code\datums\sprite_accessories\facial_hair.dm"
+#include "code\datums\sprite_accessories\frills.dm"
+#include "code\datums\sprite_accessories\hair.dm"
+#include "code\datums\sprite_accessories\horns.dm"
+#include "code\datums\sprite_accessories\markings.dm"
+#include "code\datums\sprite_accessories\moth_antennae.dm"
+#include "code\datums\sprite_accessories\pod_hair.dm"
+#include "code\datums\sprite_accessories\snouts.dm"
+#include "code\datums\sprite_accessories\spines.dm"
+#include "code\datums\sprite_accessories\tails.dm"
+#include "code\datums\sprite_accessories\wings.dm"
#include "code\datums\station_traits\_station_trait.dm"
#include "code\datums\station_traits\admin_panel.dm"
#include "code\datums\station_traits\job_traits.dm"
@@ -2150,6 +2165,7 @@
#include "code\datums\votes\custom_vote.dm"
#include "code\datums\votes\map_vote.dm"
#include "code\datums\votes\restart_vote.dm"
+#include "code\datums\weather\particle_weather.dm"
#include "code\datums\weather\weather.dm"
#include "code\datums\weather\weather_types\ash_storm.dm"
#include "code\datums\weather\weather_types\floor_is_lava.dm"
@@ -2819,6 +2835,7 @@
#include "code\game\objects\items\implants\implant_spell.dm"
#include "code\game\objects\items\implants\implant_stealth.dm"
#include "code\game\objects\items\implants\implant_storage.dm"
+#include "code\game\objects\items\implants\implant_tacmap.dm"
#include "code\game\objects\items\implants\implantcase.dm"
#include "code\game\objects\items\implants\implantchair.dm"
#include "code\game\objects\items\implants\implanter.dm"
@@ -2834,6 +2851,7 @@
#include "code\game\objects\items\kirby_plants\synthetic_plants.dm"
#include "code\game\objects\items\rcd\RCD.dm"
#include "code\game\objects\items\rcd\RCL.dm"
+#include "code\game\objects\items\rcd\RDD.dm"
#include "code\game\objects\items\rcd\RHD.dm"
#include "code\game\objects\items\rcd\RLD.dm"
#include "code\game\objects\items\rcd\RPD.dm"
@@ -2973,6 +2991,7 @@
#include "code\game\objects\structures\chess.dm"
#include "code\game\objects\structures\containers.dm"
#include "code\game\objects\structures\curtains.dm"
+#include "code\game\objects\structures\decorations.dm"
#include "code\game\objects\structures\deployable_turret.dm"
#include "code\game\objects\structures\destructible_structures.dm"
#include "code\game\objects\structures\detectiveboard.dm"
@@ -3776,6 +3795,7 @@
#include "code\modules\asset_cache\assets\portraits.dm"
#include "code\modules\asset_cache\assets\radar.dm"
#include "code\modules\asset_cache\assets\rcd.dm"
+#include "code\modules\asset_cache\assets\rdd.dm"
#include "code\modules\asset_cache\assets\research_designs.dm"
#include "code\modules\asset_cache\assets\rtd.dm"
#include "code\modules\asset_cache\assets\safe.dm"
@@ -5095,6 +5115,18 @@
#include "code\modules\meteors\meteor_spawning.dm"
#include "code\modules\meteors\meteor_types.dm"
#include "code\modules\meteors\meteor_waves.dm"
+#include "code\modules\minimap\_minimap.dm"
+#include "code\modules\minimap\minimap.dm"
+#include "code\modules\minimap\minimap_action.dm"
+#include "code\modules\minimap\minimap_table.dm"
+#include "code\modules\minimap\hud\minimal_z_level.dm"
+#include "code\modules\minimap\hud\minimap_dimmer.dm"
+#include "code\modules\minimap\hud\minimap_display.dm"
+#include "code\modules\minimap\hud\minimap_drawing.dm"
+#include "code\modules\minimap\hud\minimap_element.dm"
+#include "code\modules\minimap\hud\minimap_label.dm"
+#include "code\modules\minimap\hud\minimap_toolbar.dm"
+#include "code\modules\minimap\hud\minimap_tracking_blip.dm"
#include "code\modules\mining\aux_base.dm"
#include "code\modules\mining\fulton.dm"
#include "code\modules\mining\machine_processing.dm"
diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts
index 1955fa8ab8e1..9894fa1a95ec 100644
--- a/tgui/packages/common/storage.ts
+++ b/tgui/packages/common/storage.ts
@@ -33,6 +33,10 @@ const testHubStorage = testGeneric(
() => window.hubStorage && !!window.hubStorage.getItem,
);
+const STORAGE_CDN_TIMEOUT = 5000;
+const persistedStorageKeys = ['panel-settings', 'chat-state', 'chat-messages'];
+const legacyHubMigrationKeys = ['panel-settings'];
+
class HubStorageBackend implements StorageBackend {
public impl: StorageImplementation;
@@ -77,20 +81,35 @@ class IFrameIndexedDbBackend implements StorageBackend {
iframe.src = Byond.storageCdn;
const completePromise: Promise = new Promise((resolve) => {
- fetch(Byond.storageCdn, { method: "HEAD" }).then((response) => {
- if (response.status !== 200) {
- resolve(false);
+ const listener = (message: MessageEvent) => {
+ if (
+ message.source === iframe.contentWindow &&
+ message.data === 'ready'
+ ) {
+ resolveReady(true);
}
+ };
+ const resolveReady = (ready: boolean) => {
+ clearTimeout(timeout);
+ window.removeEventListener('message', listener);
+ resolve(ready);
+ };
+ const timeout = setTimeout(
+ () => resolveReady(false),
+ STORAGE_CDN_TIMEOUT,
+ );
+
+ fetch(Byond.storageCdn, { method: 'HEAD' })
+ .then((response) => {
+ if (response.status !== 200) {
+ resolveReady(false);
+ }
+ })
+ .catch(() => {
+ resolveReady(false);
+ });
- }).catch(() => {
- resolve(false);
- })
-
- window.addEventListener('message', (message) => {
- if (message.data === "ready") {
- resolve(true);
- }
- })
+ window.addEventListener('message', listener);
});
this.documentElement = document.body.appendChild(iframe);
@@ -105,11 +124,23 @@ class IFrameIndexedDbBackend implements StorageBackend {
async get(key: string): Promise {
const promise = new Promise((resolve) => {
- window.addEventListener('message', (message) => {
- if (message.data.key && message.data.key === key) {
+ const listener = (message: MessageEvent) => {
+ if (
+ message.source === this.iframeWindow &&
+ message.data.key &&
+ message.data.key === key
+ ) {
+ clearTimeout(timeout);
+ window.removeEventListener('message', listener);
resolve(message.data.value);
}
- });
+ };
+ const timeout = setTimeout(() => {
+ window.removeEventListener('message', listener);
+ resolve(undefined);
+ }, STORAGE_CDN_TIMEOUT);
+
+ window.addEventListener('message', listener);
});
this.iframeWindow.postMessage({ type: 'get', key: key }, '*');
@@ -143,65 +174,89 @@ class StorageProxy implements StorageBackend {
constructor() {
this.backendPromise = (async () => {
-
- // If we have not enabled byondstorage yet, we need to check
- // if we can use the IFrame, or if we need to enable byondstorage
- if (!testHubStorage()) {
-
- // If we have an IFrame URL we can use, and we haven't already enabled
- // byondstorage, we should use the IFrame backend
- if (Byond.storageCdn) {
- const iframe = new IFrameIndexedDbBackend();
-
- if ((await iframe.ready()) === true) {
- if (await iframe.get('byondstorage-migrated')) return iframe;
-
- Byond.winset(null, 'browser-options', '+byondstorage');
-
- await new Promise((resolve) => {
- document.addEventListener('byondstorageupdated', async () => {
- setTimeout(() => {
- const hub = new HubStorageBackend();
-
- // Migrate these existing settings from byondstorage to the IFrame
- for (const setting of ['panel-settings', 'chat-state', 'chat-messages']) {
- hub
- .get(setting)
- .then((settings) => iframe.set(setting, settings));
+ // Prefer the configured iframe storage when available. hubStorage may
+ // already be enabled by another window/server, but the iframe origin is
+ // the server-configured storage boundary.
+ if (Byond.storageCdn) {
+ const iframe = new IFrameIndexedDbBackend();
+
+ if ((await iframe.ready()) === true) {
+ if (await iframe.get('byondstorage-migrated')) return iframe;
+
+ const iframeHasPersistedStorage = (
+ await Promise.all(
+ persistedStorageKeys.map((setting) => iframe.get(setting)),
+ )
+ ).some((settings) => settings !== undefined);
+
+ if (!iframeHasPersistedStorage) {
+ const hubStorageWasEnabled = testHubStorage();
+ if (!hubStorageWasEnabled) {
+ Byond.winset(null, 'browser-options', '+byondstorage');
+
+ await new Promise((resolve) => {
+ document.addEventListener(
+ 'byondstorageupdated',
+ () => {
+ // This event is emitted *before* byondstorage is actually
+ // created, so we have to wait a little bit before using it.
+ setTimeout(resolve, 1);
+ },
+ { once: true },
+ );
+ });
+ }
+
+ const hub = new HubStorageBackend();
+
+ // Migrate safe legacy settings from byondstorage to the IFrame.
+ // Chat history may contain server-specific HTML/components from
+ // other codebases that shared the old byondstorage namespace.
+ await Promise.all(
+ legacyHubMigrationKeys.map(async (setting) => {
+ try {
+ const settings = await hub.get(setting);
+ if (settings !== undefined) {
+ await iframe.set(setting, settings);
}
+ } catch {
+ // Ignore unreadable legacy storage entries. A bad old cache
+ // key should not keep the client on byondstorage.
+ }
+ }),
+ );
+
+ if (!hubStorageWasEnabled) {
+ Byond.winset(null, 'browser-options', '-byondstorage');
+ }
+ }
- iframe.set('byondstorage-migrated', true);
- Byond.winset(null, 'browser-options', '-byondstorage');
-
- resolve();
- }, 1);
- });
- });
+ await iframe.set('byondstorage-migrated', true);
- return iframe;
- }
+ return iframe;
+ }
- iframe.destroy();
- };
+ iframe.destroy();
+ }
- // IFrame hasn't worked out for us, we'll need to enable byondstorage
- Byond.winset(null, 'browser-options', '+byondstorage');
+ if (testHubStorage()) {
+ return new HubStorageBackend();
+ }
- return new Promise((resolve) => {
- const listener = () => {
- document.removeEventListener('byondstorageupdated', listener);
+ // IFrame hasn't worked out for us, we'll need to enable byondstorage
+ Byond.winset(null, 'browser-options', '+byondstorage');
- // This event is emitted *before* byondstorage is actually created
- // so we have to wait a little bit before we can use it
- setTimeout(() => resolve(new HubStorageBackend()), 1);
- };
+ return new Promise((resolve) => {
+ const listener = () => {
+ document.removeEventListener('byondstorageupdated', listener);
- document.addEventListener('byondstorageupdated', listener);
- });
- }
+ // This event is emitted *before* byondstorage is actually created
+ // so we have to wait a little bit before we can use it
+ setTimeout(() => resolve(new HubStorageBackend()), 1);
+ };
- // byondstorage is already enabled, we can use it straight away
- return new HubStorageBackend();
+ document.addEventListener('byondstorageupdated', listener);
+ });
})();
}
diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx
index bee627b36ebb..8c28e1942dc5 100644
--- a/tgui/packages/tgui-panel/chat/renderer.tsx
+++ b/tgui/packages/tgui-panel/chat/renderer.tsx
@@ -422,10 +422,19 @@ class ChatRenderer {
outputProps[canon_name] = working_value;
}
const oldHtml = { __html: childNode.innerHTML };
+ const Element = TGUI_CHAT_COMPONENTS[targetName];
+ if (!Element) {
+ logger.error(
+ `Error: unknown chat component "${targetName}" in message`,
+ message,
+ );
+ childNode.removeAttribute('data-component');
+ continue;
+ }
+
while (childNode.firstChild) {
childNode.removeChild(childNode.firstChild);
}
- const Element = TGUI_CHAT_COMPONENTS[targetName];
const reactRoot = createRoot(childNode);
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index e12b3f6cdf65..8bbc44edfd1d 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -567,7 +567,7 @@ em {
}
.green {
- color: hsl(132.8, 93.4%, 29.6%);
+ color: hsl(132.9, 100%, 50.6%);
}
.grey {
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index efaeb41b6a50..ea2b3aaabc48 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -585,7 +585,7 @@ h2.alert {
}
.green {
- color: hsl(132.9, 100%, 50.6%);
+ color: hsl(132.8, 93.4%, 29.6%);
}
.grey {
diff --git a/tgui/packages/tgui/drag.ts b/tgui/packages/tgui/drag.ts
index aeb991bac360..68ed9df38d76 100644
--- a/tgui/packages/tgui/drag.ts
+++ b/tgui/packages/tgui/drag.ts
@@ -5,8 +5,8 @@
*/
import { storage } from 'common/storage';
-import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'tgui-core/vector';
import type { BooleanLike } from 'tgui-core/react';
+import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'tgui-core/vector';
import { createLogger } from './logging';
type Point = [number, number];
diff --git a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
index 144e9d3e94c9..e56d1dd431f8 100644
--- a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
+++ b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
@@ -173,7 +173,7 @@ function TaskEditModal(props: TaskEditModalProps) {
const isCargo = !!task.turf;
const isPickup = task.task_type.includes('pickup');
- const isDropoff = task.task_type.includes('dropoff');
+ const isDropoff = task.task_type === 'drop' || task.task_type === 'throw' || task.task_type === 'use';
const isInteract = task.task_type.includes('interact');
const currentButton = task.turf
@@ -236,12 +236,16 @@ function TaskEditModal(props: TaskEditModalProps) {
- adjust('cycle_filtering_mode')}
- tooltip="Cycle object category"
- />
+ {(task.task_type === 'pickup' ||
+ task.task_type === 'drop' ||
+ task.task_type === 'throw') && (
+ adjust('cycle_filtering_mode')}
+ tooltip="Cycle object category"
+ />
+ )}
- adjust('cycle_interaction_mode')}
- tooltip="Drop / Throw / Use"
- />
- adjust('cycle_overflow_status')}
- tooltip="Cycle overflow behaviour"
- />
- {task.interaction_mode?.toUpperCase() === 'THROW' && (
+ {task.task_type === 'drop' && (
+ adjust('cycle_overflow_status')}
+ tooltip="Cycle overflow behaviour"
+ />
+ )}
+ {task.task_type === 'throw' && (
)}
- {(isDropoff || isInteract) && task.interaction_mode?.toUpperCase() !== 'THROW' && (
+ {(task.task_type === 'use' || isInteract) && (
<>
adjust('toggle_worker_combat')}
tooltip="Use combat mode during interaction"
/>
+ adjust('toggle_skip_anchored')}
+ tooltip="Skip anchored objects when looking for interaction targets"
+ />
void;
+}) => {
+ const { design, selected, onClick } = props;
+
+ return (
+
+ );
+};
+
+export const RapidDecorationDevice = () => {
+ const { act, data } = useBackend();
+ const {
+ categories = [],
+ selected_category,
+ selected_design,
+ matter,
+ max_matter,
+ } = data;
+
+ return (
+
+
+
+
+
+
+
+ {matter}/{max_matter} Units
+
+
+ {selected_design ? capitalizeAll(selected_design) : 'None'}
+
+
+
+
+
+
+ {categories.map((category) => (
+
+
+ {category.designs.map((design) => (
+
+
+ act('design', {
+ category: category.cat_name,
+ name: design.name,
+ })
+ }
+ />
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Techweb/helpers.ts b/tgui/packages/tgui/interfaces/Techweb/helpers.ts
index 8215572d64f5..11368fc8e7eb 100644
--- a/tgui/packages/tgui/interfaces/Techweb/helpers.ts
+++ b/tgui/packages/tgui/interfaces/Techweb/helpers.ts
@@ -1,6 +1,7 @@
import { map } from 'es-toolkit/compat';
import { useBackend } from '../../backend';
+import type { Design } from '../Fabrication/Types';
import type { NodeCache, TechWebData } from './types';
type Cost = {
@@ -13,9 +14,10 @@ type RemappedNode = NodeCache & {
costs: Cost[];
};
-type RemappedDesignCache = {
- name: string;
+type RemappedDesignCache = Design & {
class: string;
+ department_flags: number;
+ build_type: number;
};
// Data reshaping / ingestion (thanks stylemistake for the help, very cool!)
@@ -58,16 +60,27 @@ function selectRemappedStaticData(data: TechWebData) {
// Do the same as the above for the design cache
const design_cache = {} as RemappedDesignCache;
for (const id of Object.keys(data.static_data.design_cache)) {
- const [name, classes] = data.static_data.design_cache[id];
+ const [name, cost, build_types, department_flags, classes] =
+ data.static_data.design_cache[id];
design_cache[remapId(id)] = {
name: name,
+ cost: cost,
+ build_types: build_types,
+ department_flags: department_flags,
class: classes.startsWith('design') ? classes : `design32x32 ${classes}`,
};
}
+ const SHEET_MATERIAL_AMOUNT = data.static_data.SHEET_MATERIAL_AMOUNT;
+ const build_types = data.static_data.build_types;
+ const department_flags = data.static_data.department_flags;
+
return {
node_cache,
design_cache,
+ build_types,
+ department_flags,
+ SHEET_MATERIAL_AMOUNT,
};
}
diff --git a/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx b/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
index 613fe82708d3..03852a4b7c2b 100644
--- a/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
+++ b/tgui/packages/tgui/interfaces/Techweb/nodes/TechNode.tsx
@@ -2,13 +2,14 @@ import {
Box,
Button,
Collapsible,
+ ImageButton,
ProgressBar,
Section,
Stack,
} from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
-
import { Experiment } from '../../ExperimentConfigure';
+import { MaterialCostSequence } from '../../Fabrication/MaterialCostSequence';
import { useRemappedBackend } from '../helpers';
import { useTechWebRoute } from '../hooks';
import { LockedExperiment } from '../LockedExperiment';
@@ -26,6 +27,9 @@ export function TechNode(props: Props) {
const {
node_cache,
design_cache,
+ SHEET_MATERIAL_AMOUNT,
+ build_types,
+ department_flags,
experiments,
points = [],
nodes,
@@ -201,10 +205,48 @@ export function TechNode(props: Props) {
{design_ids.map((k, i) => (
-