diff --git a/_maps/RandomRuins/IceRuins/icemoon_surface_engioutpost.dmm b/_maps/RandomRuins/IceRuins/icemoon_surface_engioutpost.dmm
index 1a1d07b6a999..e6be68cae54f 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_surface_engioutpost.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_surface_engioutpost.dmm
@@ -818,7 +818,7 @@
/turf/open/floor/engine/vacuum,
/area/ruin/planetengi)
"yF" = (
-/mob/living/simple_animal/hostile/construct/proteon,
+/mob/living/basic/construct/proteon,
/turf/open/floor/iron,
/area/ruin/planetengi)
"Ai" = (
@@ -829,7 +829,7 @@
/turf/open/floor/iron/icemoon,
/area/ruin/planetengi)
"Iy" = (
-/mob/living/simple_animal/hostile/construct/proteon,
+/mob/living/basic/construct/proteon,
/obj/effect/turf_decal/tile/yellow,
/turf/open/floor/iron/icemoon,
/area/ruin/planetengi)
diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
index 44e5b655c89f..7b99eeba0758 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
@@ -1,11 +1,4 @@
//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
-"ac" = (
-/obj/structure/window/reinforced/spawner/directional/north,
-/obj/structure/window/reinforced/spawner,
-/obj/structure/window/reinforced/spawner/directional/east,
-/obj/structure/grille,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"af" = (
/obj/structure/window/reinforced/spawner/directional/north,
/obj/structure/grille,
@@ -82,6 +75,37 @@
/obj/effect/mapping_helpers/airlock/abandoned,
/turf/open/floor/plating,
/area/ruin/plasma_facility/commons)
+"aZ" = (
+/obj/structure/railing{
+ dir = 5
+ },
+/obj/structure/closet/crate{
+ icon_state = "crateopen"
+ },
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/sheet/mineral/silver,
+/obj/item/stack/ore/silver,
+/obj/item/stack/ore/silver,
+/obj/item/stack/ore/silver,
+/obj/item/stack/ore/silver,
+/obj/item/stack/ore/silver,
+/obj/item/stack/ore/silver,
+/turf/open/floor/plating/snowed/icemoon,
+/area/icemoon/underground/explored)
+"bj" = (
+/obj/effect/turf_decal/bot/left,
+/obj/structure/closet/crate{
+ icon_state = "crateopen"
+ },
+/obj/item/stack/sheet/mineral/plasma/thirty,
+/turf/open/floor/iron/smooth_half{
+ initial_gas_mix = "ICEMOON_ATMOS"
+ },
+/area/ruin/plasma_facility/operations)
"br" = (
/obj/structure/table/reinforced,
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
@@ -127,7 +151,7 @@
name = "Plasma Relief Valve"
},
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/turf/open/floor/plating/snowed/icemoon,
/area/icemoon/underground/explored)
"bX" = (
@@ -274,16 +298,15 @@
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron,
/area/ruin/plasma_facility/commons)
+"ea" = (
+/obj/effect/decal/cleanable/dirt,
+/obj/machinery/light/floor/has_bulb,
+/turf/open/floor/plating/snowed/icemoon,
+/area/icemoon/underground/explored)
"ed" = (
/obj/machinery/atmospherics/components/tank/plasma,
/turf/open/floor/plating/snowed/icemoon,
/area/ruin/plasma_facility/operations)
-"ev" = (
-/obj/structure/window/reinforced/unanchored{
- dir = 1
- },
-/turf/open/misc/asteroid/snow/icemoon,
-/area/icemoon/underground/explored)
"eC" = (
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
@@ -298,17 +321,6 @@
/obj/structure/fence/cut/large,
/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
-"eM" = (
-/obj/structure/window/reinforced/spawner,
-/obj/effect/decal/cleanable/glass,
-/obj/item/shard{
- pixel_x = 11;
- pixel_y = 6
- },
-/obj/item/shard,
-/obj/effect/decal/cleanable/dirt,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"eO" = (
/obj/structure/table,
/obj/item/plate,
@@ -326,6 +338,12 @@
/obj/structure/bonfire,
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
+"fc" = (
+/obj/structure/window/reinforced/unanchored{
+ dir = 1
+ },
+/turf/open/misc/asteroid/snow/icemoon,
+/area/icemoon/underground/explored)
"fx" = (
/obj/machinery/light/small/built/directional/west,
/turf/open/lava/plasma/ice_moon,
@@ -359,9 +377,9 @@
},
/turf/open/lava/plasma/ice_moon,
/area/icemoon/underground/explored)
-"gd" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/misc/asteroid/snow/icemoon,
+"gi" = (
+/mob/living/basic/mining/wolf,
+/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
"gj" = (
/obj/structure/closet/emcloset,
@@ -475,12 +493,6 @@
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/wood/large,
/area/ruin/plasma_facility/commons)
-"hN" = (
-/obj/structure/grille,
-/obj/structure/window/reinforced/spawner,
-/obj/structure/window/reinforced/spawner/directional/east,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/commons)
"hR" = (
/turf/closed/wall/ice,
/area/ruin/plasma_facility/operations)
@@ -563,6 +575,13 @@
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/stone,
/area/ruin/plasma_facility/commons)
+"im" = (
+/obj/structure/window/reinforced/spawner,
+/obj/effect/decal/cleanable/glass,
+/obj/item/shard,
+/obj/effect/decal/cleanable/dirt,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"iw" = (
/obj/structure/railing,
/obj/item/grown/log{
@@ -608,6 +627,17 @@
initial_gas_mix = "ICEMOON_ATMOS"
},
/area/ruin/plasma_facility/operations)
+"jI" = (
+/obj/structure/window/reinforced/spawner,
+/obj/effect/decal/cleanable/glass,
+/obj/item/shard{
+ pixel_x = 11;
+ pixel_y = 6
+ },
+/obj/item/shard,
+/obj/effect/decal/cleanable/dirt,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"jN" = (
/obj/item/grown/log{
pixel_x = -7;
@@ -630,24 +660,6 @@
},
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
-"jT" = (
-/obj/structure/window/reinforced/spawner,
-/obj/structure/grille,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
-"kh" = (
-/obj/structure/window/reinforced/spawner,
-/obj/structure/frame/computer{
- anchored = 1;
- dir = 4
- },
-/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
-/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
-/obj/effect/decal/cleanable/dirt,
-/turf/open/floor/iron/smooth{
- initial_gas_mix = "ICEMOON_ATMOS"
- },
-/area/ruin/plasma_facility/operations)
"ku" = (
/obj/effect/decal/cleanable/dirt,
/obj/effect/turf_decal/tile/brown/anticorner{
@@ -660,30 +672,6 @@
dir = 4
},
/area/ruin/plasma_facility/commons)
-"kA" = (
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/obj/item/stack/ore/plasma,
-/turf/open/floor/plating/snowed/icemoon,
-/area/icemoon/underground/explored)
"kG" = (
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/icemoon,
@@ -813,6 +801,12 @@
/obj/effect/turf_decal/tile/purple/half,
/turf/open/floor/iron/textured_half,
/area/ruin/plasma_facility/operations)
+"ng" = (
+/obj/structure/grille/broken,
+/obj/structure/window/reinforced/spawner,
+/obj/effect/decal/cleanable/glass,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"nk" = (
/obj/machinery/atmospherics/pipe/smart/simple/general/hidden{
dir = 5
@@ -856,6 +850,14 @@
dir = 1
},
/area/ruin/plasma_facility/commons)
+"ns" = (
+/obj/item/kirbyplants{
+ icon_state = "applebush"
+ },
+/obj/effect/decal/cleanable/dirt,
+/obj/effect/turf_decal/tile/brown/full,
+/turf/open/floor/iron/large,
+/area/ruin/plasma_facility/commons)
"nu" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
@@ -990,14 +992,6 @@
},
/turf/open/floor/iron/edge,
/area/ruin/plasma_facility/commons)
-"pn" = (
-/obj/item/kirbyplants{
- icon_state = "applebush"
- },
-/obj/effect/decal/cleanable/dirt,
-/obj/effect/turf_decal/tile/brown/full,
-/turf/open/floor/iron/large,
-/area/ruin/plasma_facility/commons)
"pt" = (
/obj/structure/table,
/obj/item/paper/paperslip{
@@ -1106,14 +1100,6 @@
"rs" = (
/turf/open/lava/plasma/ice_moon,
/area/icemoon/underground/explored)
-"rE" = (
-/obj/effect/spawner/structure/window/hollow/reinforced/directional{
- dir = 8
- },
-/obj/effect/decal/cleanable/glass,
-/obj/structure/window/reinforced/spawner,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"rF" = (
/obj/effect/decal/cleanable/dirt,
/obj/item/wallframe/airalarm{
@@ -1127,16 +1113,18 @@
dir = 1
},
/area/ruin/plasma_facility/commons)
-"rS" = (
-/obj/effect/turf_decal/bot/left,
+"rW" = (
+/obj/structure/lattice/catwalk,
+/obj/structure/railing/corner,
/obj/structure/closet/crate{
icon_state = "crateopen"
},
-/obj/item/stack/sheet/mineral/plasma/thirty,
-/turf/open/floor/iron/smooth_half{
- initial_gas_mix = "ICEMOON_ATMOS"
- },
-/area/ruin/plasma_facility/operations)
+/obj/item/stack/sheet/mineral/uranium/five,
+/obj/item/stack/sheet/mineral/uranium/five,
+/obj/item/stack/sheet/mineral/uranium/five,
+/obj/item/stack/sheet/mineral/uranium/five,
+/turf/open/lava/plasma/ice_moon,
+/area/icemoon/underground/explored)
"sq" = (
/obj/structure/table/reinforced/rglass,
/obj/effect/decal/cleanable/dirt,
@@ -1144,27 +1132,6 @@
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark/textured,
/area/ruin/plasma_facility/commons)
-"sr" = (
-/obj/structure/railing{
- dir = 5
- },
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/sheet/mineral/silver,
-/obj/item/stack/ore/silver,
-/obj/item/stack/ore/silver,
-/obj/item/stack/ore/silver,
-/obj/item/stack/ore/silver,
-/obj/item/stack/ore/silver,
-/obj/item/stack/ore/silver,
-/turf/open/floor/plating/snowed/icemoon,
-/area/icemoon/underground/explored)
"su" = (
/obj/structure/chair/stool/directional/south,
/obj/effect/decal/cleanable/dirt,
@@ -1249,6 +1216,16 @@
},
/turf/open/lava/plasma/ice_moon,
/area/icemoon/underground/explored)
+"ub" = (
+/obj/effect/decal/cleanable/glass,
+/obj/effect/turf_decal/box/corners{
+ dir = 4
+ },
+/mob/living/basic/mining/wolf,
+/turf/open/floor/iron/smooth{
+ initial_gas_mix = "ICEMOON_ATMOS"
+ },
+/area/ruin/plasma_facility/operations)
"ui" = (
/obj/structure/lattice/catwalk,
/obj/structure/railing{
@@ -1268,16 +1245,6 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/turf/open/floor/plating/icemoon,
/area/ruin/plasma_facility/operations)
-"up" = (
-/obj/effect/decal/cleanable/glass,
-/obj/effect/turf_decal/box/corners{
- dir = 4
- },
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/iron/smooth{
- initial_gas_mix = "ICEMOON_ATMOS"
- },
-/area/ruin/plasma_facility/operations)
"uq" = (
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
@@ -1323,11 +1290,6 @@
"vt" = (
/turf/closed/wall,
/area/ruin/plasma_facility/operations)
-"vz" = (
-/obj/effect/decal/cleanable/dirt,
-/obj/machinery/light/floor/has_bulb,
-/turf/open/floor/plating/snowed/icemoon,
-/area/icemoon/underground/explored)
"vI" = (
/obj/structure/fence{
dir = 4
@@ -1439,6 +1401,14 @@
},
/turf/open/floor/iron/edge,
/area/ruin/plasma_facility/commons)
+"yG" = (
+/obj/effect/spawner/structure/window/hollow/reinforced/directional{
+ dir = 8
+ },
+/obj/effect/decal/cleanable/glass,
+/obj/structure/window/reinforced/spawner,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"yL" = (
/obj/structure/girder,
/turf/open/floor/plating/icemoon,
@@ -1625,7 +1595,7 @@
name = "mining conveyor"
},
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer,
+/mob/living/basic/skeleton/plasmaminer,
/obj/effect/turf_decal/tile/brown/opposingcorners,
/turf/open/floor/iron,
/area/ruin/plasma_facility/operations)
@@ -1689,13 +1659,6 @@
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron,
/area/ruin/plasma_facility/commons)
-"DQ" = (
-/obj/item/kirbyplants{
- icon_state = "applebush"
- },
-/obj/effect/turf_decal/tile/brown/full,
-/turf/open/floor/iron/large,
-/area/ruin/plasma_facility/commons)
"DR" = (
/obj/machinery/light/small/broken/directional/north,
/turf/open/lava/plasma/ice_moon,
@@ -1757,18 +1720,6 @@
/obj/machinery/light/small/broken/directional/north,
/turf/open/lava/plasma/ice_moon,
/area/ruin/plasma_facility/operations)
-"FD" = (
-/obj/structure/lattice/catwalk,
-/obj/structure/railing/corner,
-/obj/structure/closet/crate{
- icon_state = "crateopen"
- },
-/obj/item/stack/sheet/mineral/uranium/five,
-/obj/item/stack/sheet/mineral/uranium/five,
-/obj/item/stack/sheet/mineral/uranium/five,
-/obj/item/stack/sheet/mineral/uranium/five,
-/turf/open/lava/plasma/ice_moon,
-/area/icemoon/underground/explored)
"FG" = (
/obj/structure/lattice/catwalk,
/obj/structure/railing{
@@ -1815,6 +1766,22 @@
/obj/item/shard,
/turf/open/floor/plating/icemoon,
/area/ruin/plasma_facility/operations)
+"Gu" = (
+/obj/machinery/door/poddoor/shutters/window/preopen{
+ id = "fire_facility_car";
+ name = "Garage Door"
+ },
+/mob/living/basic/mining/wolf,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
+"Hn" = (
+/obj/structure/grille,
+/obj/structure/window/reinforced/spawner/directional/east,
+/obj/structure/window/reinforced/spawner,
+/obj/item/shard,
+/obj/effect/decal/cleanable/glass,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"Hp" = (
/obj/structure/railing/corner,
/turf/open/lava/plasma/ice_moon,
@@ -1832,10 +1799,6 @@
},
/turf/open/floor/plating/icemoon,
/area/ruin/plasma_facility/commons)
-"Hz" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/plating/snowed/smoothed/icemoon,
-/area/icemoon/underground/explored)
"HA" = (
/obj/effect/turf_decal/siding/wood{
dir = 8
@@ -1885,14 +1848,6 @@
/obj/machinery/light/small/directional/north,
/turf/open/lava/plasma/ice_moon,
/area/ruin/plasma_facility/operations)
-"Ij" = (
-/obj/structure/grille,
-/obj/structure/window/reinforced/spawner/directional/east,
-/obj/structure/window/reinforced/spawner,
-/obj/item/shard,
-/obj/effect/decal/cleanable/glass,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"In" = (
/obj/structure/flora/grass/green/style_random,
/obj/machinery/light/small/broken/directional/east,
@@ -1903,13 +1858,13 @@
/obj/machinery/light/small/broken/directional/south,
/turf/open/lava/plasma/ice_moon,
/area/ruin/plasma_facility/operations)
-"IJ" = (
-/obj/structure/window/reinforced/spawner,
-/obj/effect/decal/cleanable/glass,
-/obj/item/shard,
-/obj/effect/decal/cleanable/dirt,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
+"IP" = (
+/obj/item/kirbyplants{
+ icon_state = "applebush"
+ },
+/obj/effect/turf_decal/tile/brown/full,
+/turf/open/floor/iron/large,
+/area/ruin/plasma_facility/commons)
"IU" = (
/obj/structure/chair/sofa/right/brown{
dir = 4
@@ -1976,7 +1931,7 @@
},
/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2,
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/obj/machinery/light_switch/directional/north,
/turf/open/floor/iron/smooth{
initial_gas_mix = "ICEMOON_ATMOS"
@@ -2037,6 +1992,13 @@
/obj/effect/turf_decal/box/corners,
/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
+"Ku" = (
+/obj/structure/window/reinforced/spawner/directional/north,
+/obj/structure/window/reinforced/spawner,
+/obj/structure/window/reinforced/spawner/directional/east,
+/obj/structure/grille,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"Kw" = (
/obj/effect/turf_decal/siding/wood/corner{
dir = 8
@@ -2175,12 +2137,6 @@
/obj/item/stack/sheet/iron,
/turf/open/floor/plating/snowed/icemoon,
/area/ruin/plasma_facility/operations)
-"MN" = (
-/obj/structure/grille/broken,
-/obj/structure/window/reinforced/spawner,
-/obj/effect/decal/cleanable/glass,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"MO" = (
/obj/structure/lattice/catwalk,
/obj/structure/railing/corner{
@@ -2260,6 +2216,10 @@
},
/turf/open/floor/plating/snowed/icemoon,
/area/icemoon/underground/explored)
+"Np" = (
+/mob/living/basic/mining/wolf,
+/turf/open/misc/asteroid/snow/icemoon,
+/area/icemoon/underground/explored)
"Nw" = (
/obj/machinery/atmospherics/components/binary/valve{
dir = 4;
@@ -2302,7 +2262,7 @@
/obj/effect/decal/cleanable/glass,
/obj/item/shard,
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/turf/open/floor/iron/freezer,
/area/ruin/plasma_facility/commons)
"Or" = (
@@ -2317,6 +2277,11 @@
/obj/item/crowbar/red,
/turf/open/floor/plating/snowed/icemoon,
/area/icemoon/underground/explored)
+"OB" = (
+/obj/structure/window/reinforced/spawner,
+/obj/structure/grille,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/operations)
"OC" = (
/obj/structure/chair/office/light{
dir = 4
@@ -2325,7 +2290,7 @@
dir = 8
},
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/obj/machinery/light_switch/directional/south,
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark/textured,
@@ -2413,6 +2378,30 @@
dir = 1
},
/area/ruin/plasma_facility/commons)
+"PE" = (
+/obj/structure/closet/crate{
+ icon_state = "crateopen"
+ },
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/obj/item/stack/ore/plasma,
+/turf/open/floor/plating/snowed/icemoon,
+/area/icemoon/underground/explored)
"PP" = (
/obj/structure/dresser,
/obj/effect/decal/cleanable/dirt,
@@ -2477,6 +2466,12 @@
/obj/item/screwdriver,
/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
+"QH" = (
+/obj/structure/grille,
+/obj/structure/window/reinforced/spawner,
+/obj/structure/window/reinforced/spawner/directional/east,
+/turf/open/floor/plating/icemoon,
+/area/ruin/plasma_facility/commons)
"QJ" = (
/obj/machinery/computer/order_console/mining,
/obj/effect/decal/cleanable/dirt,
@@ -2583,14 +2578,6 @@
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/plating/icemoon,
/area/ruin/plasma_facility/commons)
-"SV" = (
-/obj/machinery/atmospherics/components/unary/portables_connector{
- dir = 4
- },
-/obj/effect/decal/cleanable/dirt,
-/obj/structure/canister_frame/machine,
-/turf/open/floor/plating/snowed/icemoon,
-/area/icemoon/underground/explored)
"Tr" = (
/obj/effect/turf_decal/tile/brown/half{
dir = 1
@@ -2664,14 +2651,6 @@
/obj/structure/flora/tree/stump,
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
-"Vy" = (
-/obj/machinery/door/poddoor/shutters/window/preopen{
- id = "fire_facility_car";
- name = "Garage Door"
- },
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/plating/icemoon,
-/area/ruin/plasma_facility/operations)
"VD" = (
/obj/effect/turf_decal/tile/brown/half,
/obj/effect/turf_decal/tile/neutral/half/contrasted{
@@ -2701,6 +2680,14 @@
/obj/structure/grille,
/turf/open/floor/plating/icemoon,
/area/ruin/plasma_facility/commons)
+"Wi" = (
+/obj/machinery/atmospherics/components/unary/portables_connector{
+ dir = 4
+ },
+/obj/effect/decal/cleanable/dirt,
+/obj/structure/canister_frame/machine,
+/turf/open/floor/plating/snowed/icemoon,
+/area/icemoon/underground/explored)
"Wu" = (
/turf/open/floor/wood/large,
/area/ruin/plasma_facility/commons)
@@ -2815,6 +2802,19 @@
/obj/effect/turf_decal/tile/brown/full,
/turf/open/floor/iron/textured_large,
/area/ruin/plasma_facility/commons)
+"Yk" = (
+/obj/structure/window/reinforced/spawner,
+/obj/structure/frame/computer{
+ anchored = 1;
+ dir = 4
+ },
+/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
+/obj/effect/decal/cleanable/dirt,
+/turf/open/floor/iron/smooth{
+ initial_gas_mix = "ICEMOON_ATMOS"
+ },
+/area/ruin/plasma_facility/operations)
"Yl" = (
/obj/structure/flora/rock/pile/icy/style_random,
/turf/open/misc/asteroid/snow/icemoon,
@@ -3135,7 +3135,7 @@ Yl
vI
Yw
ms
-Hz
+gi
Yw
Yw
Yw
@@ -3208,11 +3208,11 @@ hJ
Py
Yw
hJ
-gd
+Np
Yw
XI
wb
-hN
+QH
nD
Km
Yw
@@ -3238,7 +3238,7 @@ AG
AG
PQ
vI
-Hz
+gi
Yw
Yw
Yw
@@ -3321,7 +3321,7 @@ lW
pl
Mc
af
-rS
+bj
hd
vt
hR
@@ -3333,7 +3333,7 @@ Ve
ms
VT
Yw
-Hz
+gi
ms
Yw
vY
@@ -3355,7 +3355,7 @@ PB
bM
VD
gj
-ac
+Ku
pX
Ib
js
@@ -3404,7 +3404,7 @@ ms
hR
Dr
Dr
-Vy
+Gu
yL
vY
"}
@@ -3424,7 +3424,7 @@ pw
Zk
eC
bv
-DQ
+IP
hR
OJ
go
@@ -3475,7 +3475,7 @@ vt
FO
LD
FO
-MN
+ng
vY
"}
(16,1,1) = {"
@@ -3490,7 +3490,7 @@ hK
ZH
wF
Cm
-pn
+ns
PB
eC
yv
@@ -3507,7 +3507,7 @@ AZ
dO
DZ
Jb
-up
+ub
FO
ia
HX
@@ -3685,7 +3685,7 @@ ms
eD
tG
Zf
-ev
+fc
AG
"}
(22,1,1) = {"
@@ -3849,7 +3849,7 @@ wm
XA
Rj
Gb
-kA
+PE
rk
fO
rs
@@ -3885,7 +3885,7 @@ rk
hR
DZ
ie
-SV
+Wi
AT
rs
rs
@@ -3919,7 +3919,7 @@ KV
ie
TY
ie
-vz
+ea
HS
oc
rs
@@ -3991,7 +3991,7 @@ XK
ak
IU
Dp
-FD
+rW
Ee
rs
rs
@@ -4022,7 +4022,7 @@ rs
tX
fN
Na
-vz
+ea
rk
hR
qP
@@ -4056,7 +4056,7 @@ Ue
rs
Qn
rs
-sr
+aZ
Mz
vt
ed
@@ -4194,7 +4194,7 @@ NX
vt
no
ly
-eM
+jI
rs
rs
rs
@@ -4227,9 +4227,9 @@ rs
rs
af
lX
-kh
+Yk
mA
-jT
+OB
rs
rs
rs
@@ -4299,7 +4299,7 @@ oi
qw
Or
hc
-IJ
+im
rs
rs
rs
@@ -4330,7 +4330,7 @@ AG
AG
rs
rs
-Ij
+Hn
qw
qw
vt
@@ -4368,7 +4368,7 @@ rs
Pz
ul
Kx
-rE
+yG
Pz
rs
rs
diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_village.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_village.dmm
index 8d851421f1cd..939ddf92b5ef 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_village.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_village.dmm
@@ -108,7 +108,7 @@
"la" = (
/obj/effect/decal/cleanable/blood/gibs/up,
/obj/effect/mob_spawn/corpse/human/assistant,
-/mob/living/simple_animal/hostile/skeleton/eskimo{
+/mob/living/basic/skeleton/settler{
name = "Village Hunter"
},
/turf/open/misc/asteroid/snow/icemoon,
@@ -135,7 +135,7 @@
/area/icemoon/underground/explored)
"mi" = (
/obj/effect/decal/remains/human,
-/mob/living/simple_animal/hostile/construct/juggernaut/hostile{
+/mob/living/basic/construct/juggernaut/hostile{
health = 450;
maxHealth = 450;
name = "Right Hand of the Elder"
@@ -360,7 +360,7 @@
"EF" = (
/obj/effect/decal/cleanable/blood/gibs/torso,
/obj/effect/decal/remains/human,
-/mob/living/simple_animal/hostile/construct/juggernaut/hostile{
+/mob/living/basic/construct/juggernaut/hostile{
health = 450;
maxHealth = 450;
name = "Left Hand of the Elder"
diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_frozen_comms.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_frozen_comms.dmm
index 03bd2ad2a5d6..77db984ba4ea 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_frozen_comms.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_frozen_comms.dmm
@@ -1,4 +1,8 @@
//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"al" = (
+/mob/living/basic/mining/wolf,
+/turf/open/floor/plating/icemoon,
+/area/ruin/powered/shuttle)
"aD" = (
/obj/structure/fence/cut/medium,
/turf/open/misc/asteroid/snow/icemoon,
@@ -6,10 +10,6 @@
"aU" = (
/turf/open/genturf,
/area/icemoon/underground/unexplored/rivers)
-"bh" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/plating/snowed/smoothed/icemoon,
-/area/icemoon/underground/explored)
"cv" = (
/obj/structure/flora/rock/pile/icy/style_random,
/obj/machinery/light/small/directional/north,
@@ -43,6 +43,12 @@
initial_gas_mix = "ICEMOON_ATMOS"
},
/area/ruin/powered/shuttle)
+"fD" = (
+/mob/living/basic/mining/wolf,
+/turf/open/floor/iron/showroomfloor{
+ initial_gas_mix = "ICEMOON_ATMOS"
+ },
+/area/ruin/powered/shuttle)
"gz" = (
/turf/closed/wall/ice,
/area/ruin/powered/shuttle)
@@ -68,12 +74,6 @@
initial_gas_mix = "ICEMOON_ATMOS"
},
/area/ruin/powered/shuttle)
-"oD" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/iron/showroomfloor{
- initial_gas_mix = "ICEMOON_ATMOS"
- },
-/area/ruin/powered/shuttle)
"oT" = (
/turf/open/floor/iron/showroomfloor{
initial_gas_mix = "ICEMOON_ATMOS"
@@ -98,10 +98,6 @@
initial_gas_mix = "ICEMOON_ATMOS"
},
/area/ruin/powered/shuttle)
-"rn" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/turf/open/floor/plating/icemoon,
-/area/ruin/powered/shuttle)
"rS" = (
/obj/structure/flora/tree/pine/style_random,
/obj/structure/flora/grass/green/style_random,
@@ -130,6 +126,13 @@
},
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
+"tx" = (
+/obj/structure/window/reinforced/spawner,
+/obj/structure/grille,
+/obj/item/shard,
+/obj/effect/decal/cleanable/glass,
+/turf/open/floor/plating/icemoon,
+/area/ruin/powered/shuttle)
"uz" = (
/obj/structure/tank_dispenser,
/turf/open/floor/iron/smooth{
@@ -226,7 +229,7 @@
/obj/structure/fence/cut/large,
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
-"CH" = (
+"DV" = (
/obj/structure/window/reinforced/spawner,
/obj/effect/spawner/structure/window/hollow/reinforced/directional{
dir = 1
@@ -248,11 +251,6 @@
},
/turf/open/floor/plating/icemoon,
/area/ruin/powered/shuttle)
-"FK" = (
-/mob/living/simple_animal/hostile/asteroid/wolf,
-/obj/machinery/light/small/broken/directional/north,
-/turf/open/floor/plating/snowed/smoothed/icemoon,
-/area/ruin/powered/shuttle)
"FP" = (
/obj/effect/spawner/structure/window/hollow/reinforced/middle{
dir = 4
@@ -276,12 +274,10 @@
/obj/effect/decal/cleanable/glass,
/turf/open/floor/plating/icemoon,
/area/ruin/powered/shuttle)
-"GQ" = (
-/obj/structure/window/reinforced/spawner,
-/obj/structure/grille,
-/obj/item/shard,
-/obj/effect/decal/cleanable/glass,
-/turf/open/floor/plating/icemoon,
+"GX" = (
+/mob/living/basic/mining/wolf,
+/obj/machinery/light/small/broken/directional/north,
+/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/ruin/powered/shuttle)
"HT" = (
/obj/structure/closet/wardrobe/grey,
@@ -347,6 +343,10 @@
},
/turf/open/floor/plating/icemoon,
/area/ruin/powered/shuttle)
+"Ok" = (
+/mob/living/basic/mining/wolf,
+/turf/open/floor/plating/snowed/smoothed/icemoon,
+/area/icemoon/underground/explored)
"OM" = (
/obj/effect/turf_decal/stripes/line{
dir = 8
@@ -530,7 +530,7 @@ aU
aU
qS
TG
-CH
+DV
Gl
gz
Cr
@@ -553,7 +553,7 @@ GH
NO
MS
oT
-oD
+fD
oT
oT
eC
@@ -568,7 +568,7 @@ qS
aU
qS
qS
-GQ
+tx
oj
gz
wJ
@@ -613,7 +613,7 @@ Et
gz
SR
wP
-bh
+Ok
qS
th
yl
@@ -645,7 +645,7 @@ aU
aU
gz
wg
-rn
+al
Qj
te
RK
@@ -668,7 +668,7 @@ OM
yW
Kh
gz
-FK
+GX
wP
qS
wP
diff --git a/_maps/RandomRuins/LavaRuins/lavaland_biodome_clown_planet.dmm b/_maps/RandomRuins/LavaRuins/lavaland_biodome_clown_planet.dmm
index 7ab354935c39..ef650913f141 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_biodome_clown_planet.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_biodome_clown_planet.dmm
@@ -671,7 +671,7 @@
/area/ruin/powered/clownplanet)
"RW" = (
/obj/effect/mapping_helpers/no_lava,
-/mob/living/simple_animal/hostile/retaliate/clown,
+/mob/living/basic/clown,
/turf/open/floor/noslip,
/area/ruin/powered/clownplanet)
"Tj" = (
@@ -714,7 +714,7 @@
"YI" = (
/obj/machinery/light/small/directional/south,
/obj/effect/mapping_helpers/no_lava,
-/mob/living/simple_animal/hostile/retaliate/clown,
+/mob/living/basic/clown,
/turf/open/floor/noslip,
/area/ruin/powered/clownplanet)
"Zg" = (
diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
index d63d261b813c..47fe3152a8b5 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_ash_walker1.dmm
@@ -213,7 +213,7 @@
dir = 1
},
/obj/structure/stone_tile,
-/mob/living/simple_animal/hostile/asteroid/gutlunch/gubbuck,
+/mob/living/basic/mining/gutlunch/milk,
/turf/open/indestructible/boss,
/area/ruin/unpowered/ash_walkers)
"aR" = (
@@ -287,7 +287,7 @@
/obj/structure/stone_tile{
dir = 4
},
-/mob/living/simple_animal/hostile/asteroid/gutlunch/guthen,
+/mob/living/basic/mining/gutlunch/warrior,
/turf/open/indestructible/boss,
/area/ruin/unpowered/ash_walkers)
"bd" = (
diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_biodome_winter.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_biodome_winter.dmm
index f83887d9b427..a49846f2b0a6 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_surface_biodome_winter.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_biodome_winter.dmm
@@ -148,7 +148,7 @@
/turf/open/floor/wood,
/area/ruin/powered/snow_biodome)
"aH" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/floor/wood,
/area/ruin/powered/snow_biodome)
"aI" = (
@@ -258,11 +258,11 @@
/area/ruin/powered/snow_biodome)
"gz" = (
/obj/structure/chair/stool/directional/south,
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/floor/pod/dark,
/area/ruin/powered/snow_biodome)
"hr" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/obj/effect/turf_decal/siding/wood{
dir = 1
},
diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_mookvillage.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_mookvillage.dmm
new file mode 100644
index 000000000000..01891e8ef3dd
--- /dev/null
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_mookvillage.dmm
@@ -0,0 +1,620 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"aa" = (
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"ab" = (
+/obj/structure/bonfire/prelit,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"ad" = (
+/obj/structure/railing{
+ dir = 1
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"ae" = (
+/obj/structure/flora/ash/fireblossom,
+/mob/living/basic/mining/mook/worker/tribal_chief,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"ah" = (
+/turf/closed/wall/mineral/wood,
+/area/lavaland/surface/outdoors)
+"al" = (
+/obj/structure/flora/ash/fireblossom,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"an" = (
+/obj/structure/flora/ash/fireblossom,
+/mob/living/basic/mining/mook/worker,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"as" = (
+/obj/effect/landmark/mook_village,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"at" = (
+/obj/structure/chair/pew/left,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"au" = (
+/obj/structure/railing,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aw" = (
+/obj/structure/railing{
+ dir = 4
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"az" = (
+/mob/living/basic/mining/mook/worker/bard,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aA" = (
+/obj/structure/chair/pew/right,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aB" = (
+/turf/open/misc/grass/jungle,
+/area/lavaland/surface/outdoors)
+"aC" = (
+/obj/structure/sign/nanotrasen,
+/turf/closed/wall/mineral/wood,
+/area/lavaland/surface/outdoors)
+"aD" = (
+/obj/item/flashlight/lantern{
+ on = 1
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aF" = (
+/obj/structure/chair/pew,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aI" = (
+/mob/living/basic/mining/mook,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aJ" = (
+/obj/structure/railing{
+ dir = 8
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aK" = (
+/obj/structure/table/wood/shuttle_bar,
+/obj/item/hatchet/wooden,
+/obj/item/storage/belt/champion/wrestling,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"aL" = (
+/obj/item/flashlight/lantern{
+ on = 1
+ },
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"aM" = (
+/obj/structure/chair/pew/right{
+ dir = 8
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aN" = (
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"aP" = (
+/mob/living/basic/mining/mook/worker,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aR" = (
+/obj/structure/bed/pod,
+/obj/item/bedsheet/nanotrasen,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"aT" = (
+/obj/structure/flora/tree/jungle/style_5,
+/turf/open/misc/grass/jungle,
+/area/lavaland/surface/outdoors)
+"aU" = (
+/obj/structure/table/wood/shuttle_bar,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"aV" = (
+/obj/structure/chair/pew{
+ dir = 8
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"aX" = (
+/obj/structure/flora/grass/jungle/b,
+/turf/open/misc/grass/jungle,
+/area/lavaland/surface/outdoors)
+"aY" = (
+/obj/structure/chair/pew/left{
+ dir = 8
+ },
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"eN" = (
+/obj/structure/table/wood/shuttle_bar,
+/obj/item/flashlight/flare/candle/infinite,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"hi" = (
+/turf/open/floor/carpet/lone,
+/area/lavaland/surface/outdoors)
+"sk" = (
+/obj/structure/bed/maint,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"xb" = (
+/obj/structure/chair/wood{
+ dir = 1
+ },
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"xv" = (
+/obj/item/fishing_rod,
+/obj/structure/bed/maint,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"Br" = (
+/obj/structure/ore_container/material_stand,
+/turf/open/misc/asteroid/basalt/lava_land_surface,
+/area/lavaland/surface/outdoors)
+"Jh" = (
+/obj/structure/closet/cabinet,
+/obj/item/food/grown/banana,
+/obj/item/food/meat/slab/goliath,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+"Mn" = (
+/obj/structure/flora/tree/jungle/style_3,
+/turf/open/misc/grass/jungle,
+/area/lavaland/surface/outdoors)
+"WL" = (
+/obj/structure/chair/wood,
+/turf/open/floor/holofloor/wood,
+/area/lavaland/surface/outdoors)
+
+(1,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+al
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+"}
+(2,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+al
+aD
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+at
+aa
+aa
+aa
+aa
+"}
+(3,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+Br
+aa
+aa
+aa
+aa
+aF
+aa
+aa
+aa
+aa
+"}
+(4,1,1) = {"
+ah
+ah
+ah
+ah
+ah
+ah
+aa
+as
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aF
+aa
+ab
+aa
+aa
+"}
+(5,1,1) = {"
+aN
+aN
+Jh
+sk
+aN
+aN
+aa
+aD
+aa
+aa
+aa
+aa
+aP
+aa
+aD
+aF
+aa
+aa
+aa
+aa
+"}
+(6,1,1) = {"
+aN
+aL
+aN
+aN
+aN
+aN
+hi
+an
+aa
+aP
+aa
+aa
+aa
+aa
+aa
+aA
+aa
+aa
+aa
+aa
+"}
+(7,1,1) = {"
+aN
+aN
+aN
+aN
+aN
+aN
+hi
+aa
+aI
+aa
+aa
+aa
+aa
+aa
+al
+ae
+aY
+aV
+aV
+aM
+"}
+(8,1,1) = {"
+aN
+WL
+eN
+xb
+aN
+aN
+aa
+aa
+aa
+aa
+aw
+aw
+aw
+aa
+aI
+aa
+aa
+aa
+aa
+aa
+"}
+(9,1,1) = {"
+ah
+ah
+ah
+ah
+ah
+ah
+aa
+aa
+aa
+au
+aB
+aB
+aT
+ad
+aa
+aa
+aa
+aP
+aa
+aa
+"}
+(10,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aI
+aa
+au
+aB
+aB
+aX
+ad
+aa
+aa
+al
+aa
+aa
+aa
+"}
+(11,1,1) = {"
+aa
+aa
+al
+al
+aa
+aa
+aa
+aa
+aa
+au
+aB
+Mn
+aX
+ad
+aa
+aa
+aa
+aa
+aa
+aa
+"}
+(12,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aP
+aa
+aa
+aa
+au
+aB
+aB
+aT
+ad
+aa
+aa
+aa
+aa
+aa
+aa
+"}
+(13,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aJ
+aJ
+aJ
+aa
+aa
+aa
+aP
+aa
+aa
+aa
+"}
+(14,1,1) = {"
+ah
+ah
+ah
+ah
+ah
+ah
+ah
+az
+aD
+al
+aa
+aa
+aI
+al
+al
+aa
+aI
+aa
+aa
+al
+"}
+(15,1,1) = {"
+sk
+sk
+sk
+sk
+xv
+sk
+sk
+al
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+"}
+(16,1,1) = {"
+aN
+aN
+aN
+aN
+aN
+aN
+aN
+hi
+aa
+aa
+aa
+aa
+ah
+aN
+aN
+aN
+ah
+aa
+aa
+aa
+"}
+(17,1,1) = {"
+aN
+aN
+aN
+aL
+aN
+aN
+aN
+hi
+aa
+aP
+aa
+aa
+ah
+aK
+aN
+aN
+ah
+aa
+aa
+aa
+"}
+(18,1,1) = {"
+sk
+sk
+sk
+sk
+sk
+sk
+sk
+aa
+aa
+aa
+aa
+aa
+aC
+aU
+aN
+aN
+ah
+aa
+aa
+aa
+"}
+(19,1,1) = {"
+ah
+ah
+ah
+ah
+ah
+ah
+ah
+aa
+aa
+al
+aa
+aD
+ah
+aL
+aN
+aR
+ah
+aa
+aa
+aa
+"}
+(20,1,1) = {"
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+aa
+ah
+aN
+aN
+aN
+ah
+aa
+aa
+aa
+"}
diff --git a/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm b/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
index a68da2a99759..8670ad8113ce 100644
--- a/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
+++ b/_maps/RandomRuins/SpaceRuins/atmosasteroidruin.dmm
@@ -604,7 +604,7 @@
/obj/effect/turf_decal/tile/yellow/half{
dir = 4
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/iron/co2_pressurized,
/area/ruin/space/has_grav/atmosasteroid)
"RK" = (
diff --git a/_maps/RandomRuins/SpaceRuins/caravanambush.dmm b/_maps/RandomRuins/SpaceRuins/caravanambush.dmm
index bf0972ae3118..9641778ac2cf 100644
--- a/_maps/RandomRuins/SpaceRuins/caravanambush.dmm
+++ b/_maps/RandomRuins/SpaceRuins/caravanambush.dmm
@@ -749,7 +749,7 @@
dir = 4
},
/obj/effect/decal/cleanable/dirt,
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/turf/open/floor/iron/airless,
/area/shuttle/ruin/caravan/freighter3)
"Cj" = (
@@ -1009,7 +1009,7 @@
/area/shuttle/ruin/caravan/freighter3)
"PF" = (
/obj/effect/decal/cleanable/dirt,
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/turf/open/floor/iron/airless,
/area/shuttle/ruin/caravan/freighter3)
"PY" = (
@@ -1165,7 +1165,7 @@
/obj/effect/turf_decal/box/white/corners{
dir = 4
},
-/mob/living/basic/syndicate/ranged/shotgun/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/stormtrooper,
/turf/open/floor/iron/dark/airless,
/area/shuttle/ruin/caravan/freighter3)
"Zz" = (
diff --git a/_maps/RandomRuins/SpaceRuins/clericden.dmm b/_maps/RandomRuins/SpaceRuins/clericden.dmm
index 0bf5a3a05798..5b1782559aed 100644
--- a/_maps/RandomRuins/SpaceRuins/clericden.dmm
+++ b/_maps/RandomRuins/SpaceRuins/clericden.dmm
@@ -131,7 +131,7 @@
/area/ruin/space)
"E" = (
/obj/effect/decal/cleanable/blood/tracks,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/turf/open/floor/carpet/airless,
/area/ruin/space)
"F" = (
@@ -171,7 +171,7 @@
/turf/open/floor/plating/airless,
/area/ruin/space)
"N" = (
-/mob/living/simple_animal/hostile/construct/proteon,
+/mob/living/basic/construct/proteon,
/turf/open/misc/asteroid/airless,
/area/ruin/space)
"O" = (
diff --git a/_maps/RandomRuins/SpaceRuins/clownplanet.dmm b/_maps/RandomRuins/SpaceRuins/clownplanet.dmm
index b0cc8cc29e20..fa0b50cc4435 100644
--- a/_maps/RandomRuins/SpaceRuins/clownplanet.dmm
+++ b/_maps/RandomRuins/SpaceRuins/clownplanet.dmm
@@ -26,7 +26,7 @@
/turf/open/floor/engine,
/area/ruin/space/has_grav/powered/clownplanet)
"ah" = (
-/mob/living/simple_animal/hostile/retaliate/clown/clownhulk/destroyer,
+/mob/living/basic/clown/clownhulk/destroyer,
/turf/open/floor/engine,
/area/ruin/space/has_grav/powered/clownplanet)
"ai" = (
diff --git a/_maps/RandomRuins/SpaceRuins/dangerous_research.dmm b/_maps/RandomRuins/SpaceRuins/dangerous_research.dmm
index da21f9ffb4a0..52b7347f1353 100644
--- a/_maps/RandomRuins/SpaceRuins/dangerous_research.dmm
+++ b/_maps/RandomRuins/SpaceRuins/dangerous_research.dmm
@@ -1334,10 +1334,7 @@
/turf/open/floor/iron/dark,
/area/ruin/space/has_grav/dangerous_research/lab)
"re" = (
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet{
- AIStatus = 1;
- stop_automated_movement = 0
- },
+/mob/living/basic/heretic_summon/raw_prophet/ruins,
/turf/open/floor/plating/rust,
/area/ruin/space/has_grav/dangerous_research/medical)
"ri" = (
@@ -2017,10 +2014,7 @@
/obj/effect/decal/cleanable/glass,
/obj/item/shard,
/obj/item/stack/sheet/iron,
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit{
- AIStatus = 1;
- stop_automated_movement = 0
- },
+/mob/living/basic/heretic_summon/rust_walker,
/turf/open/floor/plating/rust,
/area/ruin/space/has_grav/dangerous_research/lab)
"zR" = (
@@ -2050,10 +2044,7 @@
dir = 1
},
/obj/effect/decal/cleanable/blood/footprints,
-/mob/living/simple_animal/hostile/heretic_summon/raw_prophet{
- AIStatus = 1;
- stop_automated_movement = 0
- },
+/mob/living/basic/heretic_summon/raw_prophet/ruins,
/obj/effect/turf_decal/tile/blue/fourcorners,
/turf/open/floor/iron/white,
/area/ruin/space/has_grav/dangerous_research/medical)
@@ -3751,10 +3742,7 @@
/turf/open/floor/engine/airless,
/area/ruin/space/has_grav/dangerous_research/maint)
"Xh" = (
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit{
- AIStatus = 1;
- stop_automated_movement = 0
- },
+/mob/living/basic/heretic_summon/rust_walker,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/obj/structure/cable,
@@ -3896,10 +3884,7 @@
/turf/open/floor/iron/white,
/area/ruin/space/has_grav/dangerous_research/medical)
"YZ" = (
-/mob/living/simple_animal/hostile/heretic_summon/rust_spirit{
- AIStatus = 1;
- stop_automated_movement = 0
- },
+/mob/living/basic/heretic_summon/rust_walker,
/turf/open/floor/plating/rust,
/area/ruin/space/has_grav/dangerous_research/lab)
"Zc" = (
diff --git a/_maps/RandomRuins/SpaceRuins/forgottenship.dmm b/_maps/RandomRuins/SpaceRuins/forgottenship.dmm
index 27626b3466d4..fb7e04b2d32e 100644
--- a/_maps/RandomRuins/SpaceRuins/forgottenship.dmm
+++ b/_maps/RandomRuins/SpaceRuins/forgottenship.dmm
@@ -295,7 +295,7 @@
/area/ruin/space/has_grav/syndicate_forgotten_cargopod)
"bc" = (
/obj/machinery/light/directional/south,
-/mob/living/simple_animal/hostile/nanotrasen/ranged/assault,
+/mob/living/basic/trooper/nanotrasen/ranged/assault,
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/space/has_grav/syndicate_forgotten_ship)
"bd" = (
@@ -657,7 +657,7 @@
/area/ruin/space/has_grav/syndicate_forgotten_ship)
"cd" = (
/obj/machinery/light/directional/south,
-/mob/living/simple_animal/hostile/nanotrasen/ranged/assault,
+/mob/living/basic/trooper/nanotrasen/ranged/assault,
/turf/open/floor/iron/dark,
/area/ruin/space/has_grav/syndicate_forgotten_ship)
"ce" = (
@@ -812,7 +812,7 @@
dir = 10
},
/obj/item/wrench,
-/mob/living/simple_animal/hostile/nanotrasen/ranged/assault,
+/mob/living/basic/trooper/nanotrasen/ranged/assault,
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/space/has_grav/syndicate_forgotten_ship)
"cz" = (
@@ -971,7 +971,7 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
-/mob/living/simple_animal/hostile/nanotrasen/elite,
+/mob/living/basic/trooper/nanotrasen/ranged/elite,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/syndicate_forgotten_ship)
"cR" = (
diff --git a/_maps/RandomRuins/SpaceRuins/prey_pod.dmm b/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
index 9089dea664c0..6f53409b8735 100644
--- a/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
+++ b/_maps/RandomRuins/SpaceRuins/prey_pod.dmm
@@ -17,7 +17,7 @@
/obj/structure/chair/comfy/shuttle{
dir = 4
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"p" = (
@@ -32,7 +32,7 @@
/area/ruin/space/has_grav)
"D" = (
/obj/structure/chair/comfy/shuttle,
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"E" = (
@@ -57,7 +57,7 @@
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"S" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/hivelord,
/turf/open/floor/mineral/titanium/white/airless,
/area/ruin/space/has_grav)
"T" = (
diff --git a/_maps/RandomRuins/SpaceRuins/spacehotel.dmm b/_maps/RandomRuins/SpaceRuins/spacehotel.dmm
index cafa74f8d3f5..e1bfb59abaa2 100644
--- a/_maps/RandomRuins/SpaceRuins/spacehotel.dmm
+++ b/_maps/RandomRuins/SpaceRuins/spacehotel.dmm
@@ -772,7 +772,7 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/obj/machinery/light/directional/east,
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/turf/open/floor/iron/dark/textured,
/area/ruin/space/has_grav/hotel/security)
"gQ" = (
@@ -875,7 +875,7 @@
"hH" = (
/obj/structure/cable,
/obj/machinery/airalarm/directional/north,
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/turf/open/floor/glass/reinforced,
/area/ruin/space/has_grav/hotel/security)
"hI" = (
@@ -1645,7 +1645,7 @@
/obj/effect/turf_decal/trimline/yellow/warning{
dir = 10
},
-/mob/living/basic/syndicate/ranged/shotgun/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/stormtrooper,
/obj/effect/decal/cleanable/blood/drip,
/obj/effect/decal/cleanable/blood,
/turf/open/floor/iron/dark/textured,
@@ -1907,7 +1907,7 @@
/area/ruin/space/has_grav/hotel/security)
"pr" = (
/obj/effect/decal/cleanable/dirt/dust,
-/mob/living/basic/syndicate/melee/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/space/stormtrooper,
/turf/open/floor/iron/dark/textured_large,
/area/ruin/space/has_grav/hotel/dock)
"pz" = (
@@ -1993,7 +1993,7 @@
/turf/open/misc/beach/sand,
/area/ruin/space/has_grav/hotel/pool)
"qx" = (
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/turf/open/floor/glass/reinforced,
/area/ruin/space/has_grav/hotel/security)
"qz" = (
diff --git a/_maps/RandomRuins/SpaceRuins/the_faceoff.dmm b/_maps/RandomRuins/SpaceRuins/the_faceoff.dmm
index dd6656c22cdd..1c727e56d91c 100644
--- a/_maps/RandomRuins/SpaceRuins/the_faceoff.dmm
+++ b/_maps/RandomRuins/SpaceRuins/the_faceoff.dmm
@@ -174,8 +174,7 @@
/turf/open/floor/iron/dark/airless,
/area/ruin/space)
"eI" = (
-/obj/item/ammo_casing/a357,
-/mob/living/basic/syndicate/melee/space,
+/mob/living/basic/trooper/syndicate/melee/space,
/turf/open/floor/iron/dark/airless,
/area/ruin/space)
"eU" = (
@@ -699,7 +698,14 @@
/obj/effect/decal/cleanable/dirt/dust,
/obj/effect/turf_decal/stripes/full,
/obj/item/grenade/c4,
-/mob/living/simple_animal/hostile/mining_drone,
+/obj/effect/decal/cleanable/dirt,
+/obj/effect/mob_spawn/corpse/human/russian,
+/obj/effect/decal/cleanable/blood,
+/obj/effect/decal/cleanable/dirt,
+/obj/effect/decal/cleanable/dirt,
+/obj/effect/turf_decal/stripes/full,
+/mob/living/basic/mining_drone,
+/obj/item/grenade/c4,
/turf/open/misc/asteroid/basalt/airless,
/area/ruin/space)
"Do" = (
diff --git a/_maps/RandomRuins/SpaceRuins/the_outlet.dmm b/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
index 44721abb8966..05a9867e8ca7 100644
--- a/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
+++ b/_maps/RandomRuins/SpaceRuins/the_outlet.dmm
@@ -103,7 +103,7 @@
/turf/open/floor/eighties,
/area/ruin/space/has_grav/the_outlet/storefront)
"cK" = (
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/machinery/light/small/directional/west,
/obj/effect/decal/cleanable/blood{
icon_state = "floor2-old"
@@ -1064,7 +1064,7 @@
/obj/effect/decal/cleanable/dirt{
icon_state = "dirt-4"
},
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune6"
},
@@ -1238,7 +1238,7 @@
/area/ruin/space/has_grav/the_outlet/storefront)
"Db" = (
/obj/effect/decal/cleanable/crayon,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/turf/open/floor/cult,
/area/ruin/space/has_grav/the_outlet/cultinfluence)
"Do" = (
@@ -1493,7 +1493,7 @@
/obj/effect/decal/cleanable/crayon{
icon_state = "rune2"
},
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/turf/open/floor/iron/white,
/area/ruin/space/has_grav/the_outlet/researchrooms)
"IF" = (
@@ -1585,7 +1585,7 @@
/turf/template_noop,
/area/template_noop)
"LL" = (
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune2"
},
@@ -1645,7 +1645,7 @@
/obj/effect/decal/cleanable/blood{
icon_state = "gib2-old"
},
-/mob/living/simple_animal/hostile/construct/juggernaut/hostile,
+/mob/living/basic/construct/juggernaut/hostile,
/turf/open/floor/cult,
/area/ruin/space/has_grav/the_outlet/cultinfluence)
"MR" = (
@@ -1686,7 +1686,7 @@
/obj/effect/decal/cleanable/crayon{
icon_state = "rune4"
},
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/turf/open/floor/cult,
/area/ruin/space/has_grav/the_outlet/cultinfluence)
"NE" = (
@@ -2013,7 +2013,7 @@
dir = 8
},
/obj/effect/decal/cleanable/crayon,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/dirt{
icon_state = "dirt-127"
},
@@ -2142,7 +2142,7 @@
/turf/open/floor/iron/white,
/area/ruin/space/has_grav/the_outlet/researchrooms)
"Yr" = (
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon,
/turf/open/floor/cult,
/area/ruin/space/has_grav/the_outlet/cultinfluence)
diff --git a/_maps/RandomRuins/SpaceRuins/waystation.dmm b/_maps/RandomRuins/SpaceRuins/waystation.dmm
index 6dac443f81ab..9f4c138fec8c 100644
--- a/_maps/RandomRuins/SpaceRuins/waystation.dmm
+++ b/_maps/RandomRuins/SpaceRuins/waystation.dmm
@@ -73,7 +73,7 @@
/turf/open/floor/plating,
/area/ruin/space/has_grav/waystation)
"bA" = (
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/turf/open/floor/iron/dark,
/area/ruin/space/has_grav/waystation/securestorage)
"bG" = (
@@ -1960,7 +1960,7 @@
/turf/open/floor/plating,
/area/ruin/space/has_grav/waystation/cargobay)
"JM" = (
-/mob/living/basic/syndicate/ranged/shotgun/space,
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space,
/turf/open/floor/iron,
/area/ruin/space/has_grav/waystation/cargobay)
"Kc" = (
@@ -2120,7 +2120,7 @@
"LW" = (
/obj/effect/turf_decal/loading_area/red,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/obj/structure/cable,
/turf/open/floor/iron,
/area/ruin/space/has_grav/waystation/cargobay)
@@ -2562,19 +2562,7 @@
/turf/open/floor/iron,
/area/ruin/space/has_grav/waystation/cargobay)
"SJ" = (
-/mob/living/basic/syndicate/ranged/shotgun/space,
-/obj/item/ammo_casing/shotgun/buckshot{
- pixel_x = 6;
- pixel_y = 1
- },
-/obj/item/ammo_casing/shotgun/buckshot{
- pixel_x = 3;
- pixel_y = 10
- },
-/obj/item/ammo_casing/shotgun/buckshot{
- pixel_x = -3;
- pixel_y = 1
- },
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space,
/turf/open/floor/iron,
/area/ruin/space/has_grav/waystation/dorms)
"SL" = (
@@ -2686,7 +2674,7 @@
/area/ruin/space/has_grav/waystation/dorms)
"UC" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/obj/structure/cable,
/turf/open/floor/iron,
/area/ruin/space/has_grav/waystation/cargooffice)
diff --git a/_maps/RandomZLevels/SnowCabin.dmm b/_maps/RandomZLevels/SnowCabin.dmm
index e9661b18531c..f28f8e70e8d1 100644
--- a/_maps/RandomZLevels/SnowCabin.dmm
+++ b/_maps/RandomZLevels/SnowCabin.dmm
@@ -3044,12 +3044,11 @@
/obj/effect/decal/cleanable/dirt,
/obj/effect/decal/cleanable/blood,
/obj/effect/decal/cleanable/blood/gibs,
-/mob/living/simple_animal/hostile/skeleton{
+/mob/living/basic/skeleton{
desc = "Oh shit!";
dir = 1;
faction = list("sewer");
- name = "sewer skeleton";
- wander = 0
+ name = "sewer skeleton"
},
/turf/open/floor/carpet,
/area/awaymission/cabin/caves/mountain)
@@ -3859,14 +3858,13 @@
/turf/open/misc/asteroid/snow/snow_cabin,
/area/awaymission/cabin/caves)
"yj" = (
-/mob/living/simple_animal/hostile/skeleton/ice{
+/mob/living/basic/skeleton/ice{
desc = "A reanimated skeleton covered in thick sheet of natural ice. It is obvious to tell that they look really slow.";
maxHealth = 20;
melee_damage_lower = 5;
melee_damage_upper = 5;
name = "frozen skeleton";
- speed = 7;
- wander = 0
+ speed = 7
},
/turf/open/misc/ice/smooth,
/area/awaymission/cabin/caves)
@@ -3899,7 +3897,7 @@
/obj/item/shovel{
desc = "A large tool for digging and moving snow.";
force = 10;
- name = "eskimo shovel"
+ name = "settler's shovel"
},
/obj/effect/decal/remains/human{
color = "#72e4fa"
@@ -4036,14 +4034,13 @@
/turf/open/misc/asteroid/snow/snow_cabin,
/area/awaymission/cabin/caves)
"BR" = (
-/mob/living/simple_animal/hostile/skeleton/ice{
+/mob/living/basic/skeleton/ice{
desc = "A reanimated skeleton covered in thick sheet of natural ice. It is obvious to tell that they look really slow.";
maxHealth = 20;
melee_damage_lower = 5;
melee_damage_upper = 5;
name = "frozen skeleton";
- speed = 7;
- wander = 0
+ speed = 7
},
/turf/open/misc/asteroid/snow/snow_cabin,
/area/awaymission/cabin/caves)
diff --git a/_maps/RandomZLevels/TheBeach.dmm b/_maps/RandomZLevels/TheBeach.dmm
index 29098c05096d..a34169c53478 100644
--- a/_maps/RandomZLevels/TheBeach.dmm
+++ b/_maps/RandomZLevels/TheBeach.dmm
@@ -930,7 +930,7 @@
/turf/open/water/beach/biodome,
/area/awaymission/beach)
"da" = (
-/mob/living/simple_animal/parrot,
+/mob/living/basic/parrot,
/turf/open/misc/beach/sand,
/area/awaymission/beach)
"db" = (
diff --git a/_maps/RandomZLevels/caves.dmm b/_maps/RandomZLevels/caves.dmm
index 40b88aa6addf..0263f8789372 100644
--- a/_maps/RandomZLevels/caves.dmm
+++ b/_maps/RandomZLevels/caves.dmm
@@ -283,7 +283,7 @@
},
/area/awaymission/caves/bmp_asteroid/level_three)
"bD" = (
-/mob/living/simple_animal/hostile/skeleton,
+/mob/living/basic/skeleton,
/turf/open/floor/engine/cult{
initial_gas_mix = "n2=23;o2=14;TEMP=2.7"
},
@@ -360,7 +360,7 @@
},
/area/awaymission/caves/bmp_asteroid/level_two)
"bY" = (
-/mob/living/simple_animal/hostile/skeleton,
+/mob/living/basic/skeleton,
/turf/open/floor/plating{
initial_gas_mix = "n2=23;o2=14;TEMP=2.7"
},
@@ -1210,7 +1210,7 @@
"he" = (
/obj/structure/window/spawner/directional/west,
/obj/structure/window/spawner/directional/south,
-/mob/living/simple_animal/hostile/mining_drone,
+/mob/living/basic/mining_drone,
/turf/open/floor/plating,
/area/awaymission/caves/listeningpost)
"hq" = (
@@ -1734,7 +1734,7 @@
/area/awaymission/caves/bmp_asteroid/level_three)
"IH" = (
/obj/structure/window/spawner/directional/west,
-/mob/living/simple_animal/hostile/mining_drone,
+/mob/living/basic/mining_drone,
/turf/open/floor/plating,
/area/awaymission/caves/listeningpost)
"IN" = (
@@ -2006,7 +2006,7 @@
},
/area/awaymission/caves/bmp_asteroid/level_three)
"YZ" = (
-/mob/living/simple_animal/hostile/skeleton,
+/mob/living/basic/skeleton,
/turf/open/misc/asteroid/basalt{
initial_gas_mix = "n2=23;o2=14;TEMP=2.7"
},
diff --git a/_maps/RandomZLevels/moonoutpost19.dmm b/_maps/RandomZLevels/moonoutpost19.dmm
index 28b4725a1600..759bd7198736 100644
--- a/_maps/RandomZLevels/moonoutpost19.dmm
+++ b/_maps/RandomZLevels/moonoutpost19.dmm
@@ -5450,6 +5450,10 @@
heat_capacity = 1e+006
},
/area/awaymission/moonoutpost19/research)
+"pO" = (
+/mob/living/basic/construct/proteon/hostile,
+/turf/open/space,
+/area/space)
"qr" = (
/obj/effect/decal/cleanable/blood/tracks{
desc = "Your instincts say you shouldn't be following these.";
@@ -13954,7 +13958,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -14731,6 +14735,7 @@ aa
aa
aa
aa
+pO
aa
aa
aa
@@ -14741,8 +14746,7 @@ aa
aa
aa
aa
-aa
-aa
+pO
aa
aa
aa
@@ -17041,7 +17045,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -17310,13 +17314,13 @@ aa
aa
aa
aa
+pO
aa
aa
aa
aa
aa
-aa
-aa
+pO
aa
aa
aa
@@ -18312,7 +18316,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -18838,6 +18842,7 @@ aa
aa
aa
aa
+pO
aa
aa
aa
@@ -18851,8 +18856,7 @@ aa
aa
aa
aa
-aa
-aa
+pO
aa
aa
aa
@@ -20107,7 +20111,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -20400,7 +20404,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -21644,7 +21648,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
@@ -21900,7 +21904,7 @@ aa
aa
aa
aa
-aa
+pO
aa
aa
aa
diff --git a/_maps/RandomZLevels/research.dmm b/_maps/RandomZLevels/research.dmm
index bc6878f09530..cd0fab883407 100644
--- a/_maps/RandomZLevels/research.dmm
+++ b/_maps/RandomZLevels/research.dmm
@@ -72,7 +72,7 @@
/turf/closed/wall/r_wall,
/area/awaymission/research/interior/engineering)
"as" = (
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/turf/open/floor/mineral/plastitanium/red,
/area/awaymission/research/interior/engineering)
"at" = (
@@ -113,7 +113,7 @@
/obj/structure/chair{
dir = 8
},
-/mob/living/basic/syndicate,
+/mob/living/basic/trooper/syndicate,
/turf/open/floor/mineral/plastitanium/red,
/area/awaymission/research/interior/engineering)
"az" = (
@@ -207,7 +207,7 @@
/turf/open/floor/plating,
/area/awaymission/research/interior/engineering)
"aT" = (
-/mob/living/basic/syndicate/melee/sword,
+/mob/living/basic/trooper/syndicate/melee/sword,
/turf/open/floor/plating,
/area/awaymission/research/interior/engineering)
"aU" = (
@@ -575,7 +575,7 @@
/area/awaymission/research/interior/maint)
"cz" = (
/obj/structure/cable,
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/turf/open/floor/plating,
/area/awaymission/research/interior/maint)
"cA" = (
@@ -603,7 +603,7 @@
/area/awaymission/research/interior/gateway)
"cL" = (
/obj/structure/cable,
-/mob/living/basic/syndicate/ranged,
+/mob/living/basic/trooper/syndicate/ranged,
/turf/open/floor/plating,
/area/awaymission/research/interior/maint)
"cM" = (
@@ -718,7 +718,7 @@
/area/awaymission/research/interior)
"dn" = (
/obj/item/ammo_casing/c45,
-/mob/living/basic/syndicate,
+/mob/living/basic/trooper/syndicate,
/obj/effect/turf_decal/tile/yellow/half/contrasted{
dir = 8
},
@@ -801,7 +801,7 @@
/turf/open/floor/iron/dark,
/area/awaymission/research/interior/secure)
"dP" = (
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/obj/effect/turf_decal/tile/blue/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior/medbay)
@@ -848,7 +848,7 @@
/turf/open/floor/iron/stairs,
/area/awaymission/research/interior/genetics)
"ec" = (
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/turf/open/floor/plating,
/area/awaymission/research/interior/maint)
"ed" = (
@@ -947,7 +947,7 @@
/turf/open/floor/iron/white,
/area/awaymission/research/interior/cryo)
"eB" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged,
+/mob/living/basic/trooper/nanotrasen/ranged,
/obj/effect/turf_decal/tile/purple/half/contrasted{
dir = 1
},
@@ -996,7 +996,7 @@
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
"eL" = (
-/mob/living/basic/syndicate,
+/mob/living/basic/trooper/syndicate,
/obj/effect/turf_decal/tile/yellow/anticorner/contrasted{
dir = 4
},
@@ -1048,7 +1048,7 @@
/turf/open/floor/iron/dark,
/area/awaymission/research/interior/secure)
"eV" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
+/mob/living/basic/trooper/nanotrasen/ranged/smg,
/turf/open/floor/iron/dark,
/area/awaymission/research/interior/secure)
"eW" = (
@@ -1134,7 +1134,7 @@
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
"fn" = (
-/mob/living/basic/syndicate/melee/sword,
+/mob/living/basic/trooper/syndicate/melee/sword,
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
"fo" = (
@@ -1201,7 +1201,7 @@
/area/awaymission/research/interior/secure)
"fA" = (
/obj/machinery/light/small/directional/west,
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
+/mob/living/basic/trooper/nanotrasen/ranged/smg,
/turf/open/floor/iron/dark,
/area/awaymission/research/interior/secure)
"fD" = (
@@ -1331,7 +1331,7 @@
/area/awaymission/research/interior/secure)
"gj" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
+/mob/living/basic/trooper/nanotrasen/ranged/smg,
/turf/open/floor/iron/dark,
/area/awaymission/research/interior/secure)
"gk" = (
@@ -1511,7 +1511,7 @@
/turf/open/floor/iron/white,
/area/awaymission/research/interior/security)
"hq" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
+/mob/living/basic/trooper/nanotrasen/ranged/smg,
/obj/effect/turf_decal/tile/red/half/contrasted{
dir = 1
},
@@ -1858,7 +1858,7 @@
/turf/open/floor/iron/freezer,
/area/awaymission/research/interior/bathroom)
"iQ" = (
-/mob/living/basic/syndicate,
+/mob/living/basic/trooper/syndicate,
/obj/effect/turf_decal/tile/green/half/contrasted{
dir = 4
},
@@ -3093,7 +3093,7 @@
/turf/open/floor/iron,
/area/awaymission/research/interior/security)
"sn" = (
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/obj/effect/turf_decal/tile/green/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
@@ -3265,7 +3265,7 @@
pixel_x = 32;
pixel_y = 26
},
-/mob/living/basic/syndicate/ranged/smg,
+/mob/living/basic/trooper/syndicate/ranged/smg,
/obj/effect/turf_decal/tile/green/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
@@ -3283,7 +3283,7 @@
/turf/open/floor/iron/white,
/area/awaymission/research/interior)
"yW" = (
-/mob/living/simple_animal/hostile/nanotrasen,
+/mob/living/basic/trooper/nanotrasen,
/obj/effect/turf_decal/tile/purple/fourcorners,
/turf/open/floor/iron,
/area/awaymission/research/interior/cryo)
@@ -3343,7 +3343,7 @@
/turf/open/floor/iron,
/area/awaymission/research/interior/genetics)
"BQ" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged/smg,
+/mob/living/basic/trooper/nanotrasen/ranged/smg,
/obj/effect/turf_decal/tile/red/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior/security)
@@ -3413,7 +3413,7 @@
/turf/open/floor/iron,
/area/awaymission/research/interior/genetics)
"DY" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged,
+/mob/living/basic/trooper/nanotrasen/ranged,
/obj/effect/turf_decal/tile/purple/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior/cryo)
@@ -3823,7 +3823,7 @@
/turf/open/floor/iron,
/area/awaymission/research/interior/genetics)
"Qq" = (
-/mob/living/simple_animal/hostile/nanotrasen/ranged,
+/mob/living/basic/trooper/nanotrasen/ranged,
/obj/effect/turf_decal/tile/red/fourcorners,
/turf/open/floor/iron,
/area/awaymission/research/interior/security)
@@ -3892,7 +3892,7 @@
/turf/open/misc/asteroid,
/area/awaymission/research/exterior)
"Th" = (
-/mob/living/basic/syndicate,
+/mob/living/basic/trooper/syndicate,
/obj/effect/turf_decal/tile/blue/fourcorners,
/turf/open/floor/iron/white,
/area/awaymission/research/interior/medbay)
@@ -3951,7 +3951,7 @@
/turf/open/floor/plating,
/area/awaymission/research/interior/maint)
"Uq" = (
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/turf/open/misc/asteroid/airless,
/area/awaymission/research/exterior)
"Uu" = (
diff --git a/_maps/RandomZLevels/snowdin.dmm b/_maps/RandomZLevels/snowdin.dmm
index 5ee092df524a..962a25801369 100644
--- a/_maps/RandomZLevels/snowdin.dmm
+++ b/_maps/RandomZLevels/snowdin.dmm
@@ -6207,7 +6207,7 @@
/turf/open/floor/engine/cult,
/area/awaymission/snowdin/post/cavern2)
"st" = (
-/mob/living/simple_animal/hostile/skeleton/ice,
+/mob/living/basic/skeleton/ice,
/turf/open/misc/asteroid/snow{
floor_variance = 0;
icon_state = "snow_dug";
@@ -6430,7 +6430,7 @@
/turf/open/misc/asteroid/snow,
/area/awaymission/snowdin/outside)
"tt" = (
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/turf/open/floor/plating/snowed/cavern,
/area/awaymission/snowdin/cave/cavern)
"tu" = (
@@ -6443,7 +6443,7 @@
/turf/open/floor/plating/snowed/cavern,
/area/awaymission/snowdin/cave/cavern)
"tz" = (
-/mob/living/simple_animal/hostile/skeleton/ice,
+/mob/living/basic/skeleton/ice,
/turf/open/floor/plating/snowed/cavern,
/area/awaymission/snowdin/cave/cavern)
"tE" = (
@@ -6573,7 +6573,7 @@
/area/awaymission/snowdin/post/cavern1)
"uK" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/skeleton/plasmaminer,
+/mob/living/basic/skeleton/plasmaminer,
/turf/open/floor/iron,
/area/awaymission/snowdin/post/cavern1)
"uM" = (
@@ -6595,7 +6595,7 @@
/turf/open/floor/plating,
/area/awaymission/snowdin/post/cavern1)
"uS" = (
-/mob/living/simple_animal/hostile/skeleton/plasmaminer,
+/mob/living/basic/skeleton/plasmaminer,
/obj/effect/mapping_helpers/broken_floor,
/turf/open/floor/plating,
/area/awaymission/snowdin/post/cavern1)
@@ -6678,7 +6678,7 @@
},
/area/awaymission/snowdin/outside)
"vi" = (
-/mob/living/simple_animal/hostile/skeleton/templar,
+/mob/living/basic/skeleton/templar,
/turf/open/floor/engine/cult{
initial_gas_mix = "n2=82;plasma=24;TEMP=120";
temperature = 120
@@ -7197,7 +7197,7 @@
/obj/structure/bed{
dir = 4
},
-/mob/living/simple_animal/hostile/skeleton/ice{
+/mob/living/basic/skeleton/ice{
name = "Privateer Jones"
},
/turf/open/misc/asteroid/snow{
@@ -7450,7 +7450,7 @@
/obj/effect/turf_decal/weather/snow/corner{
dir = 4
},
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/misc/asteroid/snow{
floor_variance = 0;
icon_state = "snow_dug";
@@ -7856,7 +7856,7 @@
/turf/open/floor/plating/snowed/smoothed,
/area/awaymission/snowdin/cave)
"AA" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/floor/wood,
/area/awaymission/snowdin/igloo)
"AC" = (
@@ -10490,7 +10490,7 @@
/area/awaymission/snowdin/post/mining_dock)
"Ma" = (
/obj/effect/turf_decal/weather/snow/corner,
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/misc/asteroid/snow{
floor_variance = 0;
icon_state = "snow_dug";
@@ -11145,7 +11145,7 @@
/obj/effect/turf_decal/weather/snow/corner{
dir = 4
},
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/misc/asteroid/snow{
floor_variance = 0;
icon_state = "snow_dug";
@@ -11517,7 +11517,7 @@
/turf/open/misc/asteroid/snow,
/area/awaymission/snowdin/cave)
"Sb" = (
-/mob/living/simple_animal/hostile/skeleton/plasmaminer/jackhammer,
+/mob/living/basic/skeleton/plasmaminer/jackhammer,
/turf/open/misc/asteroid/snow/ice,
/area/awaymission/snowdin/cave/cavern)
"Sd" = (
@@ -11821,7 +11821,7 @@
/turf/open/floor/iron,
/area/awaymission/snowdin/post/gateway)
"TA" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/misc/asteroid/snow,
/area/awaymission/snowdin/outside)
"TD" = (
@@ -11866,7 +11866,7 @@
/turf/open/floor/iron/dark,
/area/awaymission/snowdin/cave)
"TW" = (
-/mob/living/simple_animal/hostile/skeleton/plasmaminer,
+/mob/living/basic/skeleton/plasmaminer,
/turf/open/floor/plating/snowed/cavern,
/area/awaymission/snowdin/cave/cavern)
"TY" = (
@@ -12023,7 +12023,7 @@
/turf/open/misc/asteroid/snow/ice,
/area/awaymission/snowdin/cave/cavern)
"UM" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/misc/asteroid/snow{
floor_variance = 0;
icon_state = "snow_dug";
@@ -12060,7 +12060,7 @@
/turf/open/floor/iron/white,
/area/awaymission/snowdin/post/mining_main/robotics)
"Ve" = (
-/mob/living/simple_animal/hostile/skeleton/plasmaminer,
+/mob/living/basic/skeleton/plasmaminer,
/turf/open/misc/asteroid/snow/ice,
/area/awaymission/snowdin/cave/cavern)
"Vf" = (
@@ -12362,7 +12362,7 @@
/turf/open/floor/plating,
/area/awaymission/snowdin/post/mining_main/mechbay)
"Xb" = (
-/mob/living/simple_animal/hostile/skeleton/eskimo,
+/mob/living/basic/skeleton/settler,
/turf/open/floor/plating,
/area/awaymission/snowdin/post/mining_main)
"Xd" = (
@@ -12756,7 +12756,7 @@
/turf/open/misc/asteroid/snow,
/area/awaymission/snowdin/post/minipost)
"Zv" = (
-/mob/living/simple_animal/hostile/skeleton/ice{
+/mob/living/basic/skeleton/ice{
name = "Captain Bones"
},
/turf/open/misc/asteroid/snow{
diff --git a/_maps/map_files/Basketball/ash_gladiators.dmm b/_maps/map_files/Basketball/ash_gladiators.dmm
new file mode 100644
index 000000000000..75315e028a83
--- /dev/null
+++ b/_maps/map_files/Basketball/ash_gladiators.dmm
@@ -0,0 +1,1657 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"af" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"ao" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"at" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"aw" = (
+/obj/structure/fluff/drake_statue,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"bA" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked,
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"cc" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"cw" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"dg" = (
+/obj/structure/stone_tile/block,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"eD" = (
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"eM" = (
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"fj" = (
+/obj/effect/decal/remains/human,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"fu" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/block,
+/obj/item/reagent_containers/cup/glass/trophy/silver_cup,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"fT" = (
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"gY" = (
+/turf/open/floor/plating,
+/area/centcom/basketball)
+"hn" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/center/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"hZ" = (
+/obj/item/storage/bag/plants/portaseeder,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"ie" = (
+/mob/living/basic/mining/gutlunch/warrior,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"ix" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"iB" = (
+/obj/item/spear,
+/obj/structure/stone_tile/block/cracked,
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"jb" = (
+/obj/structure/stone_tile/surrounding_tile/cracked,
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"jq" = (
+/obj/structure/stone_tile/cracked,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"ku" = (
+/obj/structure/fence{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"kv" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"ld" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"lh" = (
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/block/cracked,
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"lD" = (
+/obj/structure/fence{
+ dir = 8
+ },
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"lQ" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/center,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"mk" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/center,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"mW" = (
+/obj/structure/stone_tile/surrounding_tile/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"nv" = (
+/obj/machinery/hydroponics/soil,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"oR" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"pV" = (
+/obj/structure/stone_tile/block,
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"qF" = (
+/obj/structure/fence/corner{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"ro" = (
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"ru" = (
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"rU" = (
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"sb" = (
+/turf/closed/indestructible/riveted/boss,
+/area/centcom/basketball)
+"sA" = (
+/obj/effect/decal/remains/xeno,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"un" = (
+/obj/effect/decal/cleanable/blood,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"uH" = (
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"vo" = (
+/turf/open/floor/fakepit,
+/area/centcom/basketball)
+"vq" = (
+/obj/structure/fence/corner{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"wN" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/effect/landmark/basketball/team_spawn/referee,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"xe" = (
+/obj/item/flashlight/flare/torch,
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"xG" = (
+/obj/structure/fence/corner{
+ dir = 5
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"xI" = (
+/obj/structure/stone_tile/slab/burnt,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"yk" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/center/cracked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"yK" = (
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"yO" = (
+/obj/effect/landmark/basketball/team_spawn/home,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"yY" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/table/wood,
+/obj/item/spear,
+/obj/item/scythe,
+/turf/open/indestructible/boss,
+/area/centcom/basketball)
+"zk" = (
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"zU" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/center/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"zZ" = (
+/obj/structure/stone_tile/block/cracked,
+/obj/structure/stone_tile/block/cracked{
+ dir = 1
+ },
+/obj/item/reagent_containers/cup/glass/trophy/bronze_cup,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Aj" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/table/wood,
+/obj/item/spear,
+/obj/item/storage/belt/utility,
+/turf/open/indestructible/boss,
+/area/centcom/basketball)
+"Aw" = (
+/obj/effect/mob_spawn/corpse/human/miner,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"AA" = (
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"BD" = (
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"BW" = (
+/obj/structure/stone_tile/block/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Ct" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Cv" = (
+/obj/structure/hoop/minigame{
+ dir = 4
+ },
+/obj/effect/landmark/basketball/team_spawn/home_hoop,
+/turf/open/floor/fakepit,
+/area/centcom/basketball)
+"Cw" = (
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"CI" = (
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Dc" = (
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Di" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"DN" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"DP" = (
+/obj/effect/landmark/basketball/team_spawn/away_hoop,
+/obj/structure/hoop/minigame{
+ dir = 8
+ },
+/turf/open/floor/fakepit,
+/area/centcom/basketball)
+"DV" = (
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Eb" = (
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"EX" = (
+/obj/structure/fence/corner,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Fh" = (
+/obj/item/flashlight/lantern,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Fi" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Fy" = (
+/mob/living/basic/mining/gutlunch/milk,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"FS" = (
+/obj/structure/stone_tile/block/burnt,
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Gh" = (
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Hu" = (
+/turf/closed/indestructible/riveted,
+/area/centcom/basketball)
+"Hx" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Ih" = (
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"IM" = (
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Jb" = (
+/obj/structure/stone_tile/block{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"JX" = (
+/obj/structure/stone_tile/block{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Kf" = (
+/obj/structure/stone_tile/slab/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Kh" = (
+/obj/structure/stone_tile/block{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Lu" = (
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile,
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Lx" = (
+/obj/effect/landmark/basketball/team_spawn/home,
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"LV" = (
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/closet/crate/internals,
+/obj/item/pickaxe,
+/obj/item/pickaxe,
+/obj/item/pickaxe,
+/obj/item/pickaxe,
+/obj/item/pickaxe,
+/obj/item/pickaxe,
+/obj/effect/mapping_helpers/no_lava,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"MA" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/item/flashlight/flare/torch,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"MN" = (
+/obj/structure/stone_tile/surrounding,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"MR" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Nc" = (
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/block/burnt,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Nt" = (
+/turf/closed/indestructible/rock,
+/area/centcom/basketball)
+"Nw" = (
+/obj/item/cultivator/rake,
+/obj/structure/stone_tile/surrounding/cracked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"NX" = (
+/obj/structure/stone_tile/block,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"OB" = (
+/obj/effect/landmark/basketball/team_spawn/home,
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"OF" = (
+/obj/structure/stone_tile/block{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/table/wood,
+/obj/item/spear,
+/turf/open/indestructible/boss,
+/area/centcom/basketball)
+"OG" = (
+/obj/effect/mob_spawn/corpse/human/damaged,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"OS" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Ql" = (
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"QF" = (
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/center/cracked,
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Rc" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Rl" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Ry" = (
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile,
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 4
+ },
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Se" = (
+/obj/structure/stone_tile/block{
+ dir = 4
+ },
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Tc" = (
+/obj/structure/stone_tile/block/cracked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Ue" = (
+/obj/structure/fluff/drake_statue/falling,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Uv" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"UL" = (
+/obj/structure/bonfire/dense,
+/obj/structure/stone_tile/center,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"UT" = (
+/obj/effect/landmark/basketball/team_spawn/away,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Vc" = (
+/obj/structure/stone_tile/block{
+ dir = 8
+ },
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Vf" = (
+/obj/structure/fence,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Vm" = (
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Vr" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"VI" = (
+/obj/effect/landmark/basketball/team_spawn/away,
+/obj/structure/stone_tile,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"VM" = (
+/obj/structure/stone_tile/block{
+ dir = 4
+ },
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 4
+ },
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"VY" = (
+/obj/structure/stone_tile,
+/obj/structure/stone_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/table/wood,
+/obj/item/spear,
+/obj/item/clothing/head/helmet/roman/legionnaire,
+/turf/open/indestructible/boss,
+/area/centcom/basketball)
+"WE" = (
+/obj/structure/ore_box,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"WH" = (
+/obj/structure/stone_tile/surrounding/cracked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Xq" = (
+/obj/structure/stone_tile/surrounding_tile{
+ dir = 8
+ },
+/obj/structure/stone_tile/center,
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/surrounding_tile/cracked{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"XQ" = (
+/obj/structure/stone_tile/slab/cracked{
+ dir = 4
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"XT" = (
+/obj/structure/stone_tile/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 8
+ },
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/obj/structure/stone_tile/cracked,
+/turf/open/lava/smooth/basketball,
+/area/centcom/basketball)
+"Yl" = (
+/obj/structure/water_source/puddle{
+ pixel_x = -3;
+ pixel_y = 1
+ },
+/obj/item/reagent_containers/cup/bucket/wooden,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Yv" = (
+/obj/effect/landmark/basketball/team_spawn/away,
+/obj/structure/stone_tile/cracked{
+ dir = 1
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"YI" = (
+/obj/structure/stone_tile/block/cracked{
+ dir = 4
+ },
+/obj/structure/stone_tile/block/cracked{
+ dir = 8
+ },
+/obj/item/reagent_containers/cup/glass/trophy/gold_cup{
+ pixel_x = 0
+ },
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"YX" = (
+/obj/structure/lavaland/ash_walker_fake,
+/obj/item/toy/basketball,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+"Zc" = (
+/obj/structure/necropolis_arch,
+/obj/structure/stone_tile/slab,
+/obj/structure/necropolis_gate/locked,
+/turf/open/floor/fakebasalt,
+/area/centcom/basketball)
+
+(1,1,1) = {"
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+"}
+(2,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(3,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Cw
+Ql
+Ql
+Nt
+Nt
+WE
+LV
+Nt
+Ql
+Ql
+Nt
+WE
+Nt
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(4,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Cw
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Ql
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(5,1,1) = {"
+Hu
+gY
+sb
+sb
+sb
+sb
+Cw
+Ql
+vq
+Vf
+Vf
+Vf
+Vf
+Vf
+Vf
+Vf
+qF
+Ql
+Ql
+sb
+sb
+sb
+sb
+gY
+Hu
+"}
+(6,1,1) = {"
+Hu
+gY
+sb
+eD
+JX
+Eb
+Cw
+Ql
+ku
+sA
+Ql
+vo
+Cv
+vo
+Ql
+sA
+ku
+Ql
+Ql
+ix
+Aj
+yY
+sb
+gY
+Hu
+"}
+(7,1,1) = {"
+Hu
+gY
+sb
+dg
+aw
+Dc
+Cw
+Ql
+ku
+Ql
+yO
+vo
+vo
+vo
+yO
+Ql
+ku
+Fh
+Ql
+XQ
+WH
+OF
+sb
+gY
+Hu
+"}
+(8,1,1) = {"
+Hu
+sb
+sb
+Fi
+DN
+mk
+Cw
+Ql
+ku
+Ct
+zk
+OB
+yO
+Lx
+zk
+Ct
+ku
+Ql
+Aw
+Vr
+MA
+VY
+sb
+sb
+Hu
+"}
+(9,1,1) = {"
+Hu
+sb
+Cw
+Cw
+Cw
+Cw
+Cw
+Uv
+lD
+kv
+kv
+lh
+kv
+ao
+kv
+Lu
+lD
+ao
+ao
+ao
+ao
+Nt
+Nt
+sb
+Hu
+"}
+(10,1,1) = {"
+Hu
+sb
+Cw
+MN
+ro
+AA
+rU
+MN
+ku
+Ql
+OS
+MR
+xI
+VM
+yK
+Ql
+ku
+MN
+Vc
+iB
+Jb
+MN
+Nt
+sb
+Hu
+"}
+(11,1,1) = {"
+Hu
+sb
+Ry
+at
+dg
+zZ
+Dc
+at
+ku
+Ql
+NX
+jb
+cw
+QF
+fT
+Ql
+ku
+XT
+eM
+Kh
+cc
+FS
+Nt
+Nt
+Hu
+"}
+(12,1,1) = {"
+Hu
+Zc
+Se
+Uv
+BW
+YI
+Dc
+Uv
+ku
+Ql
+xI
+Tc
+YX
+Gh
+Kf
+Ql
+ku
+wN
+Tc
+UL
+af
+NX
+Nt
+Nt
+Hu
+"}
+(13,1,1) = {"
+Hu
+sb
+yk
+bA
+dg
+fu
+Dc
+Rl
+ku
+Ql
+pV
+mW
+Ct
+BD
+oR
+un
+ku
+IM
+Di
+Ct
+Xq
+Nc
+Nt
+Nt
+Hu
+"}
+(14,1,1) = {"
+Hu
+sb
+jq
+MN
+Ih
+Vm
+DV
+MN
+ku
+Ql
+Hx
+ld
+Kf
+ao
+uH
+Ql
+ku
+MN
+Lu
+Vc
+lh
+MN
+Nt
+sb
+Hu
+"}
+(15,1,1) = {"
+Hu
+sb
+Cw
+Cw
+Cw
+ru
+Cw
+Uv
+lD
+kv
+lh
+kv
+Lu
+NX
+kv
+ao
+lD
+VM
+VM
+VM
+VM
+Nt
+Nt
+sb
+Hu
+"}
+(16,1,1) = {"
+Hu
+sb
+sb
+CI
+JX
+hn
+Cw
+Ql
+ku
+Kh
+Rc
+VI
+UT
+Yv
+Rc
+Kh
+ku
+Ql
+xe
+nv
+nv
+Nt
+sb
+sb
+Hu
+"}
+(17,1,1) = {"
+Hu
+gY
+sb
+dg
+Ue
+Dc
+Cw
+Ql
+ku
+Ql
+UT
+vo
+vo
+vo
+UT
+Ql
+ku
+Ql
+Yl
+hZ
+Nw
+Nt
+sb
+gY
+Hu
+"}
+(18,1,1) = {"
+Hu
+gY
+sb
+zU
+DN
+lQ
+Cw
+Ql
+ku
+fj
+Ql
+vo
+DP
+vo
+Ql
+fj
+ku
+Ql
+Gh
+nv
+nv
+Nt
+sb
+gY
+Hu
+"}
+(19,1,1) = {"
+Hu
+gY
+sb
+sb
+sb
+sb
+Cw
+Ql
+xG
+Vf
+Vf
+Vf
+Vf
+Vf
+Vf
+Vf
+EX
+Ql
+Ql
+sb
+sb
+sb
+sb
+gY
+Hu
+"}
+(20,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Cw
+Ql
+Ql
+Ql
+Ql
+Fy
+Ql
+OG
+Ql
+Ql
+Ql
+Ql
+Nt
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(21,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Cw
+Ql
+OG
+Nt
+Nt
+Ql
+Ql
+Ql
+ie
+Ql
+Nt
+Ql
+Nt
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(22,1,1) = {"
+Hu
+gY
+gY
+gY
+gY
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+Nt
+gY
+gY
+gY
+gY
+Hu
+"}
+(23,1,1) = {"
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+Hu
+"}
diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index 6d418b15bb42..ed4307e9e9b4 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -14713,10 +14713,11 @@
/turf/open/floor/iron/dark,
/area/station/service/theater)
"dxl" = (
-/obj/effect/turf_decal/tile/neutral/fourcorners,
/obj/machinery/status_display/evac/directional/west,
/obj/structure/filingcabinet/chestdrawer,
-/mob/living/simple_animal/parrot/poly,
+/obj/effect/turf_decal/tile/neutral/fourcorners,
+/obj/machinery/newscaster/directional/north,
+/mob/living/basic/parrot/poly,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/ce)
"dxo" = (
@@ -38713,7 +38714,8 @@
/obj/structure/disposalpipe/segment{
dir = 10
},
-/mob/living/simple_animal/sloth/citrus,
+/mob/living/basic/sloth/citrus,
+/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron,
/area/station/command/heads_quarters/qm)
"jrA" = (
@@ -82277,9 +82279,7 @@
/obj/structure/disposalpipe/segment,
/obj/effect/decal/cleanable/dirt,
/obj/machinery/duct,
-/mob/living/simple_animal/hostile/retaliate/goat{
- name = "Pete"
- },
+/mob/living/basic/goat/pete,
/turf/open/floor/iron/freezer,
/area/station/service/kitchen/coldroom)
"uaz" = (
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index 2270243a2afc..3c425eb7696b 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -4256,7 +4256,7 @@
"bup" = (
/obj/structure/filingcabinet/chestdrawer,
/obj/effect/turf_decal/tile/neutral/fourcorners,
-/mob/living/simple_animal/parrot/poly,
+/mob/living/basic/parrot/poly,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/ce)
"buv" = (
@@ -17404,7 +17404,7 @@
/area/station/maintenance/starboard/aft)
"fya" = (
/obj/structure/cable,
-/mob/living/simple_animal/sloth/paperwork,
+/mob/living/basic/sloth/paperwork,
/turf/open/floor/iron,
/area/station/cargo/storage)
"fyc" = (
@@ -32284,7 +32284,7 @@
/turf/open/floor/plating/snowed/smoothed/icemoon,
/area/icemoon/underground/explored)
"ksu" = (
-/mob/living/simple_animal/hostile/asteroid/gutlunch,
+/mob/living/basic/mining/gutlunch/warrior,
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/underground/explored)
"ksC" = (
@@ -43925,10 +43925,10 @@
/turf/open/floor/iron/white,
/area/station/science/xenobiology)
"ocF" = (
-/mob/living/simple_animal/hostile/retaliate/goat{
- atmos_requirements = list("min_oxy"=1,"max_oxy"=0,"min_plas"=0,"max_plas"=1,"min_co2"=0,"max_co2"=5,"min_n2"=0,"max_n2"=0);
+/mob/living/basic/goat/pete{
desc = "Not known for their pleasant disposition. This one seems a bit more hardy to the cold.";
- minbodytemp = 150;
+ habitable_atmos = list("min_oxy"=1,"max_oxy"=0,"min_plas"=0,"max_plas"=1,"min_co2"=0,"max_co2"=5,"min_n2"=0,"max_n2"=0);
+ minimum_survivable_temperature = 150;
name = "Snowy Pete"
},
/turf/open/misc/asteroid/snow/coldroom,
diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm
index 1d51253a6d62..9aa4fe401324 100644
--- a/_maps/map_files/KiloStation/KiloStation.dmm
+++ b/_maps/map_files/KiloStation/KiloStation.dmm
@@ -6746,7 +6746,7 @@
/obj/effect/turf_decal/tile/yellow{
dir = 1
},
-/mob/living/simple_animal/sloth/citrus,
+/mob/living/basic/sloth/citrus,
/turf/open/floor/iron,
/area/station/cargo/sorting)
"coy" = (
@@ -7040,7 +7040,7 @@
/turf/open/misc/asteroid/airless,
/area/space/nearstation)
"cuj" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/legion,
/turf/open/misc/asteroid/airless,
/area/space/nearstation)
"cup" = (
@@ -13219,9 +13219,6 @@
dir = 8
},
/obj/effect/decal/cleanable/blood/old,
-/mob/living/simple_animal/hostile/retaliate/goat{
- name = "Pete"
- },
/turf/open/floor/iron/freezer,
/area/station/service/kitchen/coldroom)
"eBx" = (
@@ -27198,6 +27195,17 @@
/obj/effect/mapping_helpers/broken_floor,
/turf/open/floor/plating,
/area/station/maintenance/port/fore)
+"iQO" = (
+/obj/structure/chair/office/light{
+ dir = 4
+ },
+/mob/living/basic/parrot/poly,
+/obj/effect/decal/cleanable/dirt,
+/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
+ dir = 4
+ },
+/turf/open/space/basic,
+/area/space)
"iRq" = (
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4,
/obj/structure/cable,
@@ -31818,10 +31826,6 @@
/obj/effect/decal/cleanable/blood/gibs/old,
/obj/effect/decal/cleanable/blood/old,
/obj/effect/decal/cleanable/dirt,
-/mob/living/simple_animal/hostile/jungle/mook{
- environment_smash = 0;
- name = "deformed creature"
- },
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark,
/area/station/maintenance/port/fore)
@@ -38280,7 +38284,7 @@
/obj/structure/spider/stickyweb,
/obj/effect/mapping_helpers/broken_floor,
/obj/effect/turf_decal/siding/wood,
-/mob/living/basic/syndicate/russian/ranged/lootless,
+/mob/living/basic/trooper/russian/ranged/lootless,
/turf/open/floor/plating,
/area/station/maintenance/starboard)
"mCO" = (
@@ -41625,10 +41629,6 @@
/obj/effect/decal/remains/human,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
-/mob/living/simple_animal/hostile/jungle/mook{
- environment_smash = 0;
- name = "deformed creature"
- },
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark,
/area/station/maintenance/port/fore)
@@ -51478,7 +51478,7 @@
/obj/structure/cable,
/obj/effect/mapping_helpers/broken_floor,
/obj/structure/chair/stool/directional/west,
-/mob/living/basic/syndicate/russian/ranged/lootless,
+/mob/living/basic/trooper/russian/ranged/lootless,
/turf/open/floor/plating,
/area/station/maintenance/starboard)
"rbO" = (
@@ -53023,6 +53023,12 @@
/obj/effect/turf_decal/delivery,
/turf/open/floor/iron/freezer,
/area/station/service/kitchen/coldroom)
+"rAn" = (
+/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
+/obj/machinery/holopad,
+/mob/living/basic/sloth/citrus,
+/turf/open/space/basic,
+/area/station/solars/port/fore)
"rAr" = (
/obj/effect/turf_decal/delivery,
/obj/machinery/light/directional/west,
@@ -63425,11 +63431,11 @@
pixel_x = -4;
pixel_y = 4
},
-/obj/item/pen,
-/obj/effect/turf_decal/stripes/corner{
+/obj/structure/disposalpipe/segment{
dir = 4
},
-/obj/effect/turf_decal/tile/neutral/anticorner/contrasted,
+/mob/living/basic/goat/pete,
+/obj/machinery/duct,
/turf/open/floor/iron/dark,
/area/station/hallway/primary/central/fore)
"uWQ" = (
@@ -68121,7 +68127,7 @@
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
dir = 8
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/legion,
/turf/open/floor/plating,
/area/station/cargo/warehouse)
"wpw" = (
@@ -68791,7 +68797,7 @@
/obj/effect/turf_decal/stripes/line{
dir = 1
},
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/legion,
/turf/open/floor/plating,
/area/station/cargo/warehouse)
"wAz" = (
@@ -70742,7 +70748,7 @@
pixel_x = 12
},
/obj/item/radio/intercom/directional/east,
-/mob/living/simple_animal/parrot/poly,
+/mob/living/basic/parrot/poly,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/ce)
"xjr" = (
@@ -73513,7 +73519,7 @@
/turf/open/floor/iron/dark,
/area/station/ai_monitored/turret_protected/ai_upload)
"ycp" = (
-/mob/living/simple_animal/hostile/asteroid/hivelord,
+/mob/living/basic/mining/legion,
/turf/open/misc/asteroid/lowpressure,
/area/space/nearstation)
"ycr" = (
@@ -87601,7 +87607,7 @@ aof
aUz
cmU
grV
-grV
+rAn
grV
grV
grV
@@ -94515,7 +94521,7 @@ aaa
aaa
aaa
aaa
-aaa
+iQO
aaa
aaa
aaa
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index 7182dcf69a6f..fd3027598486 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -25229,7 +25229,7 @@
dir = 4
},
/obj/structure/disposalpipe/segment,
-/mob/living/simple_animal/sloth/citrus,
+/mob/living/basic/sloth/citrus,
/turf/open/floor/iron,
/area/station/cargo/storage)
"jhD" = (
@@ -47254,7 +47254,7 @@
"qXw" = (
/obj/structure/filingcabinet/chestdrawer,
/obj/effect/turf_decal/tile/neutral/fourcorners,
-/mob/living/simple_animal/parrot/poly,
+/mob/living/basic/parrot/poly,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/ce)
"qXB" = (
@@ -58876,10 +58876,7 @@
"uYg" = (
/obj/structure/cable,
/obj/structure/sink/kitchen/directional/west,
-/obj/machinery/power/apc/auto_name/directional/north,
-/mob/living/simple_animal/hostile/retaliate/goat{
- name = "Pete"
- },
+/mob/living/basic/goat/pete,
/turf/open/floor/iron/kitchen_coldroom/freezerfloor,
/area/station/service/kitchen/coldroom)
"uYi" = (
diff --git a/_maps/map_files/NorthStar/north_star.dmm b/_maps/map_files/NorthStar/north_star.dmm
index 0da7b68198dd..f46e67ab5d9e 100644
--- a/_maps/map_files/NorthStar/north_star.dmm
+++ b/_maps/map_files/NorthStar/north_star.dmm
@@ -7993,10 +7993,10 @@
/obj/structure/table/reinforced/plastitaniumglass,
/obj/structure/window/reinforced/spawner/directional/west,
/obj/structure/cable,
-/mob/living/simple_animal/parrot/poly,
/obj/machinery/computer/security/telescreen/engine{
name = "Engineering and atmospherics monitor"
},
+/mob/living/basic/parrot/poly,
/turf/open/floor/catwalk_floor/iron_dark,
/area/station/command/heads_quarters/ce)
"bVT" = (
@@ -30014,7 +30014,7 @@
/turf/open/floor/plating,
/area/station/maintenance/floor3/starboard/fore)
"idf" = (
-/mob/living/simple_animal/sloth/paperwork,
+/mob/living/basic/sloth/paperwork,
/obj/effect/turf_decal/tile/yellow,
/obj/effect/turf_decal/tile/brown{
dir = 4
@@ -57672,9 +57672,7 @@
"pmG" = (
/obj/machinery/power/apc/auto_name/directional/west,
/obj/structure/cable,
-/mob/living/simple_animal/hostile/retaliate/goat{
- name = "Pete"
- },
+/mob/living/basic/goat/pete,
/turf/open/floor/iron/kitchen_coldroom/freezerfloor,
/area/station/service/kitchen/coldroom)
"pmO" = (
diff --git a/_maps/map_files/Oshan/oshan.dmm b/_maps/map_files/Oshan/oshan.dmm
index a8787a400312..3a5b43de35f0 100644
--- a/_maps/map_files/Oshan/oshan.dmm
+++ b/_maps/map_files/Oshan/oshan.dmm
@@ -18608,7 +18608,7 @@
/turf/open/floor/wood,
/area/station/commons/dorms)
"jCm" = (
-/mob/living/simple_animal/hostile/retaliate/clown{
+/mob/living/basic/clown{
limb_destroyer = 1;
health = 200;
desc = "This clown was hired to entertain visitors.";
@@ -24103,7 +24103,7 @@
dir = 1;
icon_state = "pink2_1"
},
-/mob/living/simple_animal/sloth/citrus,
+/mob/living/basic/sloth/citrus,
/turf/open/floor/wood,
/area/station/command/heads_quarters/qm)
"mlu" = (
diff --git a/_maps/map_files/tramstation/maintenance_modules/medsciupper_attachment_b_1.dmm b/_maps/map_files/tramstation/maintenance_modules/medsciupper_attachment_b_1.dmm
index 8bb9fa1c7463..ec76b1e334cc 100644
--- a/_maps/map_files/tramstation/maintenance_modules/medsciupper_attachment_b_1.dmm
+++ b/_maps/map_files/tramstation/maintenance_modules/medsciupper_attachment_b_1.dmm
@@ -59,7 +59,7 @@
},
/obj/effect/landmark/generic_maintenance_landmark,
/obj/structure/closet/body_bag,
-/mob/living/simple_animal/hostile/skeleton{
+/mob/living/basic/skeleton{
name = "Jim";
desc = "Left to rot in maintenance, a poor soul whose green light was never noticed by the doctor..."
},
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index 5c73bd9058f8..31d6d443310c 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -1496,13 +1496,17 @@
/turf/open/misc/asteroid/snow/coldroom,
/area/station/service/kitchen/coldroom)
"aep" = (
+/obj/effect/turf_decal/weather/snow/corner{
+ dir = 10
+ },
/obj/effect/turf_decal/weather/snow/corner{
dir = 10
},
/obj/effect/turf_decal/weather/snow,
-/mob/living/simple_animal/hostile/retaliate/goat{
- name = "Pete"
+/obj/machinery/atmospherics/components/unary/vent_scrubber/on/layer2{
+ dir = 4
},
+/mob/living/basic/goat/pete,
/turf/open/floor/iron/kitchen_coldroom,
/area/station/service/kitchen/coldroom)
"aeq" = (
@@ -3291,6 +3295,10 @@
/area/station/tcommsat/computer)
"apZ" = (
/obj/structure/filingcabinet/chestdrawer,
+/obj/effect/turf_decal/trimline/yellow/filled/line{
+ dir = 9
+ },
+/obj/structure/filingcabinet/chestdrawer,
/obj/effect/turf_decal/trimline/yellow/filled/line{
dir = 9
},
@@ -3311,8 +3319,7 @@
pixel_y = -8;
req_access = list("engineering")
},
-/obj/structure/sign/clock/directional/north,
-/mob/living/simple_animal/parrot/poly,
+/mob/living/basic/parrot/poly,
/turf/open/floor/iron,
/area/station/command/heads_quarters/ce)
"aqf" = (
@@ -48527,7 +48534,7 @@
/turf/open/floor/iron/dark/textured,
/area/station/engineering/main)
"oDF" = (
-/mob/living/simple_animal/hostile/retaliate/clown{
+/mob/living/basic/clown{
name = "The Forbidden One";
limb_destroyer = 1;
health = 1000;
@@ -59229,7 +59236,7 @@
},
/obj/effect/landmark/generic_maintenance_landmark,
/obj/structure/closet/body_bag,
-/mob/living/simple_animal/hostile/skeleton{
+/mob/living/basic/skeleton{
name = "Jim";
desc = "Left to rot in maintenance, a poor soul whose green light was never noticed by the doctor..."
},
@@ -59717,7 +59724,7 @@
/turf/open/floor/iron/white,
/area/station/science/lower)
"sha" = (
-/mob/living/simple_animal/sloth/paperwork,
+/mob/living/basic/sloth/paperwork,
/turf/open/floor/glass,
/area/station/cargo/storage)
"shg" = (
diff --git a/_maps/shuttles/emergency_bar.dmm b/_maps/shuttles/emergency_bar.dmm
index 5f248c384609..01023ed050e3 100644
--- a/_maps/shuttles/emergency_bar.dmm
+++ b/_maps/shuttles/emergency_bar.dmm
@@ -252,7 +252,7 @@
/turf/open/floor/iron/grimy,
/area/shuttle/escape)
"aX" = (
-/mob/living/simple_animal/drone/snowflake/bardrone,
+/mob/living/basic/drone/snowflake/bardrone,
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/shuttle/escape)
diff --git a/_maps/shuttles/emergency_cruise.dmm b/_maps/shuttles/emergency_cruise.dmm
index a6d02332e064..6d1ca3b9e5a4 100644
--- a/_maps/shuttles/emergency_cruise.dmm
+++ b/_maps/shuttles/emergency_cruise.dmm
@@ -133,7 +133,7 @@
/obj/structure/disposalpipe/segment{
dir = 8
},
-/mob/living/simple_animal/hostile/retaliate/clown,
+/mob/living/basic/clown,
/turf/open/floor/eighties,
/area/shuttle/escape/luxury)
"cZ" = (
@@ -659,7 +659,7 @@
"ld" = (
/obj/effect/decal/cleanable/confetti,
/obj/structure/window/reinforced/spawner/directional/west,
-/mob/living/simple_animal/hostile/retaliate/clown{
+/mob/living/basic/clown{
health = 300;
name = "Honk-E-Clown";
desc = "This clown looks pretty tough, better stay on his good side...";
@@ -1823,7 +1823,7 @@
/turf/open/floor/iron/dark/textured,
/area/shuttle/escape/brig)
"Ht" = (
-/mob/living/simple_animal/hostile/retaliate/clown,
+/mob/living/basic/clown,
/turf/open/floor/eighties,
/area/shuttle/escape/luxury)
"HA" = (
@@ -2618,6 +2618,13 @@
},
/turf/open/floor/iron/dark/textured,
/area/shuttle/escape/brig)
+"Wa" = (
+/obj/structure/chair/comfy/teal{
+ dir = 4
+ },
+/mob/living/basic/drone/snowflake/bardrone,
+/turf/open/floor/wood/tile,
+/area/shuttle/escape)
"Wc" = (
/obj/effect/turf_decal/trimline/red/corner{
dir = 4
@@ -3541,7 +3548,7 @@ xf
xf
mw
mw
-mw
+Wa
xf
Ov
uV
diff --git a/_maps/shuttles/emergency_hugcage.dmm b/_maps/shuttles/emergency_hugcage.dmm
index b1162a76a137..cd8d4e7ff00d 100644
--- a/_maps/shuttles/emergency_hugcage.dmm
+++ b/_maps/shuttles/emergency_hugcage.dmm
@@ -30,7 +30,7 @@
/turf/open/floor/mineral/titanium/blue,
/area/shuttle/escape)
"dz" = (
-/mob/living/simple_animal/parrot/natural{
+/mob/living/basic/parrot{
melee_damage_upper = 5
},
/turf/open/floor/mineral/titanium/blue,
diff --git a/_maps/shuttles/emergency_narnar.dmm b/_maps/shuttles/emergency_narnar.dmm
index 835a6054a874..d6c48664e08e 100644
--- a/_maps/shuttles/emergency_narnar.dmm
+++ b/_maps/shuttles/emergency_narnar.dmm
@@ -111,7 +111,7 @@
/turf/open/floor/cult,
/area/shuttle/escape)
"w" = (
-/mob/living/simple_animal/hostile/construct/artificer,
+/mob/living/basic/construct/artificer,
/turf/open/floor/cult,
/area/shuttle/escape)
"x" = (
@@ -139,7 +139,7 @@
/turf/open/floor/cult,
/area/shuttle/escape)
"C" = (
-/mob/living/simple_animal/hostile/construct/juggernaut,
+/mob/living/basic/construct/juggernaut,
/turf/open/floor/cult,
/area/shuttle/escape)
"D" = (
@@ -166,7 +166,7 @@
/turf/open/floor/cult,
/area/shuttle/escape)
"H" = (
-/mob/living/simple_animal/hostile/construct/wraith,
+/mob/living/basic/construct/wraith,
/turf/open/floor/cult,
/area/shuttle/escape)
"I" = (
diff --git a/_maps/shuttles/emergency_rollerdome.dmm b/_maps/shuttles/emergency_rollerdome.dmm
index f6cec0091aa9..0518c951e1ba 100644
--- a/_maps/shuttles/emergency_rollerdome.dmm
+++ b/_maps/shuttles/emergency_rollerdome.dmm
@@ -238,6 +238,10 @@
/obj/structure/window/reinforced/spawner/directional/north,
/turf/open/floor/plating/airless,
/area/shuttle/escape)
+"KO" = (
+/mob/living/basic/drone/snowflake/bardrone,
+/turf/open/floor/wood,
+/area/shuttle/escape)
"Ow" = (
/obj/structure/table,
/obj/item/storage/box/drinkingglasses,
@@ -270,10 +274,6 @@
},
/turf/open/floor/wood,
/area/shuttle/escape)
-"Qv" = (
-/mob/living/simple_animal/drone/snowflake/bardrone,
-/turf/open/floor/wood,
-/area/shuttle/escape)
"QO" = (
/obj/machinery/door/airlock/gold/glass,
/turf/open/floor/wood,
@@ -469,7 +469,7 @@ Cg
Cg
ce
ns
-Qv
+KO
JR
KJ
od
@@ -517,7 +517,7 @@ Cg
Cg
ce
uN
-Ky
+KO
Ow
KJ
od
diff --git a/_maps/shuttles/ruin_caravan_victim.dmm b/_maps/shuttles/ruin_caravan_victim.dmm
index 614ae713c7b1..6b95b857f153 100644
--- a/_maps/shuttles/ruin_caravan_victim.dmm
+++ b/_maps/shuttles/ruin_caravan_victim.dmm
@@ -13,7 +13,7 @@
/turf/closed/wall/mineral/titanium/nodiagonal,
/area/shuttle/ruin/caravan/freighter1)
"cu" = (
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark/airless,
/area/shuttle/ruin/caravan/freighter1)
@@ -458,7 +458,7 @@
/obj/machinery/atmospherics/components/unary/vent_pump/on{
dir = 8
},
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/obj/effect/turf_decal/tile/yellow{
dir = 4
},
@@ -468,7 +468,7 @@
/area/shuttle/ruin/caravan/freighter1)
"Fu" = (
/obj/effect/decal/cleanable/blood,
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/obj/effect/turf_decal/tile/blue{
dir = 4
},
@@ -606,7 +606,7 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
-/mob/living/basic/syndicate/ranged/smg/space,
+/mob/living/basic/trooper/syndicate/ranged/smg/space,
/turf/open/floor/iron/airless,
/area/shuttle/ruin/caravan/freighter1)
"Ld" = (
diff --git a/_maps/shuttles/ruin_pirate_cutter.dmm b/_maps/shuttles/ruin_pirate_cutter.dmm
index 615cb8b255c3..db75b1eff6c2 100644
--- a/_maps/shuttles/ruin_pirate_cutter.dmm
+++ b/_maps/shuttles/ruin_pirate_cutter.dmm
@@ -713,7 +713,7 @@
"Ry" = (
/obj/structure/rack,
/obj/item/storage/bag/money/vault,
-/mob/living/simple_animal/parrot{
+/mob/living/basic/parrot{
faction = list("pirate");
name = "Pegwing"
},
diff --git a/_maps/shuttles/ruin_syndicate_dropship.dmm b/_maps/shuttles/ruin_syndicate_dropship.dmm
index e3987b39ed54..b1f3f00897b7 100644
--- a/_maps/shuttles/ruin_syndicate_dropship.dmm
+++ b/_maps/shuttles/ruin_syndicate_dropship.dmm
@@ -215,7 +215,7 @@
/obj/structure/chair/comfy/shuttle{
dir = 4
},
-/mob/living/basic/syndicate/ranged/smg/pilot{
+/mob/living/basic/trooper/syndicate/ranged/smg/pilot{
environment_smash = 0
},
/turf/open/floor/iron/dark,
diff --git a/_maps/shuttles/ruin_syndicate_fighter_shiv.dmm b/_maps/shuttles/ruin_syndicate_fighter_shiv.dmm
index be8b4c2bad9f..7c2903773389 100644
--- a/_maps/shuttles/ruin_syndicate_fighter_shiv.dmm
+++ b/_maps/shuttles/ruin_syndicate_fighter_shiv.dmm
@@ -20,7 +20,7 @@
req_access = list("syndicate")
},
/obj/structure/cable,
-/mob/living/basic/syndicate/ranged/smg/pilot{
+/mob/living/basic/trooper/syndicate/ranged/smg/pilot{
environment_smash = 0
},
/turf/open/floor/mineral/plastitanium/red,
diff --git a/_maps/shuttles/whiteship_meta.dmm b/_maps/shuttles/whiteship_meta.dmm
index d13fccbf7016..0e3c921935f6 100644
--- a/_maps/shuttles/whiteship_meta.dmm
+++ b/_maps/shuttles/whiteship_meta.dmm
@@ -286,7 +286,12 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
-/mob/living/basic/syndicate/ranged{
+/obj/effect/decal/cleanable/dirt,
+/obj/effect/decal/cleanable/dirt,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
+ dir = 4
+ },
+/mob/living/basic/trooper/syndicate/ranged{
environment_smash = 0;
name = "Syndicate Salvage Worker"
},
@@ -1649,7 +1654,12 @@
dir = 8
},
/obj/effect/decal/cleanable/blood/gibs/old,
-/mob/living/basic/syndicate/melee{
+/obj/effect/decal/cleanable/dirt,
+/obj/machinery/atmospherics/components/unary/vent_pump/on{
+ dir = 8
+ },
+/obj/effect/decal/cleanable/blood/gibs/old,
+/mob/living/basic/trooper/syndicate/melee{
environment_smash = 0
},
/obj/effect/turf_decal/tile/neutral/fourcorners,
@@ -1709,8 +1719,9 @@
/area/shuttle/abandoned/crew)
"qc" = (
/obj/effect/decal/cleanable/dirt/dust,
+/obj/effect/decal/cleanable/dirt,
/obj/machinery/atmospherics/components/unary/vent_pump/on,
-/mob/living/basic/syndicate/melee{
+/mob/living/basic/trooper/syndicate/melee{
environment_smash = 0
},
/obj/effect/turf_decal/tile/neutral/fourcorners,
@@ -1730,7 +1741,11 @@
dir = 4
},
/obj/effect/decal/cleanable/dirt/dust,
-/mob/living/basic/syndicate/melee{
+/obj/effect/turf_decal/box/white/corners{
+ dir = 4
+ },
+/obj/effect/decal/cleanable/dirt,
+/mob/living/basic/trooper/syndicate/melee{
environment_smash = 0
},
/turf/open/floor/iron/dark,
diff --git a/_maps/shuttles/whiteship_obelisk.dmm b/_maps/shuttles/whiteship_obelisk.dmm
index 303b0912c9f2..05b17d19b71e 100644
--- a/_maps/shuttles/whiteship_obelisk.dmm
+++ b/_maps/shuttles/whiteship_obelisk.dmm
@@ -112,7 +112,7 @@
/area/shuttle/abandoned/engine)
"gw" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune4";
paint_colour = "#DC143C"
@@ -124,7 +124,7 @@
/turf/open/floor/catwalk_floor/iron_smooth,
/area/shuttle/abandoned/engine)
"hg" = (
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/blood/old,
/obj/structure/cable,
/turf/open/floor/mineral/titanium/white,
@@ -156,7 +156,7 @@
/area/shuttle/abandoned/engine)
"ji" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
@@ -398,7 +398,7 @@
/area/shuttle/abandoned/crew)
"yo" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune4";
paint_colour = "#DC143C"
@@ -570,7 +570,7 @@
/area/shuttle/abandoned)
"HU" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/juggernaut/hostile,
+/mob/living/basic/construct/juggernaut/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune4";
paint_colour = "#DC143C"
@@ -590,7 +590,7 @@
/area/shuttle/abandoned)
"IH" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/blood/gibs/core,
/obj/machinery/atmospherics/components/unary/vent_pump/on{
dir = 1
@@ -789,7 +789,7 @@
/area/shuttle/abandoned/medbay)
"SE" = (
/obj/structure/cable,
-/mob/living/simple_animal/hostile/construct/proteon/hostile,
+/mob/living/basic/construct/proteon/hostile,
/obj/effect/decal/cleanable/crayon{
icon_state = "rune4";
paint_colour = "#DC143C"
diff --git a/_maps/virtual_domains/pirates.dmm b/_maps/virtual_domains/pirates.dmm
index 5356f53d1932..805c7bc061cc 100644
--- a/_maps/virtual_domains/pirates.dmm
+++ b/_maps/virtual_domains/pirates.dmm
@@ -752,7 +752,7 @@
/area/virtual_domain/powered)
"Pz" = (
/obj/structure/table/wood,
-/mob/living/simple_animal/parrot{
+/mob/living/basic/parrot{
name = "pepper"
},
/turf/open/floor/carpet/blue,
diff --git a/_maps/virtual_domains/syndicate_assault.dmm b/_maps/virtual_domains/syndicate_assault.dmm
index ea3cebaedd1e..a41275e20187 100644
--- a/_maps/virtual_domains/syndicate_assault.dmm
+++ b/_maps/virtual_domains/syndicate_assault.dmm
@@ -41,7 +41,7 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
-/mob/living/basic/syndicate/ranged/shotgun/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"bh" = (
@@ -241,7 +241,7 @@
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"ip" = (
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/turf/open/floor/plastic,
/area/ruin/space/has_grav/powered/virtual_domain)
"iB" = (
@@ -279,7 +279,7 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden{
dir = 4
},
-/mob/living/basic/syndicate/ranged/smg/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/smg/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"ja" = (
@@ -298,7 +298,7 @@
/area/ruin/space/has_grav/powered/virtual_domain)
"jA" = (
/obj/structure/cable,
-/mob/living/basic/syndicate/melee/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"jJ" = (
@@ -417,7 +417,7 @@
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"oZ" = (
-/mob/living/basic/syndicate/melee/sword/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/melee/sword/space/stormtrooper,
/turf/open/floor/carpet/royalblack,
/area/ruin/space/has_grav/powered/virtual_domain)
"pl" = (
@@ -648,7 +648,7 @@
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/space/has_grav/powered/virtual_domain)
"yD" = (
-/mob/living/basic/syndicate/ranged/smg/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/smg/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"yJ" = (
@@ -699,7 +699,7 @@
/obj/structure/chair/comfy/shuttle{
dir = 4
},
-/mob/living/basic/syndicate/ranged/smg/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/smg/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"Bm" = (
@@ -745,7 +745,7 @@
/obj/structure/chair/comfy/shuttle{
dir = 4
},
-/mob/living/basic/syndicate/ranged/smg/pilot,
+/mob/living/basic/trooper/syndicate/ranged/smg/pilot,
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/space/has_grav/powered/virtual_domain)
"CR" = (
@@ -795,7 +795,7 @@
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/space/has_grav/powered/virtual_domain)
"EX" = (
-/mob/living/basic/syndicate/ranged/shotgun/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/shotgun/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"Fp" = (
@@ -1084,7 +1084,7 @@
/turf/closed/mineral/random,
/area/space)
"QX" = (
-/mob/living/basic/syndicate/ranged/space/stormtrooper,
+/mob/living/basic/trooper/syndicate/ranged/space/stormtrooper,
/turf/open/floor/mineral/plastitanium,
/area/ruin/space/has_grav/powered/virtual_domain)
"Ra" = (
diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm
index 1a6829946a14..eee874a041c9 100644
--- a/code/__DEFINES/ai/ai.dm
+++ b/code/__DEFINES/ai/ai.dm
@@ -1,4 +1,5 @@
#define GET_AI_BEHAVIOR(behavior_type) SSai_behaviors.ai_behaviors[behavior_type]
+#define GET_TARGETING_STRATEGY(targeting_type) SSai_behaviors.targeting_strategies[targeting_type]
#define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type)
#define AI_STATUS_ON 1
@@ -29,7 +30,11 @@
/// Don't move if being pulled
#define STOP_MOVING_WHEN_PULLED (1<<0)
/// Continue processing even if dead
-#define CAN_ACT_WHILE_DEAD (1<<1)
+#define CAN_ACT_WHILE_DEAD (1<<1)
+/// Stop processing while in a progress bar
+#define PAUSE_DURING_DO_AFTER (1<<2)
+/// Continue processing while in stasis
+#define CAN_ACT_IN_STASIS (1<<3)
//Base Subtree defines
diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm
index fe8e9541bf33..1e7c0e892e8c 100644
--- a/code/__DEFINES/ai/ai_blackboard.dm
+++ b/code/__DEFINES/ai/ai_blackboard.dm
@@ -8,6 +8,27 @@
#define BB_FOOD_TARGET "bb_food_target"
///Path we should use next time we use the JPS movement datum
#define BB_PATH_TO_USE "BB_path_to_use"
+///How close a mob must be for us to select it as a target, if that is less than how far we can maintain it as a target
+#define BB_AGGRO_RANGE "BB_aggro_range"
+///are we hungry? determined by the udder compnent
+#define BB_CHECK_HUNGRY "BB_check_hungry"
+///are we ready to breed?
+#define BB_BREED_READY "BB_breed_ready"
+///maximum kids we can have
+#define BB_MAX_CHILDREN "BB_max_children"
+
+/// Store a single or list of emotes at this key
+#define BB_EMOTE_KEY "BB_emotes"
+/// Chance to perform an emote per second
+#define BB_EMOTE_CHANCE "BB_EMOTE_CHANCE"
+
+/// Something the mob will say when calling reinforcements
+#define BB_REINFORCEMENTS_SAY "BB_reinforcements_say"
+/// Something the mob will remote when calling reinforcements
+#define BB_REINFORCEMENTS_EMOTE "BB_reinforcements_emote"
+
+///Turf we want a mob to move to
+#define BB_TRAVEL_DESTINATION "BB_travel_destination"
///song instrument blackboard, set by instrument subtrees
#define BB_SONG_INSTRUMENT "BB_SONG_INSTRUMENT"
@@ -25,25 +46,39 @@
///Basic Mob Keys
-///Targetting subtrees
+///Targeting subtrees
#define BB_BASIC_MOB_CURRENT_TARGET "BB_basic_current_target"
#define BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION "BB_basic_current_target_hiding_location"
-#define BB_TARGETTING_DATUM "targetting_datum"
+#define BB_TARGETING_STRATEGY "targeting_strategy"
///some behaviors that check current_target also set this on deep crit mobs
#define BB_BASIC_MOB_EXECUTION_TARGET "BB_basic_execution_target"
///Blackboard key for a whitelist typecache of "things we can target while trying to move"
-#define BB_OBSTACLE_TARGETTING_WHITELIST "BB_targetting_whitelist"
-
-/// Blackboard key storing how long your targetting datum has held a particular target
+#define BB_OBSTACLE_TARGETING_WHITELIST "BB_targeting_whitelist"
+/// Key for the minimum status at which we want to target mobs (does not need to be specified if CONSCIOUS)
+#define BB_TARGET_MINIMUM_STAT "BB_target_minimum_stat"
+/// Flag for whether to target only wounded mobs
+#define BB_TARGET_WOUNDED_ONLY "BB_target_wounded_only"
+/// What typepath the holding object targeting strategy should look for
+#define BB_TARGET_HELD_ITEM "BB_target_held_item"
+
+/// Blackboard key storing how long your targeting strategy has held a particular target
#define BB_BASIC_MOB_HAS_TARGET_TIME "BB_basic_mob_has_target_time"
-///Targetting keys for something to run away from, if you need to store this separately from current target
+///Targeting keys for something to run away from, if you need to store this separately from current target
#define BB_BASIC_MOB_FLEE_TARGET "BB_basic_flee_target"
#define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location"
-#define BB_FLEE_TARGETTING_DATUM "flee_targetting_datum"
+#define BB_FLEE_TARGETING_STRATEGY "flee_targeting_strategy"
+#define BB_BASIC_MOB_FLEE_DISTANCE "BB_basic_flee_distance"
+#define DEFAULT_BASIC_FLEE_DISTANCE 9
+
+/// Generic key for a non-specific targeted action
+#define BB_TARGETED_ACTION "BB_targeted_action"
-/// Generic key for a non-specific targetted action
-#define BB_TARGETTED_ACTION "BB_targetted_action"
+/// Generic key for a non-specific action
+#define BB_GENERIC_ACTION "BB_generic_action"
+
+/// Generic key for a shapeshifting action
+#define BB_SHAPESHIFT_ACTION "BB_shapeshift_action"
///How long have we spent with no target?
#define BB_TARGETLESS_TIME "BB_targetless_time"
@@ -68,8 +103,13 @@
///List of mobs who have damaged us
#define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist"
-/// Flag to set on or off if you want your mob to prioritise running away
-#define BB_BASIC_MOB_FLEEING "BB_basic_fleeing"
+/// Chance to randomly acquire a new target
+#define BB_RANDOM_AGGRO_CHANCE "BB_random_aggro_chance"
+/// Chance to randomly drop all of our targets
+#define BB_RANDOM_DEAGGRO_CHANCE "BB_random_deaggro_chance"
+
+/// Flag to set on if you want your mob to STOP running away
+#define BB_BASIC_MOB_STOP_FLEEING "BB_basic_stop_fleeing"
///list of foods this mob likes
#define BB_BASIC_FOODS "BB_basic_foods"
@@ -85,3 +125,20 @@
#define MOD_AI_RANGE 200
#define BB_GROUP_DATUM "BB_group_datum"
+///should we skip the faction check for the targeting strategy?
+#define BB_ALWAYS_IGNORE_FACTION "BB_always_ignore_factions"
+///are we in some kind of temporary state of ignoring factions when targeting? can result in volatile results if multiple behaviours touch this
+#define BB_TEMPORARILY_IGNORE_FACTION "BB_temporarily_ignore_factions"
+
+///currently only used by clowns, a list of what can the mob speak randomly
+#define BB_BASIC_MOB_SPEAK_LINES "BB_speech_lines"
+#define BB_EMOTE_SAY "emote_say"
+#define BB_EMOTE_HEAR "emote_hear"
+#define BB_EMOTE_SEE "emote_see"
+#define BB_EMOTE_SOUND "emote_sound"
+#define BB_SPEAK_CHANCE "emote_chance"
+
+/// A target that has called this mob for reinforcements
+#define BB_BASIC_MOB_REINFORCEMENT_TARGET "BB_basic_mob_reinforcement_target"
+/// The next time at which this mob can call for reinforcements
+#define BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN "BB_basic_mob_reinforcements_cooldown"
diff --git a/code/__DEFINES/ai/carp.dm b/code/__DEFINES/ai/carp.dm
index 8286f3262077..459b98ffb020 100644
--- a/code/__DEFINES/ai/carp.dm
+++ b/code/__DEFINES/ai/carp.dm
@@ -7,9 +7,9 @@
/// Current target turf in your migration
#define BB_CARP_MIGRATION_TARGET "BB_carp_migration_target"
-/// Targetting keys for magicarp spells
+/// Targeting keys for magicarp spells
#define BB_MAGICARP_SPELL_TARGET "BB_magicarp_spell_target"
-#define BB_MAGICARP_SPELL_SPECIAL_TARGETTING "BB_magicarp_spell_special_targetting"
+#define BB_MAGICARP_SPELL_SPECIAL_TARGETING "BB_magicarp_spell_special_targeting"
#define MAGICARP_SPELL_CORPSES "magicarp_spell_corpses"
#define MAGICARP_SPELL_WALLS "magicarp_spell_walls"
#define MAGICARP_SPELL_OBJECTS "magicarp_spell_objects"
diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm
index 4915ff9cbc86..2b2a55ebe0f8 100644
--- a/code/__DEFINES/ai/monsters.dm
+++ b/code/__DEFINES/ai/monsters.dm
@@ -119,3 +119,106 @@
#define BB_ORE_TARGET "BB_ore_target"
/// which ore types we will not eat
#define BB_ORE_IGNORE_TYPES "BB_ore_ignore_types"
+
+// minebot keys
+/// key that stores our toggle light ability
+#define BB_MINEBOT_LIGHT_ABILITY "minebot_light_ability"
+/// key that stores our dump ore ability
+#define BB_MINEBOT_DUMP_ABILITY "minebot_dump_ability"
+/// key that stores our target turf
+#define BB_TARGET_MINERAL_TURF "target_mineral_turf"
+/// key that stores list of the turfs we ignore
+#define BB_BLACKLIST_MINERAL_TURFS "blacklist_mineral_turfs"
+/// key that stores the previous blocked wall
+#define BB_PREVIOUS_UNREACHABLE_WALL "previous_unreachable_wall"
+/// key that stores our mining mode
+#define BB_AUTOMATED_MINING "automated_mining"
+/// key that stores the nearest dead human
+#define BB_NEARBY_DEAD_MINER "nearby_dead_miner"
+
+//seedling keys
+/// the water can we will pick up
+#define BB_WATERCAN_TARGET "watercan_target"
+/// the hydrotray we will heal
+#define BB_HYDROPLANT_TARGET "hydroplant_target"
+/// minimum weed levels for us to cure
+#define BB_WEEDLEVEL_THRESHOLD "weedlevel_threshold"
+/// minimum water levels for us to refill
+#define BB_WATERLEVEL_THRESHOLD "waterlevel_threshold"
+/// key holds our solarbeam ability
+#define BB_SOLARBEAM_ABILITY "solarbeam_ability"
+/// key holds our rapid seeds ability
+#define BB_RAPIDSEEDS_ABILITY "rapidseeds_ability"
+/// key holds the tray we will beam
+#define BB_BEAMABLE_HYDROPLANT_TARGET "beamable_hydroplant_target"
+
+//ice demons
+///the list of items we are afraid of
+#define BB_LIST_SCARY_ITEMS "list_scary_items"
+///our teleportation ability
+#define BB_DEMON_TELEPORT_ABILITY "demon_teleport_ability"
+///the destination of our teleport ability
+#define BB_TELEPORT_DESTINATION "teleport_destination"
+///the ability to clone ourself
+#define BB_DEMON_CLONE_ABILITY "demon_clone_ability"
+///our slippery ice ability
+#define BB_DEMON_SLIP_ABILITY "demon_slip_ability"
+///the turf we are escaping to
+#define BB_ESCAPE_DESTINATION "escape_destination"
+
+/// Corpse we have consumed
+#define BB_LEGION_CORPSE "legion_corpse"
+/// Things our target recently said
+#define BB_LEGION_RECENT_LINES "legion_recent_lines"
+/// The creator of our legion skull
+#define BB_LEGION_BROOD_CREATOR "legion_brood_creator"
+
+//mook keys
+/// our home landmark
+#define BB_HOME_VILLAGE "home_village"
+/// maximum distance we can be from home during a storm
+#define BB_MAXIMUM_DISTANCE_TO_VILLAGE "maximum_distance_to_village"
+/// stand where we deposit our ores
+#define BB_MATERIAL_STAND_TARGET "material_stand_target"
+/// our jump ability
+#define BB_MOOK_JUMP_ABILITY "mook_jump_ability"
+/// our leap ability
+#define BB_MOOK_LEAP_ABILITY "mook_leap_ability"
+/// the chief we must obey
+#define BB_MOOK_TRIBAL_CHIEF "mook_tribal_chief"
+/// the injured mook we must heal
+#define BB_INJURED_MOOK "injured_mook"
+/// the player we will follow and play music for
+#define BB_MOOK_MUSIC_AUDIENCE "music_audience"
+/// the bonfire we will light up
+#define BB_MOOK_BONFIRE_TARGET "bonfire_target"
+
+//gutlunch keys
+///the trough we will eat from
+#define BB_TROUGH_TARGET "trough_target"
+//leaper keys
+///key holds our volley ability
+#define BB_LEAPER_VOLLEY "leaper_volley"
+///key holds our flop ability
+#define BB_LEAPER_FLOP "leaper_flop"
+///key holds our bubble ability
+#define BB_LEAPER_BUBBLE "leaper_bubble"
+///key holds our summon ability
+#define BB_LEAPER_SUMMON "leaper_summon"
+///key holds the world timer for swimming
+#define BB_KEY_SWIM_TIME "key_swim_time"
+///key holds the water or land target turf
+#define BB_SWIM_ALTERNATE_TURF "swim_alternate_turf"
+///key holds our state of swimming
+#define BB_CURRENTLY_SWIMMING "currently_swimming"
+///key holds how long we will be swimming for
+#define BB_KEY_SWIMMER_COOLDOWN "key_swimmer_cooldown"
+//Wizard AI keys
+/// Key where we store our main targeted spell
+#define BB_WIZARD_TARGETED_SPELL "BB_wizard_targeted_spell"
+/// Key where we store our secondary, untargeted spell
+#define BB_WIZARD_SECONDARY_SPELL "BB_wizard_secondary_spell"
+/// Key where we store our blink spell
+#define BB_WIZARD_BLINK_SPELL "BB_wizard_blink_spell"
+/// Key for the next time we can cast a spell
+#define BB_WIZARD_SPELL_COOLDOWN "BB_wizard_spell_cooldown"
diff --git a/code/__DEFINES/ai/pet_commands.dm b/code/__DEFINES/ai/pet_commands.dm
index 5894aedff14d..1e692b9f805a 100644
--- a/code/__DEFINES/ai/pet_commands.dm
+++ b/code/__DEFINES/ai/pet_commands.dm
@@ -4,6 +4,6 @@
/// Blackboard field for what we actually want the pet to target
#define BB_CURRENT_PET_TARGET "BB_current_pet_target"
/// Blackboard field for how we target things, as usually we want to be more permissive than normal
-#define BB_PET_TARGETTING_DATUM "BB_pet_targetting"
+#define BB_PET_TARGETING_STRATEGY "BB_pet_targeting"
/// Typecache of weakrefs to mobs this mob is friends with, will follow their instructions and won't attack them
#define BB_FRIENDS_LIST "BB_friends_list"
diff --git a/code/__DEFINES/ai/pets.dm b/code/__DEFINES/ai/pets.dm
index b3ad67ecc068..e41c9ac0c3ff 100644
--- a/code/__DEFINES/ai/pets.dm
+++ b/code/__DEFINES/ai/pets.dm
@@ -27,3 +27,27 @@
#define BB_FIND_MOM_TYPES "BB_find_mom_types"
///list of types of mobs we must ignore
#define BB_IGNORE_MOM_TYPES "BB_ignore_mom_types"
+
+/// The current string that this parrot will repeat back to someone
+#define BB_PARROT_REPEAT_STRING "BB_parrot_repeat_string"
+/// The odds that this parrot will repeat back a string
+#define BB_PARROT_REPEAT_PROBABILITY "BB_parrot_repeat_probability"
+/// The odds that this parrot will choose another string to repeat
+#define BB_PARROT_PHRASE_CHANGE_PROBABILITY "BB_parrot_phrase_change_probability"
+/// A copy of the string buffer that we end the shift with. DO NOT ACCESS THIS DIRECTLY - YOU SHOULD USE THE COMPONENT IN MOST CASES
+#define BB_EXPORTABLE_STRING_BUFFER_LIST "BB_parrot_repeat_string_buffer"
+/// The types of perches we desire to use
+#define BB_PARROT_PERCH_TYPES "BB_parrot_perch_types"
+/// key that holds our perch target
+#define BB_PERCH_TARGET "perch_target"
+/// key that holds our theft item target
+#define BB_HOARD_ITEM_TARGET "hoard_item_target"
+/// key that holds the mob we will steal from
+#define BB_THEFT_VICTIM "theft_victim"
+/// key that holds the turf we will be hauling stolen items to
+#define BB_HOARD_LOCATION "hoard_location"
+/// key that holds the minimum range we must be from the hoard spot
+#define BB_HOARD_LOCATION_RANGE "hoard_location_range"
+/// key that holds items we arent interested in hoarding
+#define BB_IGNORE_ITEMS "ignore_items"
+
diff --git a/code/__DEFINES/ai/trader.dm b/code/__DEFINES/ai/trader.dm
new file mode 100644
index 000000000000..853dd8736b64
--- /dev/null
+++ b/code/__DEFINES/ai/trader.dm
@@ -0,0 +1,6 @@
+///The ability to setup our "shop"
+#define BB_SETUP_SHOP "BB_setup_shop"
+///Reference to the plastic chair that is considered as our shop
+#define BB_SHOP_SPOT "BB_shop_spot"
+///Reference to our first customer to harass with deals
+#define BB_FIRST_CUSTOMER "BB_first_customer"
diff --git a/code/__DEFINES/ai/ventcrawling.dm b/code/__DEFINES/ai/ventcrawling.dm
index f981ef3bba5d..a60b7fd59401 100644
--- a/code/__DEFINES/ai/ventcrawling.dm
+++ b/code/__DEFINES/ai/ventcrawling.dm
@@ -3,7 +3,7 @@
/// Key that holds a vent that we want to exit out of (when we're already in a pipenet)
#define BB_EXIT_VENT_TARGET "BB_exit_vent_target"
/// Do we plan on going inside a vent? Boolean.
-#define BB_CURRENTLY_TARGETTING_VENT "BB_currently_targetting_vent"
+#define BB_CURRENTLY_TARGETING_VENT "BB_currently_targeting_vent"
/// How long should we wait before we try and enter a vent again?
#define BB_VENTCRAWL_COOLDOWN "BB_ventcrawl_cooldown"
/// The least amount of time (in seconds) we take to go through the vents.
diff --git a/code/__DEFINES/basic_mobs.dm b/code/__DEFINES/basic_mobs.dm
index 5a4aebaee23e..6c8a3022e8fa 100644
--- a/code/__DEFINES/basic_mobs.dm
+++ b/code/__DEFINES/basic_mobs.dm
@@ -19,3 +19,10 @@
/// Above this speed we stop gliding because it looks silly
#define END_GLIDE_SPEED 10
+
+///mook attack status flags
+#define MOOK_ATTACK_NEUTRAL 0
+#define MOOK_ATTACK_WARMUP 1
+#define MOOK_ATTACK_ACTIVE 2
+#define MOOK_ATTACK_STRIKE 3
+
diff --git a/code/__DEFINES/blob_defines.dm b/code/__DEFINES/blob_defines.dm
index 6a6e9d5c75c8..81edf9a02085 100644
--- a/code/__DEFINES/blob_defines.dm
+++ b/code/__DEFINES/blob_defines.dm
@@ -32,7 +32,6 @@
#define BLOB_CORE_EXPAND_RANGE 3 // Radius of automatic expansion
#define BLOB_CORE_STRONG_REINFORCE_RANGE 1 // The radius of tiles surrounding the core that get upgraded
#define BLOB_CORE_REFLECTOR_REINFORCE_RANGE 0
-#define BLOB_CORE_MAX_SPORES 0 // Spores that the core can produce for free
#define BLOB_NODE_MAX_HP 200
#define BLOB_NODE_HP_REGEN 3
@@ -42,7 +41,6 @@
#define BLOB_NODE_EXPAND_RANGE 2 // Radius of automatic expansion
#define BLOB_NODE_STRONG_REINFORCE_RANGE 0 // The radius of tiles surrounding the node that get upgraded
#define BLOB_NODE_REFLECTOR_REINFORCE_RANGE 0
-#define BLOB_NODE_MAX_SPORES 0 // Spores that nodes can maintain
#define BLOB_FACTORY_MAX_HP 200
#define BLOB_FACTORY_HP_REGEN 1
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 11828510f341..deb84d0f0b0f 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -45,6 +45,7 @@
#define COLOR_SOFT_RED "#FA8282"
#define COLOR_CULT_RED "#960000"
#define COLOR_BUBBLEGUM_RED "#950A0A"
+#define COLOR_CARP_RIFT_RED "#ff330030"
#define COLOR_YELLOW "#FFFF00"
#define COLOR_VIVID_YELLOW "#FBFF23"
diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm
index 03b8514e0cf1..f28dc8ec7dc6 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -270,6 +270,8 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list(
#define BODY_ZONE_L_LEG "l_leg"
#define BODY_ZONE_R_LEG "r_leg"
+GLOBAL_LIST_INIT(arm_zones, list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+
#define BODY_ZONE_PRECISE_EYES "eyes"
#define BODY_ZONE_PRECISE_MOUTH "mouth"
#define BODY_ZONE_PRECISE_GROIN "groin"
@@ -335,6 +337,8 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list(
#define ARMOR_WEAKENED_MULTIPLIER 2
/// Armor can't block more than this as a percentage
#define ARMOR_MAX_BLOCK 90
+/// Calculates the new armour value after armour penetration. Can return negative values, and those must be caught.
+#define PENETRATE_ARMOUR(armour, penetration) (penetration == 100 ? 0 : 100 * (armour - penetration) / (100 - penetration))
/// Return values used in item/melee/baton/baton_attack.
/// Does a normal item attack.
diff --git a/code/__DEFINES/crushing.dm b/code/__DEFINES/crushing.dm
new file mode 100644
index 000000000000..1261b98e730e
--- /dev/null
+++ b/code/__DEFINES/crushing.dm
@@ -0,0 +1,15 @@
+// from _vending.dm
+
+/// Set if the tipped object successfully crushed a subtype of /mob/living.
+#define SUCCESSFULLY_CRUSHED_MOB (1<<0)
+/// Set if the tipped object successfully crushed a subtype of /atom.
+#define SUCCESSFULLY_CRUSHED_ATOM (1<<1)
+/// Set if the tipped object successfully actually fell over, which can fail if it couldn't enter the turf it tried to fall into.
+#define SUCCESSFULLY_FELL_OVER (1<<2)
+
+#define CRUSH_CRIT_SHATTER_LEGS "crush_crit_shatter_legs"
+#define CRUSH_CRIT_PARAPALEGIC "crush_crit_parapalegic"
+#define CRUSH_CRIT_SQUISH_LIMB "crush_crit_pin"
+#define CRUSH_CRIT_HEADGIB "crush_crit_headgib"
+#define VENDOR_CRUSH_CRIT_PIN "vendor_crush_crit_pin"
+#define VENDOR_CRUSH_CRIT_GLASSCANDY "vendor_crush_crit_glasscandy"
diff --git a/code/__DEFINES/dcs/flags.dm b/code/__DEFINES/dcs/flags.dm
index 3f3db0212b6d..fb93e3a337a0 100644
--- a/code/__DEFINES/dcs/flags.dm
+++ b/code/__DEFINES/dcs/flags.dm
@@ -1,4 +1,4 @@
-/// Return this from `/datum/component/Initialize` or `datum/component/OnTransfer` to have the component be deleted if it's applied to an incorrect type.
+/// Return this from `/datum/component/Initialize` or `/datum/component/OnTransfer` or `/datum/component/on_source_add` to have the component be deleted if it's applied to an incorrect type.
/// `parent` must not be modified if this is to be returned.
/// This will be noted in the runtime logs
#define COMPONENT_INCOMPATIBLE 1
@@ -33,8 +33,8 @@
/**
* Component uses source tracking to manage adding and removal logic.
* Add a source/spawn to/the component by using AddComponentFrom(source, component_type, args...)
- * Only the first args will be respected, and you should instead handle most of your logic in the on_source_added proc.
* Removing the last source will automatically remove the component from the parent.
+ * Arguments will be passed to on_source_add(source, args...); ensure that Initialize and on_source_add have the same signature.
*/
#define COMPONENT_DUPE_SOURCES 3
/// old component is given the initialization args of the new
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm
index 5191d7e8c68d..55a19a4347ad 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm
@@ -52,7 +52,15 @@
#define COMSIG_ATOM_ATTACK_ROBOT_SECONDARY "atom_attack_robot_secondary"
///from relay_attackers element: (atom/attacker, attack_flags)
#define COMSIG_ATOM_WAS_ATTACKED "atom_was_attacked"
+///Called before a atom gets something tilted on them. If [COMPONENT_IMMUNE_TO_TILT_AND_CRUSH] is returned in a signal, the atom will be unaffected.
+#define COMSIG_PRE_TILT_AND_CRUSH "atom_pre_tilt_and_crush"
+ #define COMPONENT_IMMUNE_TO_TILT_AND_CRUSH (1<<0)
+///Called when a atom gets something tilted on them
+#define COMSIG_POST_TILT_AND_CRUSH "atom_post_tilt_and_crush"
+
///The damage type of the weapon projectile is non-lethal stamina
#define ATTACKER_STAMINA_ATTACK (1<<0)
///the attacker is shoving the source
#define ATTACKER_SHOVING (1<<1)
+ /// The attack is a damaging-type attack
+ #define ATTACKER_DAMAGING_ATTACK (1<<2)
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
index dc4d5a25ea0c..5e6f50a1d654 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
@@ -47,13 +47,17 @@
#define COMSIG_MOVABLE_THROW_LANDED "movable_throw_landed"
///from base of atom/movable/on_changed_z_level(): (turf/old_turf, turf/new_turf, same_z_layer)
#define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit"
+///called before hearing a message from atom/movable/Hear():
+#define COMSIG_MOVABLE_PRE_HEAR "movable_pre_hear"
+ ///cancel hearing the message because we're doing something else presumably
+ #define COMSIG_MOVABLE_CANCEL_HEARING (1<<0)
///from base of atom/movable/Hear(): (proc args list(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range))
#define COMSIG_MOVABLE_HEAR "movable_hear"
- //#define HEARING_MESSAGE 1 - (I'm pretty sure this is never really used and can be gutted)
+ #define HEARING_MESSAGE 1
#define HEARING_SPEAKER 2
#define HEARING_LANGUAGE 3
#define HEARING_RAW_MESSAGE 4
- //#define HEARING_RADIO_FREQ 5
+ #define HEARING_RADIO_FREQ 5
#define HEARING_SPANS 6
#define HEARING_MESSAGE_MODE 7
#define HEARING_RANGE 8
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
index a4f19e29979b..d21e6b286ec4 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
@@ -15,6 +15,9 @@
#define COMSIG_ATOM_ON_Z_IMPACT "movable_on_z_impact"
///called on a movable (NOT living) when it starts pulling (atom/movable/pulled, state, force)
#define COMSIG_ATOM_START_PULL "movable_start_pull"
+/// called on /atom when something attempts to pass through it (atom/movable/source, atom/movable/passing, dir)
+#define COMSIG_ATOM_TRIED_PASS "atom_tried_pass"
+ #define COMSIG_COMPONENT_PERMIT_PASSAGE (1 << 0)
///called on /living when someone starts pulling (atom/movable/pulled, state, force)
#define COMSIG_LIVING_START_PULL "living_start_pull"
///called on /living when someone is pulled (mob/living/puller)
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
index 23e91ad86b23..a40436e77365 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_x_act.dm
@@ -2,6 +2,10 @@
// When the signal is called: (signal arguments)
// All signals send the source datum of the signal as the first argument
+///from the [EX_ACT] wrapper macro: (severity, target)
+#define COMSIG_ATOM_PRE_EX_ACT "atom_pre_ex_act"
+ /// if returned, don't let the explosion act on this atom
+ #define COMPONENT_CANCEL_EX_ACT (1<<0)
///from the [EX_ACT] wrapper macro: (severity, target)
#define COMSIG_ATOM_EX_ACT "atom_ex_act"
///from base of atom/emp_act(): (severity)
@@ -9,6 +13,14 @@
///from base of atom/fire_act(): (exposed_temperature, exposed_volume)
#define COMSIG_ATOM_FIRE_ACT "atom_fire_act"
///from base of atom/bullet_act(): (/obj/projectile, def_zone)
+#define COMSIG_ATOM_PRE_BULLET_ACT "pre_atom_bullet_act"
+ /// All this does is prevent default bullet on_hit from being called, [BULLET_ACT_HIT] being return is implied
+ #define COMPONENT_BULLET_ACTED (1<<0)
+ /// Forces bullet act to return [BULLET_ACT_BLOCK], takes priority over above
+ #define COMPONENT_BULLET_BLOCKED (1<<1)
+ /// Forces bullet act to return [BULLET_ACT_FORCE_PIERCE], takes priority over above
+ #define COMPONENT_BULLET_PIERCED (1<<2)
+///from base of atom/bullet_act(): (/obj/projectile, def_zone)
#define COMSIG_ATOM_BULLET_ACT "atom_bullet_act"
///from base of atom/CheckParts(): (list/parts_list, datum/crafting_recipe/R)
#define COMSIG_ATOM_CHECKPARTS "atom_checkparts"
diff --git a/code/__DEFINES/dcs/signals/signals_blob.dm b/code/__DEFINES/dcs/signals/signals_blob.dm
new file mode 100644
index 000000000000..afd4737bdd96
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_blob.dm
@@ -0,0 +1,4 @@
+/// Signal sent when a blob overmind picked a new strain (/mob/camera/blob/overmind, /datum/blobstrain/new_strain)
+#define COMSIG_BLOB_SELECTED_STRAIN "blob_selected_strain"
+/// Signal sent by a blob spore when it creates a zombie (/mob/living/basic/blob_minion/spore/spore, //mob/living/basic/blob_minion/zombie/zombie)
+#define COMSIG_BLOB_ZOMBIFIED "blob_zombified"
diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index 5d78f2b63836..b1cafb083c0a 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -51,6 +51,8 @@
#define LINKED_UP (1<<0)
/// an obj/item is created! (obj/item/created_item)
#define COMSIG_GLOB_NEW_ITEM "!new_item"
+/// called post /obj/item initialize (obj/item/created_item)
+#define COMSIG_GLOB_ATOM_AFTER_POST_INIT "!atom_after_post_init"
/// an obj/machinery is created! (obj/machinery/created_machine)
#define COMSIG_GLOB_NEW_MACHINE "!new_machine"
/// a client (re)connected, after all /client/New() checks have passed : (client/connected_client)
diff --git a/code/__DEFINES/dcs/signals/signals_leash.dm b/code/__DEFINES/dcs/signals/signals_leash.dm
new file mode 100644
index 000000000000..4f83d7909034
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_leash.dm
@@ -0,0 +1,11 @@
+/// Called when a /datum/component/leash must forcibly teleport the parent to the owner.
+/// Fired on the object with the leash component.
+#define COMSIG_LEASH_FORCE_TELEPORT "leash_force_teleport"
+
+/// Called when a /datum/component/leash plans on pathfinding to the target, if out of range.
+/// Fired on the object with the leash component.
+#define COMSIG_LEASH_PATH_STARTED "leash_path_started"
+
+/// Called when a /datum/component/leash finishes its pathfinding to the target.
+/// Fired on the object with the leash component.
+#define COMSIG_LEASH_PATH_COMPLETE "leash_path_complete"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm
index 85630c8e8f04..a04b8e751a0c 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm
@@ -3,3 +3,4 @@
/// Signal sent when a blackboard key is cleared
#define COMSIG_AI_BLACKBOARD_KEY_CLEARED(blackboard_key) "ai_blackboard_key_clear_[blackboard_key]"
+
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm
index 18c6c651435b..8420a4864172 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm
@@ -5,3 +5,10 @@
///from the ranged_attacks component for basic mobs: (mob/living/basic/firer, atom/target, modifiers)
#define COMSIG_BASICMOB_POST_ATTACK_RANGED "basicmob_post_attack_ranged"
+
+/// Sent from /datum/ai_planning_subtree/parrot_as_in_repeat() : ()
+#define COMSIG_NEEDS_NEW_PHRASE "parrot_needs_new_phrase"
+ #define NO_NEW_PHRASE_AVAILABLE (1<<0) //! Cancel to try again later for when we actually get a new phrase
+
+/// Called whenever an animal is pet via the /datum/element/pet_bonus element: (mob/living/petter, modifiers)
+#define COMSIG_ANIMAL_PET "animal_pet"
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 adae4324d59e..42934c3c7fa9 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
@@ -22,11 +22,11 @@
///When a carbon slips. Called on /turf/open/handle_slip()
#define COMSIG_ON_CARBON_SLIP "carbon_slip"
-///When a carbon gets a vending machine tilted on them
-#define COMSIG_ON_VENDOR_CRUSH "carbon_vendor_crush"
// /mob/living/carbon physiology signals
#define COMSIG_CARBON_GAIN_WOUND "carbon_gain_wound" //from /datum/wound/proc/apply_wound() (/mob/living/carbon/C, /datum/wound/W, /obj/item/bodypart/L)
#define COMSIG_CARBON_LOSE_WOUND "carbon_lose_wound" //from /datum/wound/proc/remove_wound() (/mob/living/carbon/C, /datum/wound/W, /obj/item/bodypart/L)
+/// Called after limb AND victim has been unset
+#define COMSIG_CARBON_POST_LOSE_WOUND "carbon_post_lose_wound" //from /datum/wound/proc/remove_wound() (/datum/wound/lost_wound, /obj/item/bodypart/part, ignore_limb, replaced)
///from base of /obj/item/bodypart/proc/can_attach_limb(): (new_limb, special) allows you to fail limb attachment
#define COMSIG_ATTEMPT_CARBON_ATTACH_LIMB "attempt_carbon_attach_limb"
#define COMPONENT_NO_ATTACH (1<<0)
@@ -36,6 +36,14 @@
#define COMSIG_CARBON_POST_ATTACH_LIMB "carbon_post_attach_limb"
#define COMSIG_BODYPART_GAUZED "bodypart_gauzed" // from /obj/item/bodypart/proc/apply_gauze(/obj/item/stack/gauze)
#define COMSIG_BODYPART_GAUZE_DESTROYED "bodypart_degauzed" // from [/obj/item/bodypart/proc/seep_gauze] when it runs out of absorption
+///from /obj/item/bodypart/proc/receive_damage, sent from the limb owner (limb, brute, burn)
+#define COMSIG_CARBON_LIMB_DAMAGED "carbon_limb_damaged"
+ #define COMPONENT_PREVENT_LIMB_DAMAGE (1 << 0)
+/// from /obj/item/stack/medical/gauze/Destroy(): (/obj/item/stack/medical/gauze/removed_gauze)
+#define COMSIG_BODYPART_UNGAUZED "bodypart_ungauzed"
+
+/// Called from bodypart changing owner, which could be on attach or detachment. Either argument can be null. (mob/living/carbon/new_owner, mob/living/carbon/old_owner)
+#define COMSIG_BODYPART_CHANGED_OWNER "bodypart_changed_owner"
/// Called from update_health_hud, whenever a bodypart is being updated on the health doll
#define COMSIG_BODYPART_UPDATING_HEALTH_HUD "bodypart_updating_health_hud"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm
new file mode 100644
index 000000000000..9f4819de7974
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm
@@ -0,0 +1,7 @@
+/// Sent when a guardian is manifested
+#define COMSIG_GUARDIAN_MANIFESTED "guardian_manifested"
+/// Sent when a guardian is recalled
+#define COMSIG_GUARDIAN_RECALLED "guardian_recalled"
+
+/// Sent when an assassin guardian is forced to exit stealth
+#define COMSIG_GUARDIAN_ASSASSIN_REVEALED "guardian_assassin_revealed"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index 63fd640d77f9..2466f770827e 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -40,12 +40,50 @@
#define COMSIG_LIVING_TRY_SYRINGE "living_try_syringe"
///From living/Life(). (deltatime, times_fired)
#define COMSIG_LIVING_LIFE "living_life"
+ /// Block the Life() proc from proceeding... this should really only be done in some really wacky situations.
+ #define COMPONENT_LIVING_CANCEL_LIFE_PROCESSING (1<<0)
///From living/set_resting(): (new_resting, silent, instant)
#define COMSIG_LIVING_RESTING "living_resting"
///from base of element/bane/activate(): (item/weapon, mob/user)
#define COMSIG_LIVING_BANED "living_baned"
+///from base of element/bane/activate(): (item/weapon, mob/user)
+#define COMSIG_OBJECT_PRE_BANING "obj_pre_baning"
+ #define COMPONENT_CANCEL_BANING (1<<0)
+
+///from base of element/bane/activate(): (item/weapon, mob/user)
+#define COMSIG_OBJECT_ON_BANING "obj_on_baning"
+
+// adjust_x_loss messages sent from /mob/living/proc/adjust[x]Loss
+/// Returned from all the following messages if you actually aren't going to apply any change
+#define COMPONENT_IGNORE_CHANGE (1<<0)
+// Each of these messages sends the damagetype even though it is inferred by the signal so you can pass all of them to the same proc if required
+/// Send when bruteloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_BRUTE_DAMAGE "living_adjust_brute_damage"
+/// Send when fireloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_BURN_DAMAGE "living_adjust_burn_damage"
+/// Send when oxyloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_OXY_DAMAGE "living_adjust_oxy_damage"
+/// Send when toxloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_TOX_DAMAGE "living_adjust_tox_damage"
+/// Send when cloneloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_CLONE_DAMAGE "living_adjust_clone_damage"
+/// Send when staminaloss is modified (type, amount, forced)
+#define COMSIG_LIVING_ADJUST_STAMINA_DAMAGE "living_adjust_stamina_damage"
+
+/// List of signals sent when you receive any damage except stamina
+#define COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES list(\
+ COMSIG_LIVING_ADJUST_BRUTE_DAMAGE,\
+ COMSIG_LIVING_ADJUST_BURN_DAMAGE,\
+ COMSIG_LIVING_ADJUST_CLONE_DAMAGE,\
+ COMSIG_LIVING_ADJUST_OXY_DAMAGE,\
+ COMSIG_LIVING_ADJUST_TOX_DAMAGE,\
+)
+/// List of signals sent when you receive any kind of damage at all
+#define COMSIG_LIVING_ADJUST_ALL_DAMAGE_TYPES (COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES + COMSIG_LIVING_ADJUST_STAMINA_DAMAGE)
+
+
/// from base of mob/living/updatehealth()
#define COMSIG_LIVING_HEALTH_UPDATE "living_health_update"
///from base of mob/living/death(): (gibbed)
@@ -97,6 +135,10 @@
#define COMSIG_LIVING_UNARMED_ATTACK "living_unarmed_attack"
///From base of mob/living/MobBump() (mob/living)
#define COMSIG_LIVING_MOB_BUMP "living_mob_bump"
+///From base of mob/living/Bump() (turf/closed)
+#define COMSIG_LIVING_WALL_BUMP "living_wall_bump"
+///From base of turf/closed/Exited() (turf/closed)
+#define COMSIG_LIVING_WALL_EXITED "living_wall_exited"
///From base of mob/living/ZImpactDamage() (mob/living, levels, turf/t)
#define COMSIG_LIVING_Z_IMPACT "living_z_impact"
#define NO_Z_IMPACT_DAMAGE (1<<0)
@@ -168,3 +210,10 @@
/// From /datum/ai/behavior/climb_tree/perform() : (mob/living/basic/living_pawn)
#define COMSIG_LIVING_CLIMB_TREE "living_climb_tree"
+
+/// Sent on a mob from /datum/component/mob_chain when component is attached with it as the "front" : (mob/living/basic/tail)
+#define COMSIG_MOB_GAINED_CHAIN_TAIL "living_gained_chain_tail"
+/// Sent on a mob from /datum/component/mob_chain when component is detached from it as the "front" : (mob/living/basic/tail)
+#define COMSIG_MOB_LOST_CHAIN_TAIL "living_detached_chain_tail"
+/// Sent from a 'contract chain' button on a mob chain
+#define COMSIG_MOB_CHAIN_CONTRACT "living_chain_contracted"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index fc9fcf22893f..a7a04b7dfdd3 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -7,6 +7,8 @@
#define COMSIG_MOB_LOGIN "mob_login"
///from base of /mob/Logout(): ()
#define COMSIG_MOB_LOGOUT "mob_logout"
+///from base of /mob/mind_initialize
+#define COMSIG_MOB_MIND_INITIALIZED "mob_mind_inited"
///from base of mob/set_stat(): (new_stat, old_stat)
#define COMSIG_MOB_STATCHANGE "mob_statchange"
///from base of mob/clickon(): (atom/A, params)
@@ -25,7 +27,10 @@
/// From base of /mob/living/simple_animal/bot/proc/bot_step()
#define COMSIG_MOB_BOT_STEP "mob_bot_step"
-/// From base of /client/Move()
+/// From base of /mob/proc/update_held_items
+#define COMSIG_MOB_UPDATE_HELD_ITEMS "mob_update_held_items"
+
+/// From base of /client/Move(): (list/move_args)
#define COMSIG_MOB_CLIENT_PRE_LIVING_MOVE "mob_client_pre_living_move"
/// Should we stop the current living movement attempt
#define COMSIG_MOB_CLIENT_BLOCK_PRE_LIVING_MOVE COMPONENT_MOVABLE_BLOCK_PRE_MOVE
@@ -83,11 +88,15 @@
///from base of mob/set_invis_see(): (new_invis, old_invis)
#define COMSIG_MOB_SEE_INVIS_CHANGE "mob_see_invis_change"
-
+/// from /mob/living/proc/apply_damage(): (list/damage_mods, damage, damagetype, def_zone, sharpness, attack_direction, attacking_item)
+/// allows you to add multiplicative damage modifiers to the damage mods argument to adjust incoming damage
+/// not sent if the apply damage call was forced
+#define COMSIG_MOB_APPLY_DAMAGE_MODIFIERS "mob_apply_damage_modifiers"
///from base of /mob/living/proc/apply_damage(): (damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction)
#define COMSIG_MOB_APPLY_DAMAGE "mob_apply_damage"
///from /mob/living/proc/apply_damage(), works like above but after the damage is actually inflicted: (damage, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction)
#define COMSIG_MOB_AFTER_APPLY_DAMAGE "mob_after_apply_damage"
+
///from base of /mob/living/attack_alien(): (user)
#define COMSIG_MOB_ATTACK_ALIEN "mob_attack_alien"
///from base of /mob/throw_item(): (atom/target)
@@ -209,3 +218,6 @@
/// from mob/proc/dropItemToGround()
#define COMSIG_MOB_DROPPING_ITEM "mob_dropping_item"
+
+/// from /mob/proc/change_mob_type_unchecked() : ()
+#define COMSIG_MOB_CHANGED_TYPE "mob_changed_type"
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 5fd653fd600f..08d80b05ad6e 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -129,6 +129,15 @@
#define COMSIG_ITEM_DROPPED "item_drop"
///from base of obj/item/pickup(): (/mob/taker)
#define COMSIG_ITEM_PICKUP "item_pickup"
+///from base of obj/item/on_outfit_equip(): (mob/equipper, visuals_only, slot)
+#define COMSIG_ITEM_EQUIPPED_AS_OUTFIT "item_equip_as_outfit"
+///from base of datum/storage/attempt_insert(): ()
+#define COMSIG_ITEM_STORED "item_stored"
+
+///from base of obj/item/apply_fantasy_bonuses(): (bonus)
+#define COMSIG_ITEM_APPLY_FANTASY_BONUSES "item_apply_fantasy_bonuses"
+///from base of obj/item/remove_fantasy_bonuses(): (bonus)
+#define COMSIG_ITEM_REMOVE_FANTASY_BONUSES "item_remove_fantasy_bonuses"
/// Sebt from obj/item/ui_action_click(): (mob/user, datum/action)
#define COMSIG_ITEM_UI_ACTION_CLICK "item_action_click"
@@ -175,7 +184,11 @@
///from [/obj/structure/closet/supplypod/proc/preOpen]:
#define COMSIG_SUPPLYPOD_LANDED "supplypodgoboom"
-///from /obj/item/storage/book/bible/afterattack(): (mob/user, proximity)
+/// from [/obj/item/stack/proc/can_merge]: (obj/item/stack/merge_with, in_hand)
+#define COMSIG_STACK_CAN_MERGE "stack_can_merge"
+ #define CANCEL_STACK_MERGE (1<<0)
+
+///from /obj/item/book/bible/afterattack(): (mob/user, proximity)
#define COMSIG_BIBLE_SMACKED "bible_smacked"
///stops the bible chain from continuing. When all of the effects of the bible smacking have been moved to a signal we can kill this
#define COMSIG_END_BIBLE_CHAIN (1<<0)
@@ -221,6 +234,11 @@
///called when getting the item's exact ratio for cargo's profit, without selling the item.
#define COMSIG_ITEM_SPLIT_PROFIT_DRY "item_split_profits_dry"
+/// Called on component/uplink/OnAttackBy(..)
+#define COMSIG_ITEM_ATTEMPT_TC_REIMBURSE "item_attempt_tc_reimburse"
+///Called when a holoparasite/guardiancreator is used.
+#define COMSIG_TRAITOR_ITEM_USED(type) "traitor_item_used_[type]"
+
// /obj/item/clothing signals
///from [/mob/living/carbon/human/Move]: ()
diff --git a/code/__DEFINES/dcs/signals/signals_reagent.dm b/code/__DEFINES/dcs/signals/signals_reagent.dm
index 00fdddc6c9e7..a73d59a234c2 100644
--- a/code/__DEFINES/dcs/signals/signals_reagent.dm
+++ b/code/__DEFINES/dcs/signals/signals_reagent.dm
@@ -6,6 +6,8 @@
#define COMSIG_ATOM_EXPOSE_REAGENTS "atom_expose_reagents"
/// Prevents the atom from being exposed to reagents if returned on [COMSIG_ATOM_EXPOSE_REAGENTS]
#define COMPONENT_NO_EXPOSE_REAGENTS (1<<0)
+///from base of atom/expose_reagents(): (/list, /datum/reagents, methods, volume_modifier, show_message)
+#define COMSIG_ATOM_AFTER_EXPOSE_REAGENTS "atom_after_expose_reagents"
///from base of [/datum/reagent/proc/expose_atom]: (/datum/reagent, reac_volume)
#define COMSIG_ATOM_EXPOSE_REAGENT "atom_expose_reagent"
///from base of [/datum/reagent/proc/expose_atom]: (/atom, reac_volume)
diff --git a/code/__DEFINES/dcs/signals/signals_turf.dm b/code/__DEFINES/dcs/signals/signals_turf.dm
index ea4791f5629b..7ea0a9631096 100644
--- a/code/__DEFINES/dcs/signals/signals_turf.dm
+++ b/code/__DEFINES/dcs/signals/signals_turf.dm
@@ -39,3 +39,5 @@
#define COMSIG_TURF_PREPARE_STEP_SOUND "turf_prepare_step_sound"
///from base of datum/thrownthing/finalize(): (turf/turf, atom/movable/thrownthing) when something is thrown and lands on us
#define COMSIG_TURF_MOVABLE_THROW_LANDED "turf_movable_throw_landed"
+///from /obj/item/pushbroom/sweep(): (broom, user, items_to_sweep)
+#define COMSIG_TURF_RECEIVE_SWEEPED_ITEMS "turf_receive_sweeped_items"
diff --git a/code/__DEFINES/drone.dm b/code/__DEFINES/drone.dm
index 46c19fdbaf41..caf8f5015ecd 100644
--- a/code/__DEFINES/drone.dm
+++ b/code/__DEFINES/drone.dm
@@ -2,9 +2,8 @@
/// If drones are blacklisted from certain sensitive machines
GLOBAL_VAR_INIT(drone_machine_blacklist_enabled, TRUE)
-#define DRONE_HANDS_LAYER 1
-#define DRONE_HEAD_LAYER 2
-#define DRONE_TOTAL_LAYERS 2
+#define DRONE_HEAD_LAYER 1
+#define DRONE_TOTAL_LAYERS 1
/// Message displayed when new drone spawns in drone network
#define DRONE_NET_CONNECT span_notice("DRONE NETWORK: [name] connected.")
diff --git a/code/__DEFINES/explosions.dm b/code/__DEFINES/explosions.dm
index 0ce9644af1c2..a1645b659d1c 100644
--- a/code/__DEFINES/explosions.dm
+++ b/code/__DEFINES/explosions.dm
@@ -23,8 +23,10 @@
if(!(target.flags_1 & PREVENT_CONTENTS_EXPLOSION_1)) { \
target.contents_explosion(##args);\
};\
- SEND_SIGNAL(target, COMSIG_ATOM_EX_ACT, ##args);\
- target.ex_act(##args);
+ if(!(SEND_SIGNAL(target, COMSIG_ATOM_PRE_EX_ACT, ##args) & COMPONENT_CANCEL_EX_ACT)) { \
+ SEND_SIGNAL(target, COMSIG_ATOM_EX_ACT, ##args);\
+ target.ex_act(##args);\
+ }
// Internal explosion argument list keys.
// Must match the arguments to [/datum/controller/subsystem/explosions/proc/propagate_blastwave]
diff --git a/code/__DEFINES/footsteps.dm b/code/__DEFINES/footsteps.dm
index 4ec2ce0f90d2..fd88a17ce9f5 100644
--- a/code/__DEFINES/footsteps.dm
+++ b/code/__DEFINES/footsteps.dm
@@ -18,15 +18,25 @@
//misc footstep sounds
#define FOOTSTEP_GENERIC_HEAVY "heavy"
+
//footstep mob defines
-#define FOOTSTEP_MOB_CLAW 1
-#define FOOTSTEP_MOB_BAREFOOT 2
-#define FOOTSTEP_MOB_HEAVY 3
-#define FOOTSTEP_MOB_SHOE 4
-#define FOOTSTEP_MOB_HUMAN 5 //Warning: Only works on /mob/living/carbon/human
-#define FOOTSTEP_MOB_SLIME 6
-#define FOOTSTEP_OBJ_MACHINE 7
-#define FOOTSTEP_OBJ_ROBOT 8
+#define FOOTSTEP_MOB_CLAW "footstep_claw"
+#define FOOTSTEP_MOB_BAREFOOT "footstep_barefoot"
+#define FOOTSTEP_MOB_HEAVY "footstep_heavy"
+#define FOOTSTEP_MOB_SHOE "footstep_shoe"
+#define FOOTSTEP_MOB_HUMAN "footstep_human" //Warning: Only works on /mob/living/carbon/human
+#define FOOTSTEP_MOB_SLIME "footstep_slime"
+#define FOOTSTEP_MOB_RUST "footstep_rust"
+#define FOOTSTEP_OBJ_MACHINE "footstep_machine"
+#define FOOTSTEP_OBJ_ROBOT "footstep_robot"
+
+//priority defines for the footstep_override element
+#define STEP_SOUND_NO_PRIORITY 0
+#define STEP_SOUND_CONVEYOR_PRIORITY 1
+#define STEP_SOUND_TABLE_PRIORITY 2
+
+///the name of the index key for priority
+#define STEP_SOUND_PRIORITY "step_sound_priority"
/*
diff --git a/code/__DEFINES/guardian_defines.dm b/code/__DEFINES/guardian_defines.dm
index d7aae6965a75..ae2c3175b4a6 100644
--- a/code/__DEFINES/guardian_defines.dm
+++ b/code/__DEFINES/guardian_defines.dm
@@ -3,6 +3,28 @@
#define GUARDIAN_THEME_CARP "carp"
#define GUARDIAN_THEME_MINER "miner"
-#define GUARDIAN_COLOR_LAYER 2
-#define GUARDIAN_HANDS_LAYER 1
-#define GUARDIAN_TOTAL_LAYERS 2
+#define GUARDIAN_MAGIC "magic"
+#define GUARDIAN_TECH "tech"
+
+#define GUARDIAN_ASSASSIN "assassin"
+#define GUARDIAN_CHARGER "charger"
+#define GUARDIAN_DEXTROUS "dextrous"
+#define GUARDIAN_EXPLOSIVE "explosive"
+#define GUARDIAN_GASEOUS "gaseous"
+#define GUARDIAN_GRAVITOKINETIC "gravitokinetic"
+#define GUARDIAN_LIGHTNING "lightning"
+#define GUARDIAN_PROTECTOR "protector"
+#define GUARDIAN_RANGED "ranged"
+#define GUARDIAN_STANDARD "standard"
+#define GUARDIAN_SUPPORT "support"
+
+/// List of all guardians currently extant
+GLOBAL_LIST_EMPTY(parasites)
+
+/// Assoc list of guardian theme singletons
+GLOBAL_LIST_INIT(guardian_themes, list(
+ GUARDIAN_THEME_TECH = new /datum/guardian_fluff/tech,
+ GUARDIAN_THEME_MAGIC = new /datum/guardian_fluff,
+ GUARDIAN_THEME_CARP = new /datum/guardian_fluff/carp,
+ GUARDIAN_THEME_MINER = new /datum/guardian_fluff/miner,
+))
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 37611368f8b1..e84d26de32c3 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -80,6 +80,7 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list(
#define isabductor(A) (is_species(A, /datum/species/abductor))
#define isgolem(A) (is_species(A, /datum/species/golem))
#define islizard(A) (is_species(A, /datum/species/lizard))
+#define isashwalker(A) (is_species(A, /datum/species/lizard/ashwalker))
#define isplasmaman(A) (is_species(A, /datum/species/plasmaman))
#define issimian(A) (is_species(A, /datum/species/simian)) //Monkestation Addition
#define ispodperson(A) (is_species(A, /datum/species/pod))
@@ -130,8 +131,16 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list(
// basic mobs
#define isbasicmob(A) (istype(A, /mob/living/basic))
+#define isconstruct(A) (istype(A, /mob/living/basic/construct))
+
#define iscow(A) (istype(A, /mob/living/basic/cow))
+#define isgorilla(A) (istype(A, /mob/living/basic/gorilla))
+
+#define isshade(A) (istype(A, /mob/living/basic/shade))
+
+#define is_simian(A) (isgorilla(A) || ismonkey(A))
+
/// returns whether or not the atom is either a basic mob OR simple animal
#define isanimal_or_basicmob(A) (istype(A, /mob/living/simple_animal) || istype(A, /mob/living/basic))
@@ -143,17 +152,15 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list(
//Simple animals
#define isanimal(A) (istype(A, /mob/living/simple_animal))
-#define isrevenant(A) (istype(A, /mob/living/simple_animal/revenant))
+#define isrevenant(A) (istype(A, /mob/living/basic/revenant))
#define isbot(A) (istype(A, /mob/living/simple_animal/bot))
-#define isshade(A) (istype(A, /mob/living/simple_animal/shade))
-
#define ismouse(A) (istype(A, /mob/living/basic/mouse))
#define isslime(A) (istype(A, /mob/living/simple_animal/slime))
-#define isdrone(A) (istype(A, /mob/living/simple_animal/drone))
+#define isdrone(A) (istype(A, /mob/living/basic/drone))
#define iscat(A) (istype(A, /mob/living/simple_animal/pet/cat))
@@ -165,13 +172,11 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list(
#define isregalrat(A) (istype(A, /mob/living/basic/regal_rat))
-#define isguardian(A) (istype(A, /mob/living/simple_animal/hostile/guardian))
-
-#define isconstruct(A) (istype(A, /mob/living/simple_animal/hostile/construct))
+#define isguardian(A) (istype(A, /mob/living/basic/guardian))
#define ismegafauna(A) (istype(A, /mob/living/simple_animal/hostile/megafauna))
-#define isclown(A) (istype(A, /mob/living/simple_animal/hostile/retaliate/clown))
+#define isclown(A) (istype(A, /mob/living/basic/clown))
#define isspider(A) (istype(A, /mob/living/basic/spider/giant))
@@ -216,6 +221,8 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list(
#define ismachinery(A) (istype(A, /obj/machinery))
+#define isvendor(A) (istype(A, /obj/machinery/vending))
+
#define isvehicle(A) (istype(A, /obj/vehicle))
#define ismecha(A) (istype(A, /obj/vehicle/sealed/mecha))
@@ -267,8 +274,6 @@ GLOBAL_LIST_INIT(glass_sheet_types, typecacheof(list(
#define isholoeffect(O) (istype(O, /obj/effect/holodeck_effect))
-#define isblobmonster(O) (istype(O, /mob/living/simple_animal/hostile/blob))
-
#define isshuttleturf(T) (!isnull(T.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle)))
#define isProbablyWallMounted(O) (O.pixel_x > 20 || O.pixel_x < -20 || O.pixel_y > 20 || O.pixel_y < -20)
diff --git a/code/__DEFINES/megafauna.dm b/code/__DEFINES/megafauna.dm
new file mode 100644
index 000000000000..981b8d49f520
--- /dev/null
+++ b/code/__DEFINES/megafauna.dm
@@ -0,0 +1,4 @@
+/// Temperature of drake fire hotspots
+#define DRAKE_FIRE_TEMP 500
+/// Volume of drake fire hotspots
+#define DRAKE_FIRE_EXPOSURE 50
diff --git a/code/__DEFINES/mood.dm b/code/__DEFINES/mood.dm
new file mode 100644
index 000000000000..161f253b04c7
--- /dev/null
+++ b/code/__DEFINES/mood.dm
@@ -0,0 +1 @@
+#define MOOD_CATEGORY_LEGION_CORE "regenerative core"
diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm
index 51bb3d2e9afa..36ad2ddd1e30 100644
--- a/code/__DEFINES/obj_flags.dm
+++ b/code/__DEFINES/obj_flags.dm
@@ -42,6 +42,8 @@
#define ITEM_HAS_CONTEXTUAL_SCREENTIPS (1 << 19)
/// No blood overlay is allowed to appear on this item, and it cannot gain blood DNA forensics
#define NO_BLOOD_ON_ITEM (1 << 20)
+/// Whether this item should skip the /datum/component/fantasy applied on spawn on the RPG event. Used on things like stacks
+#define SKIP_FANTASY_ON_SPAWN (1<<21)
// Flags for the clothing_flags var on /obj/item/clothing
diff --git a/code/__DEFINES/pai.dm b/code/__DEFINES/pai.dm
index f47524d32e26..4015ee3bc504 100644
--- a/code/__DEFINES/pai.dm
+++ b/code/__DEFINES/pai.dm
@@ -11,6 +11,13 @@
/// The amount of time between spamming for pAI candidates
#define PAI_SPAM_TIME (40 SECONDS)
+/// Maximum distance you can set the holoform leash
+#define HOLOFORM_MAX_RANGE 9
+/// Minimum distance you can set the holoform leash
+#define HOLOFORM_MIN_RANGE 3
+/// Default holoform leash distance
+#define HOLOFORM_DEFAULT_RANGE HOLOFORM_MAX_RANGE
+
/// UI action to toggle huds
#define PAI_TOGGLE_MEDICAL_HUD 0
#define PAI_TOGGLE_SECURITY_HUD 1
diff --git a/code/__DEFINES/projectiles.dm b/code/__DEFINES/projectiles.dm
index 8283e3840598..712b1503aa17 100644
--- a/code/__DEFINES/projectiles.dm
+++ b/code/__DEFINES/projectiles.dm
@@ -56,6 +56,7 @@
#define CALIBER_HOOK "hook"
/// The caliber used by the changeling tentacle mutation.
#define CALIBER_TENTACLE "tentacle"
+#define CALIBER_A223 "a223"
/// For gunpoints, how many tiles around the target the shooter can roam without losing their shot
#define GUNPOINT_SHOOTER_STRAY_RANGE 2
diff --git a/code/__DEFINES/song.dm b/code/__DEFINES/song.dm
index 7af269fbe219..782a7923ea14 100644
--- a/code/__DEFINES/song.dm
+++ b/code/__DEFINES/song.dm
@@ -13,3 +13,5 @@
///it's what monkeys play!
#define MONKEY_SONG "BPM: 200\nC4/0,14,C,A4-F2,F3,A3,F-F2,A-F,F4,G4,F,D4-Bb2-G2\nD3,G3,D-G2,G3-G2,D,D4-G3,D,B4-B2,G,B3,G-B2,B3-B2\nG4,A4,G,E4-C3,E3,G3,E-C,G-C,E,E4-G,E,C5-E-A3,C4\nA-E3,C,E4-C3,A4-C4,B4-A3-A2,C5-C4,D5-F-B3,D4,B-F3\nD,F4-D3,D4,F-B-B2,G4-D,A4-C-F3,F,C/2,B3/2,A3-C3/2\nB/2,C4,E-C3,F4,G-C,F-F3,F-C,C4/2,B/2,A-A2/2,G3/2\nF/I"
+///song played by the mook bard
+#define MOOK_SONG "BPM: 240\nA5,B5,C#6,D6,E6/0.17,A/0.5,A/0.25,A3/0.25\nA4/0.25,C#5/0.25,E5/0.25,A/0.25,C#/0.25,E/0.12\nC#6/0.25,C#/0.25,E6/0.25,A3/0.25,A4/0.25\nC#5/0.25,E5/0.25,A/0.25,C#/0.25,E/0.25,D/0.25\nG6/0.25,D/0.17,F6/0.17,C#6/0.5,E6/0.5,D4/0.25\nA/0.25,D5/0.25,F5/0.25,A/0.25,D/0.25,F/0.25\nD6/0.08,F6/0.08,D4/0.25,A/0.25,D5/0.25,F5/0.25\nCn4/0.2,B/0.17,D6/0.17,G5/0.5,G/0.25,B3/0.25\nD4/0.25,G4/0.25,B4/0.25,D/0.25,G/0.25,B/0.12\nB5/0.25,B/0.25,D6/0.25,G3/0.25,G4/0.25,B4/0.25\nF/0.25,G/0.25,B/0.25,F/0.25,D/0.25,F6/0.25\nC6/0.17,E/0.17,B5/0.5,D#/0.5,C4/0.25,G/0.25\nC5/0.25,E5/0.25,G/0.25,C/0.25,E/0.25,C6/0.08\nE6/0.08,C4/0.25,Dn4/0.25,E4/0.25,A5/0.17,B/0.5\nC6/0.25,F5/0.08,F4/0.08,C5/0.08,E5/0.12,G5/0.12\nC6/0.25,E6/0.25,E4/0.08,C5/0.08,B/0.17,F6/0.17\nE6/0.5,B/0.25,E4/0.08,G#4/0.08,C6/0.17,D6/0.5\nE6/0.25,A3/0.25,E4/0.25,C5/0.25,Gn3/0.25\nF5/0.12,A5/0.12,A6/0.25,F3/0.25,F4/0.12,A4/0.12\nC/0.12,F6/0.17,A6/0.17,G#6/0.5,A/0.25,F3/0.25\nF4/0.12,A4/0.12,D#5/0.12,B/0.17,G#/0.17,B6/0.5\nB5/0.25,G#/0.25,E3/0.25,E4/0.12,G#4/0.12\nDn/0.12,E6/0.08,E3/0.25,F#3/0.25,G#3/0.25\nE5/0.17,A5/0.17,E/0.5,E/0.25,A3/0.25,C#4/0.25\nE4/0.25,C#/0.25,E/0.12,A5/0.5,B/0.5,C#6/0.5\nD6/0.5,A3/0.25,C#4/0.25,E/0.25,C#/0.25,E/0.25\nE6/0.08,Gn/0.25,E4/0.25,A4/0.25,C#5/0.25,E/0.25\nA/0.25,C#/0.25,E6/0.17,E/0.5,Fn6/0.5,G6/0.5\nG3/0.25,E4/0.25,A/0.25,C#/0.25,E/0.25,A/0.25\nC#/0.25,F/0.08,A6/0.08,F3/0.25,F4/0.25,A4/0.25\nCn/0.25,F/0.25,A/0.25,C/0.25,G6/0.12,A6/0.12\nG A G F6 G3/0.25 D4/0.25 G4/0.25 B4/0.25 D/0.25\nG/0.25 B/0.25 E6/0.12 G6/0.12 F/0.71 G/0.71 F/0.71\nE3/0.25 E4/0.25 G4/0.25 B/0.25 E/0.25 G/0.25 B/0.25\nA5/0.08 E6/0.08 A3/0.25 E4/0.25 A4/0.25 C#/0.25 E/0.25 A/0.25 C#/0.25 D6/0.17 E6/0.5 F/0.25 B3/0.25 D4/0.12 F4/0.12 B4/0.12 F6/0.25 E/0.25 D6/0.25 G#3/0.25 E4/0.12 G#4/0.12 B/0.12 Cn6/0.12 D/0.25 A3/0.25 A4/0.25 C5/0.25 E5/0.25 G#3/0.25 Gn/0.25 C4/0.25 E4/0.25 A/"
diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm
index 69190c39b626..7e31cc4a48b6 100644
--- a/code/__DEFINES/span.dm
+++ b/code/__DEFINES/span.dm
@@ -17,11 +17,13 @@
#define span_bigicon(str) ("" + str + "")
#define span_binarysay(str) ("" + str + "")
#define span_blob(str) ("" + str + "")
+#define span_blobannounce(str) ("" + str + "")
#define span_blue(str) ("" + str + "")
#define span_blueteamradio(str) ("" + str + "")
#define span_bold(str) ("" + str + "")
#define span_boldannounce(str) ("" + str + "")
#define span_bolddanger(str) ("" + str + "")
+#define span_bolditalic(str) ("" + str + "")
#define span_boldnicegreen(str) ("" + str + "")
#define span_boldnotice(str) ("" + str + "")
#define span_boldwarning(str) ("" + str + "")
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 07230479d473..a8455c502404 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -38,6 +38,8 @@
#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
+#define STASIS_ADMIN "stasis_admin"
+#define STASIS_LEGION_EATEN "stasis_eaten"
#define STASIS_NETPOD_EFFECT "stasis_netpod"
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 2a7084841746..b92ea50900c7 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -317,6 +317,9 @@
#define SSMOBS_DT (SSmobs.wait/10)
#define SSOBJ_DT (SSobj.wait/10)
+// The change in the world's time from the subsystem's last fire in seconds.
+#define DELTA_WORLD_TIME(ss) ((world.time - ss.last_fire) * 0.1)
+
/// The timer key used to know how long subsystem initialization takes
#define SS_INIT_TIMER_KEY "ss_init"
diff --git a/code/__DEFINES/surgery.dm b/code/__DEFINES/surgery.dm
index 5f698598d73e..e5b528f96d30 100644
--- a/code/__DEFINES/surgery.dm
+++ b/code/__DEFINES/surgery.dm
@@ -42,3 +42,8 @@
#define SURGERY_REQUIRE_LIMB (1<<3)
///Will allow the surgery to work only if there's a real (eg. not pseudopart) limb.
#define SURGERY_REQUIRES_REAL_LIMB (1<<4)
+///Will grant a bonus during surgery steps to users with TRAIT_MORBID while they're using tools with CRUEL_IMPLEMENT
+#define SURGERY_MORBID_CURIOSITY (1<<5)
+
+///Return true if target is not in a valid body position for the surgery
+#define IS_IN_INVALID_SURGICAL_POSITION(target, surgery) ((surgery.surgery_flags & SURGERY_REQUIRE_RESTING) && (target.mobility_flags & MOBILITY_LIEDOWN && target.body_position != LYING_DOWN))
diff --git a/code/__DEFINES/trader.dm b/code/__DEFINES/trader.dm
new file mode 100644
index 000000000000..aae6da264998
--- /dev/null
+++ b/code/__DEFINES/trader.dm
@@ -0,0 +1,16 @@
+#define ITEM_REJECTED_PHRASE "ITEM_REJECTED_PHRASE"
+#define ITEM_SELLING_CANCELED_PHRASE "ITEM_SELLING_CANCELED_PHRASE"
+#define ITEM_SELLING_ACCEPTED_PHRASE "ITEM_SELLING_ACCEPTED_PHRASE"
+#define INTERESTED_PHRASE "INTERESTED_PHRASE"
+#define BUY_PHRASE "BUY_PHRASE"
+#define NO_CASH_PHRASE "NO_CASH_PHRASE"
+#define NO_STOCK_PHRASE "NO_STOCK_PHRASE"
+#define NOT_WILLING_TO_BUY_PHRASE "NOT_WILLING_TO_BUY_PHRASE"
+#define ITEM_IS_WORTHLESS_PHRASE "ITEM_IS_WORTHLESS_PHRASE"
+#define TRADER_HAS_ENOUGH_ITEM_PHRASE "TRADER_HAS_ENOUGH_ITEM_PHRASE"
+#define TRADER_LORE_PHRASE "TRADER_LORE_PHRASE"
+#define TRADER_BATTLE_START_PHRASE "TRADER_AGGRO_PHRASE"
+#define TRADER_BATTLE_END_PHRASE "TRADER_DEAGGRO_PHRASE"
+#define TRADER_NOT_BUYING_ANYTHING "TRADER_NOT_BUYING_ANYTHING"
+#define TRADER_NOT_SELLING_ANYTHING "TRADER_NOT_SELLING_ANYTHING"
+#define TRADER_SHOP_OPENING_PHRASE "TRADER_SHOP_OPENING_PHRASE"
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 7ef7a0f10c38..014a5a970fef 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -144,6 +144,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_PULL_BLOCKED "pullblocked"
/// Abstract condition that prevents movement if being pulled and might be resisted against. Handcuffs and straight jackets, basically.
#define TRAIT_RESTRAINED "restrained"
+/// Apply this to make a mob not dense, and remove it when you want it to no longer make them undense, other sorces of undesity will still apply. Always define a unique source when adding a new instance of this!
+#define TRAIT_UNDENSE "undense"
+/// Expands our FOV by 30 degrees if restricted
+#define TRAIT_EXPANDED_FOV "expanded_fov"
/// Doesn't miss attacks
#define TRAIT_PERFECT_ATTACKER "perfect_attacker"
#define TRAIT_INCAPACITATED "incapacitated"
@@ -172,6 +176,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_DEFIB_BLACKLISTED "defib_blacklisted"
#define TRAIT_BADDNA "baddna"
#define TRAIT_CLUMSY "clumsy"
+/// Trait that means you are capable of holding items in some form
+#define TRAIT_CAN_HOLD_ITEMS "can_hold_items"
+/// Trait which lets you clamber over a barrier
+#define TRAIT_FENCE_CLIMBER "can_climb_fences"
/// means that you can't use weapons with normal trigger guards.
#define TRAIT_CHUNKYFINGERS "chunkyfingers"
#define TRAIT_DUMB "dumb"
@@ -189,8 +197,13 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Makes the owner appear as dead to most forms of medical examination
#define TRAIT_FAKEDEATH "fakedeath"
#define TRAIT_DISFIGURED "disfigured"
+/// "Magic" trait that blocks the mob from moving or interacting with anything. Used for transient stuff like mob transformations or incorporality in special cases.
+/// Will block movement, `Life()` (!!!), and other stuff based on the mob.
+#define TRAIT_NO_TRANSFORM "block_transformations"
/// Tracks whether we're gonna be a baby alien's mummy.
#define TRAIT_XENO_HOST "xeno_host"
+/// This parrot is currently perched
+#define TRAIT_PARROT_PERCHED "parrot_perched"
/// This mob is immune to stun causing status effects and stamcrit.
/// Prefer to use [/mob/living/proc/check_stun_immunity] over checking for this trait exactly.
#define TRAIT_STUNIMMUNE "stun_immunity"
@@ -479,6 +492,19 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
// and emit less heat. Present on /mob or /datum/mind
#define TRAIT_SUPERMATTER_SOOTHER "supermatter_soother"
+/// Trait added when a revenant is visible.
+#define TRAIT_REVENANT_REVEALED "revenant_revealed"
+/// Trait added when a revenant has been inhibited (typically by the bane of a holy weapon)
+#define TRAIT_REVENANT_INHIBITED "revenant_inhibited"
+
+/// Trait which prevents you from becoming overweight
+#define TRAIT_NOFAT "cant_get_fat"
+
+/// Trait which allows you to eat rocks
+#define TRAIT_ROCK_EATER "rock_eater"
+/// Trait which allows you to gain bonuses from consuming rocks
+#define TRAIT_ROCK_METAMORPHIC "rock_metamorphic"
+
/// `do_teleport` will not allow this atom to teleport
#define TRAIT_NO_TELEPORT "no-teleport"
/// This atom is a secluded location, which is counted as out of bounds.
@@ -587,7 +613,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_TENTACLE_IMMUNE "tentacle_immune"
/// Currently under the effect of overwatch
#define TRAIT_OVERWATCHED "watcher_overwatched"
-/// Cannot be targetted by watcher overwatch
+/// Cannot be targeted by watcher overwatch
#define TRAIT_OVERWATCH_IMMUNE "overwatch_immune"
//non-mob traits
@@ -829,6 +855,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
///Trait needed for the lubefish evolution
#define TRAIT_FISH_FED_LUBE "fish_fed_lube"
+/// Trait given to angelic constructs to let them purge cult runes
+#define TRAIT_ANGELIC "angelic"
+
// common trait sources
#define TRAIT_GENERIC "generic"
#define UNCONSCIOUS_TRAIT "unconscious"
@@ -899,6 +928,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define SUIT_TRAIT "suit"
/// Trait associated to lying down (having a [lying_angle] of a different value than zero).
#define LYING_DOWN_TRAIT "lying-down"
+/// A trait gained by leaning against a wall
+#define LEANING_TRAIT "leaning"
/// Trait associated to lacking electrical power.
#define POWER_LACK_TRAIT "power-lack"
/// Trait associated to lacking motor movement
@@ -968,7 +999,15 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define SPECIES_FLIGHT_TRAIT "species-flight"
#define FROSTMINER_ENRAGE_TRAIT "frostminer-enrage"
#define NO_GRAVITY_TRAIT "no-gravity"
+/// A trait gained from a mob's leap action, like the leaper
#define LEAPING_TRAIT "leaping"
+/// A trait gained from a mob's vanish action, like the herophant
+#define VANISHING_TRAIT "vanishing"
+/// A trait gained from a mob's swoop action, like the ash drake
+#define SWOOPING_TRAIT "swooping"
+/// A trait gained from a mob's mimic ability, like the mimic
+#define MIMIC_TRAIT "mimic"
+#define SHRUNKEN_TRAIT "shrunken"
#define LEAPER_BUBBLE_TRAIT "leaper-bubble"
#define DNA_VAULT_TRAIT "dna_vault"
/// sticky nodrop sounds like a bad soundcloud rapper's name
@@ -1037,7 +1076,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define AUTOPSY_TRAIT "autopsy_trait"
/// Trait given by [/datum/status_effect/blessing_of_insanity]
#define MAD_WIZARD_TRAIT "mad_wizard_trait"
-
+/// Isn't attacked harmfully by blob structures
+#define TRAIT_BLOB_ALLY "blob_ally"
/**
* Trait granted by [/mob/living/carbon/Initialize] and
@@ -1194,3 +1234,24 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Do IPC's dream of doomsday? The answer is yes
#define TRAIT_ROBOT_CAN_BLEED "robots_can_bleed"
//monkestation edit end
+/// This atom can have spells cast from it if a mob is within it
+/// This means the "caster" of the spell is changed to the mob's loc
+/// Note this doesn't mean all spells are guaranteed to work or the mob is guaranteed to cast
+#define TRAIT_CASTABLE_LOC "castable_loc"
+
+///Trait given by /datum/element/relay_attacker
+#define TRAIT_RELAYING_ATTACKER "relaying_attacker"
+// unique trait sources, still defines
+#define EMP_TRAIT "emp_trait"
+
+/// Trait given while using /datum/action/cooldown/mob_cooldown/wing_buffet
+#define TRAIT_WING_BUFFET "wing_buffet"
+/// Trait given while tired after using /datum/action/cooldown/mob_cooldown/wing_buffet
+#define TRAIT_WING_BUFFET_TIRED "wing_buffet_tired"
+/// Trait given to a dragon who fails to defend their rifts
+#define TRAIT_RIFT_FAILURE "fail_dragon_loser"
+
+///trait determines if this mob can breed given by /datum/component/breeding
+#define TRAIT_MOB_BREEDER "mob_breeder"
+/// Trait given to mobs that we do not want to mindswap
+#define TRAIT_NO_MINDSWAP "no_mindswap"
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index 32f5837c3d24..31e722178279 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -57,7 +57,7 @@
// VV HREF KEYS
#define VV_HK_TARGET "target"
-#define VV_HK_VARNAME "targetvar" //name or index of var for 1 variable targetting hrefs.
+#define VV_HK_VARNAME "targetvar" //name or index of var for 1 variable targeting hrefs.
// vv_do_list() keys
#define VV_HK_LIST_ADD "listadd"
diff --git a/code/__DEFINES/wounds.dm b/code/__DEFINES/wounds.dm
index 0aadb71d58be..815e60d0738e 100644
--- a/code/__DEFINES/wounds.dm
+++ b/code/__DEFINES/wounds.dm
@@ -1,4 +1,3 @@
-
// ~wound damage/rolling defines
/// the cornerstone of the wound threshold system, your base wound roll for any attack is rand(1, damage^this), after armor reduces said damage. See [/obj/item/bodypart/proc/check_wounding]
#define WOUND_DAMAGE_EXPONENT 1.4
@@ -13,6 +12,10 @@
/// set wound_bonus on an item or attack to this to disable checking wounding for the attack
#define CANT_WOUND -100
+/// If there are multiple possible and valid wounds for the same type and severity, weight will be used to pick among them. See _wound_pregen_data.dm for more details
+/// This is used in pick_weight, so use integers
+#define WOUND_DEFAULT_WEIGHT 50
+
// ~wound severities
/// for jokey/meme wounds like stubbed toe, no standard messages/sounds or second winds
#define WOUND_SEVERITY_TRIVIAL 0
@@ -22,16 +25,26 @@
/// outright dismemberment of limb
#define WOUND_SEVERITY_LOSS 4
+/// A "chronological" list of wound severities, starting at the least severe.
+GLOBAL_LIST_INIT(wound_severities_chronological, list(
+ "[WOUND_SEVERITY_TRIVIAL]",
+ "[WOUND_SEVERITY_MODERATE]",
+ "[WOUND_SEVERITY_SEVERE]",
+ "[WOUND_SEVERITY_CRITICAL]"
+))
-// ~wound categories
+// ~wound categories: wounding_types
/// any brute weapon/attack that doesn't have sharpness. rolls for blunt bone wounds
-#define WOUND_BLUNT 1
+#define WOUND_BLUNT "wound_blunt"
/// any brute weapon/attack with sharpness = SHARP_EDGED. rolls for slash wounds
-#define WOUND_SLASH 2
+#define WOUND_SLASH "wound_slash"
/// any brute weapon/attack with sharpness = SHARP_POINTY. rolls for piercing wounds
-#define WOUND_PIERCE 3
+#define WOUND_PIERCE "wound_pierce"
/// any concentrated burn attack (lasers really). rolls for burning wounds
-#define WOUND_BURN 4
+#define WOUND_BURN "wound_burn"
+
+/// Mainly a define used for wound_pregen_data, if a pregen data instance expects this, it will accept any and all wound types, even none at all
+#define WOUND_ALL "wound_all"
// ~determination second wind defines
@@ -46,20 +59,226 @@
/// While someone has determination in their system, their bleed rate is slightly reduced
#define WOUND_DETERMINATION_BLEED_MOD 0.85
-// ~wound global lists
-// list in order of highest severity to lowest
-GLOBAL_LIST_INIT(global_wound_types, list(WOUND_BLUNT = list(/datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/moderate),
- WOUND_SLASH = list(/datum/wound/slash/critical, /datum/wound/slash/severe, /datum/wound/slash/moderate),
- WOUND_PIERCE = list(/datum/wound/pierce/critical, /datum/wound/pierce/severe, /datum/wound/pierce/moderate),
- WOUND_BURN = list(/datum/wound/burn/critical, /datum/wound/burn/severe, /datum/wound/burn/moderate)
- ))
+/// Wounds using this competition mode will remove any wounds of a greater severity than itself in a random wound roll. In most cases, you dont want to use this.
+#define WOUND_COMPETITION_OVERPOWER_GREATERS "wound_submit"
+/// Wounds using this competition mode will remove any wounds of a lower severity than itself in a random wound roll. Used for ensuring the worse case scenario of a given injury_roll.
+#define WOUND_COMPETITION_OVERPOWER_LESSERS "wound_dominate"
+
+// ~biology defines
+// What kind of biology a limb has, and what wounds it can suffer
+/// Has absolutely fucking nothing, no wounds
+#define BIO_INORGANIC NONE
+/// Has bone - allows the victim to suffer T2-T3 bone blunt wounds
+#define BIO_BONE (1<<0)
+/// Has flesh - allows the victim to suffer fleshy slash pierce and burn wounds
+#define BIO_FLESH (1<<1)
+/// Has metal - allows the victim to suffer robotic blunt and burn wounds
+#define BIO_METAL (1<<2)
+/// Is wired internally - allows the victim to suffer electrical wounds (robotic T1-T3 slash/pierce)
+#define BIO_WIRED (1<<3)
+/// Has bloodflow - can suffer bleeding wounds and can bleed
+#define BIO_BLOODED (1<<4)
+/// Is connected by a joint - can suffer T1 bone blunt wounds (dislocation)
+#define BIO_JOINTED (1<<5)
+/// Robotic - can suffer all metal/wired wounds, such as: UNIMPLEMENTED PLEASE UPDATE ONCE SYNTH WOUNDS 9/5/2023 ~Niko
+#define BIO_ROBOTIC (BIO_METAL|BIO_WIRED)
+/// Has flesh and bone - See BIO_BONE and BIO_FLESH
+#define BIO_FLESH_BONE (BIO_BONE|BIO_FLESH)
+/// Standard humanoid - can bleed and suffer all flesh/bone wounds, such as: T1-3 slash/pierce/burn/blunt, except dislocations. Think human heads/chests
+#define BIO_STANDARD_UNJOINTED (BIO_FLESH_BONE|BIO_BLOODED)
+/// Standard humanoid limbs - can bleed and suffer all flesh/bone wounds, such as: T1-3 slash/pierce/burn/blunt. Can also bleed, and be dislocated. Think human arms and legs
+#define BIO_STANDARD_JOINTED (BIO_STANDARD_UNJOINTED|BIO_JOINTED)
+
+// "Where" a specific biostate is within a given limb
+// Interior is hard shit, the last line, shit like bones
+// Exterior is soft shit, targeted by slashes and pierces (usually), protects exterior
+// A limb needs both mangled interior and exterior to be dismembered, but slash/pierce must mangle exterior to attack the interior
+// Not having exterior/interior counts as mangled exterior/interior for the purposes of dismemberment
+/// The given biostate is on the "interior" of the limb - hard shit, protected by exterior
+#define ANATOMY_INTERIOR (1<<0)
+/// The given biostate is on the "exterior" of the limb - soft shit, protects interior
+#define ANATOMY_EXTERIOR (1<<1)
+#define ANATOMY_EXTERIOR_AND_INTERIOR (ANATOMY_EXTERIOR|ANATOMY_INTERIOR)
+
+/// A assoc list of BIO_ define to EXTERIOR/INTERIOR defines.
+/// This is where the interior/exterior state of a given biostate is set.
+/// Note that not all biostates are guaranteed to be one of these - and in fact, many are not
+/// IMPORTANT NOTE: All keys are stored as text and must be converted via text2num
+GLOBAL_LIST_INIT(bio_state_anatomy, list(
+ "[BIO_WIRED]" = ANATOMY_EXTERIOR,
+ "[BIO_METAL]" = ANATOMY_INTERIOR,
+ "[BIO_FLESH]" = ANATOMY_EXTERIOR,
+ "[BIO_BONE]" = ANATOMY_INTERIOR,
+))
+
+// Wound series
+// A "wound series" is just a family of wounds that logically follow eachother
+// Multiple wounds in a single series cannot be on a limb - the highest severity will always be prioritized, and lower ones will be skipped
+
+/// T1-T3 Bleeding slash wounds. Requires flesh. Can cause bleeding, but doesn't require it. From: slash.dm
+#define WOUND_SERIES_FLESH_SLASH_BLEED "wound_series_flesh_slash_bled"
+/// T1-T3 Basic blunt wounds. T1 requires jointed, but 2-3 require bone. From: bone.dm
+#define WOUND_SERIES_BONE_BLUNT_BASIC "wound_series_bone_blunt_basic"
+/// T1-T3 Basic burn wounds. Requires flesh. From: burns.dm
+#define WOUND_SERIES_FLESH_BURN_BASIC "wound_series_flesh_burn_basic"
+/// T1-T3 Bleeding puncture wounds. Requires flesh. Can cause bleeding, but doesn't require it. From: pierce.dm
+#define WOUND_SERIES_FLESH_PUNCTURE_BLEED "wound_series_flesh_puncture_bleed"
+/// Generic loss wounds. See loss.dm
+#define WOUND_SERIES_LOSS_BASIC "wound_series_loss_basic"
+
+/// A assoc list of (wound typepath -> wound_pregen_data instance). Every wound should have a pregen data.
+GLOBAL_LIST_INIT_TYPED(all_wound_pregen_data, /datum/wound_pregen_data, generate_wound_static_data())
+
+/// Constructs [GLOB.all_wound_pregen_data] by iterating through a typecache of pregen data, ignoring abstract types, and instantiating the rest.
+/proc/generate_wound_static_data()
+ RETURN_TYPE(/list/datum/wound_pregen_data)
+
+ var/list/datum/wound_pregen_data/all_pregen_data = list()
+
+ for (var/datum/wound_pregen_data/iterated_path as anything in typecacheof(path = /datum/wound_pregen_data, ignore_root_path = TRUE))
+ if (initial(iterated_path.abstract))
+ continue
+
+ if (!isnull(all_pregen_data[initial(iterated_path.wound_path_to_generate)]))
+ stack_trace("pre-existing pregen data for [initial(iterated_path.wound_path_to_generate)] when [iterated_path] was being considered: [all_pregen_data[initial(iterated_path.wound_path_to_generate)]]. \
+ this is definitely a bug, and is probably because one of the two pregen data have the wrong wound typepath defined. [iterated_path] will not be instantiated")
+
+ continue
+
+ var/datum/wound_pregen_data/pregen_data = new iterated_path
+ all_pregen_data[pregen_data.wound_path_to_generate] = pregen_data
+
+ return all_pregen_data
+
+// A wound series "collection" is merely a way for us to track what is in what series, and what their types are.
+// Without this, we have no centralized way to determine what type is in what series outside of iterating over every pregen data.
+
+/// A branching assoc list of (series -> list(severity -> list(typepath -> weight))). Allows you to say "I want a generic slash wound",
+/// then "Of severity 2", and get a wound of that description - via get_corresponding_wound_type()
+/// Series: A generic wound_series, such as WOUND_SERIES_BONE_BLUNT_BASIC
+/// Severity: Any wounds held within this will be of this severity.
+/// Typepath, Weight: Merely a pairing of a given typepath to its weight, held for convenience in pickweight.
+GLOBAL_LIST_INIT(wound_series_collections, generate_wound_series_collection())
+
+// Series -> severity -> type -> weight
+/// Generates [wound_series_collections] by iterating through all pregen_data. Refer to the mentioned list for documentation
+/proc/generate_wound_series_collection()
+ RETURN_TYPE(/list/datum/wound)
+
+ var/list/datum/wound/wound_collection = list()
-// every single type of wound that can be rolled naturally, in case you need to pull a random one
-GLOBAL_LIST_INIT(global_all_wound_types, list(/datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/moderate,
- /datum/wound/slash/critical, /datum/wound/slash/severe, /datum/wound/slash/moderate,
- /datum/wound/pierce/critical, /datum/wound/pierce/severe, /datum/wound/pierce/moderate,
- /datum/wound/burn/critical, /datum/wound/burn/severe, /datum/wound/burn/moderate))
+ for (var/datum/wound/wound_typepath as anything in typecacheof(/datum/wound, FALSE, TRUE))
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[wound_typepath]
+ if (!pregen_data)
+ continue
+ if (pregen_data.abstract)
+ stack_trace("somehow, a abstract wound_pregen_data instance ([pregen_data.type]) was instantiated and made it to generate_wound_series_collection()! \
+ i literally have no idea how! please fix this!")
+ continue
+
+ var/series = pregen_data.wound_series
+ var/list/datum/wound/series_list = wound_collection[series]
+ if (isnull(series_list))
+ wound_collection[series] = list()
+ series_list = wound_collection[series]
+
+ var/severity = "[(initial(wound_typepath.severity))]"
+ var/list/datum/wound/severity_list = series_list[severity]
+ if (isnull(severity_list))
+ series_list[severity] = list()
+ severity_list = series_list[severity]
+
+ severity_list[wound_typepath] = pregen_data.weight
+
+ return wound_collection
+
+/// A branching assoc list of (wounding_type -> list(wound_series)).
+/// Allows for determining of which wound series are caused by what.
+GLOBAL_LIST_INIT(wounding_types_to_series, list(
+ WOUND_BLUNT = list(
+ WOUND_SERIES_BONE_BLUNT_BASIC
+ ),
+ WOUND_SLASH = list(
+ WOUND_SERIES_FLESH_SLASH_BLEED,
+ ),
+ WOUND_BURN = list(
+ WOUND_SERIES_FLESH_BURN_BASIC,
+ ),
+ WOUND_PUNCTURE = list(
+ WOUND_SERIES_FLESH_PUNCTURE_BLEED
+ ),
+))
+
+/// Used in get_corresponding_wound_type(): Will pick the highest severity wound out of severity_min and severity_max
+#define WOUND_PICK_HIGHEST_SEVERITY 1
+/// Used in get_corresponding_wound_type(): Will pick the lowest severity wound out of severity_min and severity_max
+#define WOUND_PICK_LOWEST_SEVERITY 2
+
+/**
+ * Searches through all wounds for any of proper type, series, and biostate, and then returns a single one via pickweight.
+ * Is able to discern between, say, a flesh slash wound, and a metallic slash wound, and will return the respective one for the provided limb.
+ *
+ * The severity_max and severity_pick_mode args mostly exist in case you want a wound in a series that may not have your ideal severity wound, as it lets you
+ * essentially set a "fallback", where if your ideal wound doesnt exist, it'll still return something, trying to get closest to your ideal severity.
+ *
+ * Generally speaking, if you want a critical/severe/moderate wound, you should set severity_min to WOUND_SEVERITY_MODERATE, severity_max to your ideal wound,
+ * and severity_pick_mode to WOUND_PICK_HIGHEST_SEVERITY - UNLESS you for some reason want the LOWEST severity, in which case you should set
+ * severity_max to the highest wound you're willing to tolerate, and severity_pick_mode to WOUND_PICK_LOWEST_SEVERITY.
+ *
+ * Args:
+ * * list/wounding_types: A list of wounding_types. Only wounds that accept these wound types will be considered.
+ * * obj/item/bodypart/part: The limb we are considering. Extremely important for biostates.
+ * * severity_min: The minimum wound severity we will search for.
+ * * severity_max = severity_min: The maximum wound severity we will search for.
+ * * severity_pick_mode = WOUND_PICK_HIGHEST_SEVERITY: The "pick mode" we will use when considering multiple wounds of acceptable severity. See the above defines.
+ * * random_roll = TRUE: If this is considered a "random" consideration. If true, only wounds that can be randomly generated will be considered.
+ * * duplicates_allowed = FALSE: If exact duplicates of a given wound on part are tolerated. Useful for simply getting a path and not instantiating.
+ * * care_about_existing_wounds = TRUE: If we iterate over wounds to see if any are above or at a given wounds severity, and disregard it if any are. Useful for simply getting a path and not instantiating.
+ *
+ * Returns:
+ * A randomly picked wound typepath meeting all the above criteria and being applicable to the part's biotype - or null if there were none.
+ */
+/proc/get_corresponding_wound_type(list/wounding_types, obj/item/bodypart/part, severity_min, severity_max = severity_min, severity_pick_mode = WOUND_PICK_HIGHEST_SEVERITY, random_roll = TRUE, duplicates_allowed = FALSE, care_about_existing_wounds = TRUE)
+ RETURN_TYPE(/datum/wound) // note that just because its set to return this doesnt mean its non-nullable
+
+ var/list/wounding_type_list = list()
+ for (var/wounding_type as anything in wounding_types)
+ wounding_type_list += GLOB.wounding_types_to_series[wounding_type]
+ if (!length(wounding_type_list))
+ return null
+
+ var/list/datum/wound/paths_to_pick_from = list()
+ for (var/series as anything in shuffle(wounding_type_list))
+ var/list/severity_list = GLOB.wound_series_collections[series]
+ if (!length(severity_list))
+ continue
+
+ var/picked_severity
+ for (var/severity_text as anything in shuffle(GLOB.wound_severities_chronological))
+ var/severity = text2num(severity_text)
+ if (severity > severity_min || severity < severity_max)
+ continue
+
+ if (isnull(picked_severity) || ((severity_pick_mode == WOUND_PICK_HIGHEST_SEVERITY && severity > picked_severity) || (severity_pick_mode == WOUND_PICK_LOWEST_SEVERITY && severity < picked_severity)))
+ picked_severity = severity
+
+ var/list/wound_typepaths = severity_list["[picked_severity]"]
+ if (!length(wound_typepaths))
+ continue
+
+ for (var/datum/wound/iterated_path as anything in wound_typepaths)
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[iterated_path]
+ if (pregen_data.can_be_applied_to(part, wounding_types, random_roll = random_roll, duplicates_allowed = duplicates_allowed, care_about_existing_wounds = care_about_existing_wounds))
+ paths_to_pick_from[iterated_path] = wound_typepaths[iterated_path]
+
+ return pick_weight(paths_to_pick_from) // we found our winners!
+
+/// Assoc list of biotype -> ideal scar file to be used and grab stuff from.
+GLOBAL_LIST_INIT(biotypes_to_scar_file, list(
+ "[BIO_FLESH]" = FLESH_SCAR_FILE,
+ "[BIO_BONE]" = BONE_SCAR_FILE
+))
// ~burn wound infection defines
// Thresholds for infection for burn wounds, once infestation hits each threshold, things get steadily worse
@@ -87,40 +306,25 @@ GLOBAL_LIST_INIT(global_all_wound_types, list(/datum/wound/blunt/critical, /datu
// ~mangling defines
-// With the wounds pt. 2 update, general dismemberment now requires 2 things for a limb to be dismemberable (bone only creatures just need the second):
-// 1. Flesh is mangled: A critical slash or pierce wound on that limb
-// 2. Bone is mangled: At least a severe bone wound on that limb
-// see [/obj/item/bodypart/proc/get_mangled_state] for more information
+// With the wounds pt. 2 update, general dismemberment now requires 2 things for a limb to be dismemberable (exterior/bone only creatures just need the second):
+// 1. Exterior is mangled: A critical slash or pierce wound on that limb
+// 2. Interior is mangled: At least a severe bone wound on that limb
+// Lack of exterior or interior count as mangled exterior/interior respectively
+// see [/obj/item/bodypart/proc/get_mangled_state] for more information, as well as GLOB.bio_state_anatomy
#define BODYPART_MANGLED_NONE NONE
-#define BODYPART_MANGLED_BONE (1<<0)
-#define BODYPART_MANGLED_FLESH (1<<1)
-#define BODYPART_MANGLED_BOTH (BODYPART_MANGLED_BONE | BODYPART_MANGLED_FLESH)
-
-
-// ~biology defines
-// What kind of biology a limb has, and what wounds it can suffer
-/// golems and androids, cannot suffer any wounds
-#define BIO_INORGANIC NONE
-/// skeletons and plasmemes, can only suffer bone wounds, only needs mangled bone to be able to dismember
-#define BIO_BONE (1<<0)
-/// nothing right now, maybe slimepeople in the future, can only suffer slashing, piercing, and burn wounds
-#define BIO_FLESH (1<<1)
-/// standard humanoids, can suffer all wounds, needs mangled bone and flesh to dismember. conveniently, what you get when you combine BIO_BONE and BIO_FLESH
-#define BIO_FLESH_BONE (BIO_BONE | BIO_FLESH)
-
+#define BODYPART_MANGLED_INTERIOR (1<<0)
+#define BODYPART_MANGLED_EXTERIOR (1<<1)
+#define BODYPART_MANGLED_BOTH (BODYPART_MANGLED_INTERIOR | BODYPART_MANGLED_EXTERIOR)
// ~wound flag defines
-/// If this wound requires having the BIO_FLESH biological_state on the limb
-#define FLESH_WOUND (1<<0)
-/// If this wound requires having the BIO_BONE biological_state on the limb
-#define BONE_WOUND (1<<1)
-/// If having this wound counts as mangled flesh for dismemberment
-#define MANGLES_FLESH (1<<2)
-/// If having this wound counts as mangled bone for dismemberment
-#define MANGLES_BONE (1<<3)
+/// If having this wound counts as mangled exterior for dismemberment
+#define MANGLES_EXTERIOR (1<<0)
+/// If having this wound counts as mangled interior for dismemberment
+#define MANGLES_INTERIOR (1<<1)
/// If this wound marks the limb as being allowed to have gauze applied
-#define ACCEPTS_GAUZE (1<<4)
-
+#define ACCEPTS_GAUZE (1<<2)
+/// If this wound allows the victim to grasp it
+#define CAN_BE_GRASPED (1<<3)
// ~scar persistence defines
// The following are the order placements for persistent scar save formats
@@ -138,11 +342,13 @@ GLOBAL_LIST_INIT(global_all_wound_types, list(/datum/wound/blunt/critical, /datu
#define SCAR_SAVE_BIOLOGY 6
/// Which character slot this was saved to
#define SCAR_SAVE_CHAR_SLOT 7
+/// if the scar will check for any or all biostates on the limb (defaults to FALSE, so all)
+#define SCAR_SAVE_CHECK_ANY_BIO 8
///how many fields we save for each scar (so the number of above fields)
-#define SCAR_SAVE_LENGTH 7
+#define SCAR_SAVE_LENGTH 8
/// saved scars with a version lower than this will be discarded, increment when you update the persistent scarring format in a way that invalidates previous saved scars (new fields, reordering, etc)
-#define SCAR_CURRENT_VERSION 3
+#define SCAR_CURRENT_VERSION 4
/// how many scar slots, per character slot, we have to cycle through for persistent scarring, if enabled in character prefs
#define PERSISTENT_SCAR_SLOTS 3
diff --git a/code/__DEFINES/~monkestation/antagonists.dm b/code/__DEFINES/~monkestation/antagonists.dm
index a54245a1447d..8ec0def3b22b 100644
--- a/code/__DEFINES/~monkestation/antagonists.dm
+++ b/code/__DEFINES/~monkestation/antagonists.dm
@@ -14,6 +14,6 @@
/// maximum amount of cogscarabs the clock cult can have
#define MAXIMUM_COGSCARABS 9
/// is something a cogscarab
-#define iscogscarab(checked) (istype(checked, /mob/living/simple_animal/drone/cogscarab))
+#define iscogscarab(checked) (istype(checked, /mob/living/basic/drone/cogscarab))
/// is something an eminence
#define iseminence(checked) (istype(checked, /mob/living/eminence))
diff --git a/code/__HELPERS/bodyparts.dm b/code/__HELPERS/bodyparts.dm
index 39df8e37e5ff..6a76ef50870d 100644
--- a/code/__HELPERS/bodyparts.dm
+++ b/code/__HELPERS/bodyparts.dm
@@ -1 +1,3 @@
#define IS_ORGANIC_LIMB(limb) (limb.bodytype & BODYTYPE_ORGANIC)
+/// Helper to figure out if a limb is robotic
+#define IS_ROBOTIC_LIMB(limb) (limb.bodytype & BODYTYPE_ROBOTIC)
diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
index fdf997a0bc54..e4538c9731ec 100644
--- a/code/__HELPERS/chat.dm
+++ b/code/__HELPERS/chat.dm
@@ -76,3 +76,19 @@ In TGS3 it will always be sent to all connected designated game chats.
/// Handles text formatting for item use hints in examine text
#define EXAMINE_HINT(text) ("" + text + "")
+
+/// Sends a message to all dead and observing players, if a source is provided a follow link will be attached.
+/proc/send_to_observers(message, source)
+ var/list/all_observers = GLOB.dead_player_list + GLOB.current_observers_list
+ for(var/mob/observer as anything in all_observers)
+ if (isnull(source))
+ to_chat(observer, "[message]")
+ continue
+ var/link = FOLLOW_LINK(observer, source)
+ to_chat(observer, "[link] [message]")
+
+/// Sends a message to everyone within the list, as well as all observers.
+/proc/relay_to_list_and_observers(message, list/mob_list, source)
+ for(var/mob/creature as anything in mob_list)
+ to_chat(creature, message)
+ send_to_observers(message, source)
diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm
index e7af8e0dcd7c..0eb8457a46cd 100644
--- a/code/__HELPERS/cmp.dm
+++ b/code/__HELPERS/cmp.dm
@@ -170,3 +170,11 @@
/// Orders heretic knowledge by priority
/proc/cmp_heretic_knowledge(datum/heretic_knowledge/knowledge_a, datum/heretic_knowledge/knowledge_b)
return initial(knowledge_b.priority) - initial(knowledge_a.priority)
+
+/// Passed a list of assoc lists, sorts them by the list's "name" keys.
+/proc/cmp_assoc_list_name(list/A, list/B)
+ return sorttext(B["name"], A["name"])
+
+/// Orders mobs by health
+/proc/cmp_mob_health(mob/living/mob_a, mob/living/mob_b)
+ return mob_b.health - mob_a.health
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 3cebfa315c98..adabbeee3ce4 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -1478,3 +1478,13 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects)
if(scream)
stack_trace("Icon Lookup for state: [state] in file [file] failed.")
return FALSE
+
+/// Cache of the width and height of icon files, to avoid repeating the same expensive operation
+GLOBAL_LIST_EMPTY(icon_dimensions)
+
+/// Returns a list containing the width and height of an icon file
+/proc/get_icon_dimensions(icon_path)
+ if (isnull(GLOB.icon_dimensions[icon_path]))
+ var/icon/my_icon = icon(icon_path)
+ GLOB.icon_dimensions[icon_path] = list("width" = my_icon.Width(), "height" = my_icon.Height())
+ return GLOB.icon_dimensions[icon_path]
diff --git a/code/__HELPERS/pronouns.dm b/code/__HELPERS/pronouns.dm
index 6328591471a5..1265a8aed204 100644
--- a/code/__HELPERS/pronouns.dm
+++ b/code/__HELPERS/pronouns.dm
@@ -15,6 +15,9 @@
if(capitalized)
. = capitalize(.)
+/datum/proc/p_Their(temp_gender)
+ return capitalize(p_their(temp_gender))
+
/datum/proc/p_them(capitalized, temp_gender)
. = "it"
if(capitalized)
@@ -38,6 +41,9 @@
/datum/proc/p_theyre(capitalized, temp_gender)
. = p_they(capitalized, temp_gender) + "'" + copytext_char(p_are(temp_gender), 2)
+/datum/proc/p_Theyre(temp_gender)
+ return p_They(temp_gender) + "'" + copytext_char(p_are(temp_gender), 2)
+
/datum/proc/p_s(temp_gender) //is this a descriptive proc name, or what?
. = "s"
diff --git a/code/__HELPERS/~monkestation-helpers/icon_smoothing.dm b/code/__HELPERS/~monkestation-helpers/icon_smoothing.dm
index 27cfac8570a1..3e2668c6d177 100644
--- a/code/__HELPERS/~monkestation-helpers/icon_smoothing.dm
+++ b/code/__HELPERS/~monkestation-helpers/icon_smoothing.dm
@@ -50,3 +50,26 @@
if(length(overlays_adapters))
add_overlay(overlays_adapters)
+
+GLOBAL_LIST_EMPTY(string_numbers_lists)
+
+/**
+ * Caches lists of numeric values.
+ */
+/datum/proc/string_numbers_list(list/values)
+ //Just to to be extra-safe. If you try to shove in text or paths, you deserve the runtime errors.
+ var/list/sum = 0
+ for(var/number in values)
+ sum += number
+
+ var/string_id = values.Join("-")
+
+ . = GLOB.string_numbers_lists[string_id]
+
+ if(.)
+ return .
+
+ return GLOB.string_numbers_lists[string_id] = values
+
+/datum/proc/p_They(temp_gender)
+ return capitalize(p_they(temp_gender))
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 6e5ac2fef5b5..26755614e46f 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -12,6 +12,36 @@ GLOBAL_LIST_INIT(dangerous_turfs, typecacheof(list(
/turf/open/space,
/turf/open/openspace)))
+/// List of types of abstract mob which shouldn't usually exist in the world on its own if we're spawning random mobs
+GLOBAL_LIST_INIT(abstract_mob_types, list(
+ /mob/living/basic/blob_minion,
+ /mob/living/basic/construct,
+ /mob/living/basic/guardian,
+ /mob/living/basic/heretic_summon,
+ /mob/living/basic/mining,
+ /mob/living/basic/pet,
+ /mob/living/basic,
+ /mob/living/basic/spider,
+ /mob/living/carbon/alien/adult,
+ /mob/living/carbon/alien,
+ /mob/living/carbon/human/consistent,
+ /mob/living/carbon/human/dummy/consistent,
+ /mob/living/carbon/human/dummy,
+ /mob/living/carbon/human/species,
+ /mob/living/carbon,
+ /mob/living/silicon,
+ /mob/living/simple_animal/bot,
+ /mob/living/simple_animal/hostile/asteroid/elite,
+ /mob/living/simple_animal/hostile/asteroid,
+ /mob/living/simple_animal/hostile/megafauna,
+ /mob/living/simple_animal/hostile/mimic, // Cannot exist if spawned without being passed an item reference
+ /mob/living/simple_animal/hostile/retaliate,
+ /mob/living/simple_animal/hostile,
+ /mob/living/simple_animal/pet,
+ /mob/living/simple_animal/soulscythe, // As mimic, can't exist if spawned outside an item
+ /mob/living/simple_animal,
+))
+
//Since it didn't really belong in any other category, I'm putting this here
//This is for procs to replace all the goddamn 'in world's that are chilling around the code
@@ -53,6 +83,12 @@ GLOBAL_LIST_EMPTY(current_living_antags)
/// All observers with clients that joined as observers.
GLOBAL_LIST_EMPTY(current_observers_list)
+/// All living mobs which can hear blob telepathy
+GLOBAL_LIST_EMPTY(blob_telepathy_mobs)
+
+/// All "living" (because revenants are in between mortal planes or whatever) mobs that can hear revenants
+GLOBAL_LIST_EMPTY(revenant_relay_mobs)
+
///underages who have been reported to security for trying to buy things they shouldn't, so they can't spam
GLOBAL_LIST_EMPTY(narcd_underages)
diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm
index ded99d194f3a..d0091684e2d4 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -29,6 +29,10 @@ GLOBAL_LIST_INIT(simian_last_names, world.file2list("monkestation/strings/names/
GLOBAL_LIST_INIT(simian_names_female, world.file2list("monkestation/strings/names/simian_female_first.txt"))
GLOBAL_LIST_INIT(simian_names_male, world.file2list("monkestation/strings/names/simian_male_first.txt"))
GLOBAL_LIST_INIT(cyberauth_names, world.file2list("strings/names/cyberauth.txt"))
+GLOBAL_LIST_INIT(syndicate_monkey_names, world.file2list("strings/names/syndicate_monkey.txt"))
+GLOBAL_LIST_INIT(guardian_first_names, world.file2list("strings/names/guardian_descriptions.txt"))
+GLOBAL_LIST_INIT(guardian_tech_surnames, world.file2list("strings/names/guardian_gamepieces.txt"))
+GLOBAL_LIST_INIT(guardian_fantasy_surnames, world.file2list("strings/names/guardian_tarot.txt"))
GLOBAL_LIST_INIT(verbs, world.file2list("strings/names/verbs.txt"))
GLOBAL_LIST_INIT(ing_verbs, world.file2list("strings/names/ing_verbs.txt"))
diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm
index 6044f54587f0..c3fbb720650f 100644
--- a/code/_globalvars/lists/poll_ignore.dm
+++ b/code/_globalvars/lists/poll_ignore.dm
@@ -1,36 +1,69 @@
//Each lists stores ckeys for "Never for this round" option category
-#define POLL_IGNORE_SENTIENCE_POTION "sentience_potion"
-#define POLL_IGNORE_POSSESSED_BLADE "possessed_blade"
+#define POLL_IGNORE_ACADEMY_WIZARD "academy_wizard"
#define POLL_IGNORE_ALIEN_LARVA "alien_larva"
-#define POLL_IGNORE_SYNDICATE "syndicate"
-#define POLL_IGNORE_HOLOPARASITE "holoparasite"
-#define POLL_IGNORE_POSIBRAIN "posibrain"
-#define POLL_IGNORE_SPECTRAL_BLADE "spectral_blade"
-#define POLL_IGNORE_CONSTRUCT "construct"
-#define POLL_IGNORE_SPIDER "spider"
+#define POLL_IGNORE_ASH_SPIRIT "ash_spirit"
#define POLL_IGNORE_ASHWALKER "ashwalker"
-#define POLL_IGNORE_GOLEM "golem"
+#define POLL_IGNORE_BLOB "blob"
+#define POLL_IGNORE_BOTS "bots"
+#define POLL_IGNORE_CARGORILLA "cargorilla"
+#define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support"
+#define POLL_IGNORE_CONSTRUCT "construct"
#define POLL_IGNORE_DRONE "drone"
+#define POLL_IGNORE_FIRE_SHARK "fire_shark"
#define POLL_IGNORE_FUGITIVE "fugitive"
-#define POLL_IGNORE_DEFECTIVECLONE "defective_clone"
-#define POLL_IGNORE_PYROSLIME "slime"
-#define POLL_IGNORE_SHADE "shade"
+#define POLL_IGNORE_GOLEM "golem"
+#define POLL_IGNORE_HERETIC_MONSTER "heretic_monster"
+#define POLL_IGNORE_HOLOPARASITE "holoparasite"
#define POLL_IGNORE_IMAGINARYFRIEND "imaginary_friend"
-#define POLL_IGNORE_SPLITPERSONALITY "split_personality"
-#define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support"
-#define POLL_IGNORE_ACADEMY_WIZARD "academy_wizard"
+#define POLL_IGNORE_LAVALAND_ELITE "lavaland_elite"
+#define POLL_IGNORE_MAID_IN_MIRROR "maid_in_mirror"
+#define POLL_IGNORE_MONKEY_HELMET "mind_magnified_monkey"
#define POLL_IGNORE_PAI "pai"
-#define POLL_IGNORE_VENUSHUMANTRAP "venus_human_trap"
+#define POLL_IGNORE_POSIBRAIN "posibrain"
+#define POLL_IGNORE_POSSESSED_BLADE "possessed_blade"
+#define POLL_IGNORE_PYROSLIME "slime"
+#define POLL_IGNORE_RAW_PROPHET "raw_prophet"
#define POLL_IGNORE_REGAL_RAT "regal_rat"
-#define POLL_IGNORE_CARGORILLA "cargorilla"
-#define POLL_IGNORE_MONKEY_HELMET "mind_magnified_monkey"
-#define POLL_IGNORE_LAVALAND_ELITE "lavaland_elite"
-#define POLL_IGNORE_SHUTTLE_DENIZENS "shuttle_denizens"
-#define POLL_IGNORE_BOTS "bots"
+#define POLL_IGNORE_RUST_SPIRIT "rust_spirit"
+#define POLL_IGNORE_SENTIENCE_POTION "sentience_potion"
+#define POLL_IGNORE_SPIDER "spider"
+#define POLL_IGNORE_SHADE "shade"
+#define POLL_IGNORE_SYNDICATE "syndicate"
+#define POLL_IGNORE_SPLITPERSONALITY "splitpersonality"
+#define POLL_IGNORE_VENUSHUMANTRAP "venus"
+#define POLL_IGNORE_SPECTRAL_BLADE "spectralblade"
+#define POLL_IGNORE_SHUTTLE_DENIZENS "shuttledenizen"
+#define POLL_IGNORE_DEFECTIVECLONE "defectiveclone"
GLOBAL_LIST_INIT(poll_ignore_desc, list(
+ POLL_IGNORE_ACADEMY_WIZARD = "Academy Wizard Defender",
+ POLL_IGNORE_ALIEN_LARVA = "Xenomorph larva",
+ POLL_IGNORE_ASH_SPIRIT = "Ash Spirit",
+ POLL_IGNORE_ASHWALKER = "Ashwalker eggs",
+ POLL_IGNORE_BLOB = "Blob spores",
+ POLL_IGNORE_BOTS = "Bots",
+ POLL_IGNORE_CARGORILLA = "Cargorilla",
+ POLL_IGNORE_CONTRACTOR_SUPPORT = "Contractor Support Unit",
+ POLL_IGNORE_CONSTRUCT = "Construct",
+ POLL_IGNORE_DRONE = "Drone shells",
+ POLL_IGNORE_FIRE_SHARK = "Fire Shark",
+ POLL_IGNORE_FUGITIVE = "Fugitive Hunter",
+ POLL_IGNORE_GOLEM = "Golems",
+ POLL_IGNORE_HERETIC_MONSTER = "Heretic Monster",
+ POLL_IGNORE_HOLOPARASITE = "Holoparasite",
+ POLL_IGNORE_IMAGINARYFRIEND = "Imaginary Friend",
+ POLL_IGNORE_LAVALAND_ELITE = "Lavaland elite",
+ POLL_IGNORE_MAID_IN_MIRROR = "Maid in the Mirror",
+ POLL_IGNORE_MONKEY_HELMET = "Mind magnified monkey",
+ POLL_IGNORE_PAI = JOB_PERSONAL_AI,
+ POLL_IGNORE_POSIBRAIN = "Positronic brain",
+ POLL_IGNORE_POSSESSED_BLADE = "Possessed blade",
+ POLL_IGNORE_PYROSLIME = "Slime",
+ POLL_IGNORE_RAW_PROPHET = "Raw Prophet",
+ POLL_IGNORE_RUST_SPIRIT = "Rust Spirit",
+ POLL_IGNORE_REGAL_RAT = "Regal rat",
POLL_IGNORE_SENTIENCE_POTION = "Sentience potion",
POLL_IGNORE_POSSESSED_BLADE = "Possessed blade",
POLL_IGNORE_ALIEN_LARVA = "Xenomorph larva",
@@ -44,7 +77,7 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list(
POLL_IGNORE_GOLEM = "Golems",
POLL_IGNORE_DRONE = "Drone shells",
POLL_IGNORE_FUGITIVE = "Fugitive Hunter",
- POLL_IGNORE_DEFECTIVECLONE = "Defective clone",
+ POLL_IGNORE_DEFECTIVECLONE = "Defective clone",
POLL_IGNORE_PYROSLIME = "Slime",
POLL_IGNORE_SHADE = "Shade",
POLL_IGNORE_IMAGINARYFRIEND = "Imaginary Friend",
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index 9671ecaa57e9..9819b3499a42 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -51,33 +51,57 @@ GLOBAL_LIST_INIT(phobia_regexes, list(
))
GLOBAL_LIST_INIT(phobia_mobs, list(
- "spiders" = typecacheof(list(/mob/living/basic/spider/giant)),
- "security" = typecacheof(list(/mob/living/simple_animal/bot/secbot)),
+ "aliens" = typecacheof(list(
+ /mob/living/carbon/alien,
+ /mob/living/simple_animal/slime,
+ )),
+ "anime" = typecacheof(list(/mob/living/basic/guardian)),
+ "birds" = typecacheof(list(
+ /mob/living/basic/chick,
+ /mob/living/basic/chicken,
+ /mob/living/basic/parrot,
+ /mob/living/basic/pet/penguin,
+ )),
+ "conspiracies" = typecacheof(list(
+ /mob/living/basic/drone,
+ /mob/living/basic/pet/penguin,
+ /mob/living/simple_animal/bot/secbot,
+ )),
+ "doctors" = typecacheof(list(/mob/living/simple_animal/bot/medbot)),
+ "heresy" = typecacheof(list(
+ /mob/living/basic/heretic_summon,
+ )),
+ "insects" = typecacheof(list(
+ /mob/living/basic/cockroach,
+ /mob/living/basic/bee,
+ )),
"lizards" = typecacheof(list(/mob/living/basic/lizard)),
- "skeletons" = typecacheof(list(/mob/living/simple_animal/hostile/skeleton)),
- "snakes" = typecacheof(list(/mob/living/simple_animal/hostile/retaliate/snake)),
+ "skeletons" = typecacheof(list(/mob/living/basic/skeleton)),
"robots" = typecacheof(list(
+ /mob/living/basic/drone,
/mob/living/silicon/ai,
/mob/living/silicon/robot,
/mob/living/simple_animal/bot,
- /mob/living/simple_animal/drone,
)),
- "doctors" = typecacheof(list(/mob/living/simple_animal/bot/medbot)),
+ "security" = typecacheof(list(/mob/living/simple_animal/bot/secbot)),
+ "spiders" = typecacheof(list(/mob/living/basic/spider/giant)),
+ "skeletons" = typecacheof(list(/mob/living/basic/skeleton)),
+ "snakes" = typecacheof(list(/mob/living/basic/snake)),
"the supernatural" = typecacheof(list(
/mob/dead/observer,
/mob/living/basic/bat,
+ /mob/living/basic/construct,
/mob/living/basic/demon,
/mob/living/basic/faithless,
/mob/living/basic/ghost,
+ /mob/living/basic/heretic_summon,
+ /mob/living/basic/revenant,
+ /mob/living/basic/shade,
+ /mob/living/basic/skeleton,
+ /mob/living/basic/wizard,
/mob/living/simple_animal/bot/mulebot/paranormal,
- /mob/living/simple_animal/hostile/construct,
/mob/living/simple_animal/hostile/dark_wizard,
- /mob/living/simple_animal/hostile/heretic_summon,
- /mob/living/simple_animal/hostile/skeleton,
- /mob/living/simple_animal/hostile/wizard,
/mob/living/simple_animal/hostile/zombie,
- /mob/living/simple_animal/revenant,
- /mob/living/simple_animal/shade,
)),
"aliens" = typecacheof(list(
/mob/living/carbon/alien,
@@ -85,16 +109,16 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
)),
"conspiracies" = typecacheof(list(
/mob/living/simple_animal/bot/secbot,
- /mob/living/simple_animal/drone,
+ /mob/living/basic/drone,
/mob/living/basic/pet/penguin,
)),
"birds" = typecacheof(list(
/mob/living/basic/chick,
/mob/living/basic/chicken,
- /mob/living/simple_animal/parrot,
+ /mob/living/basic/parrot,
/mob/living/basic/pet/penguin,
)),
- "anime" = typecacheof(list(/mob/living/simple_animal/hostile/guardian)),
+ "anime" = typecacheof(list(/mob/living/basic/guardian)),
"insects" = typecacheof(list(
/mob/living/basic/cockroach,
/mob/living/basic/bee,
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 55326d5633ef..063d7f7cbf2a 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -80,6 +80,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_BLOOD_DEFICIENCY" = TRAIT_BLOOD_DEFICIENCY,
"TRAIT_JOLLY" = TRAIT_JOLLY,
"TRAIT_NO_GLIDE" = TRAIT_NO_GLIDE,
+ "TRAIT_NO_FLOATING_ANIM" = TRAIT_NO_FLOATING_ANIM,
"TRAIT_NOCRITDAMAGE" = TRAIT_NOCRITDAMAGE,
"TRAIT_NO_SLIP_WATER" = TRAIT_NO_SLIP_WATER,
"TRAIT_NO_SLIP_ICE" = TRAIT_NO_SLIP_ICE,
@@ -96,6 +97,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_PARALYSIS_R_ARM" = TRAIT_PARALYSIS_R_ARM,
"TRAIT_PARALYSIS_L_LEG" = TRAIT_PARALYSIS_L_LEG,
"TRAIT_PARALYSIS_R_LEG" = TRAIT_PARALYSIS_R_LEG,
+ "TRAIT_PARROT_PERCHED" = TRAIT_PARROT_PERCHED,
"TRAIT_CANNOT_OPEN_PRESENTS" = TRAIT_CANNOT_OPEN_PRESENTS,
"TRAIT_PRESENT_VISION" = TRAIT_PRESENT_VISION,
"TRAIT_DISK_VERIFIER" = TRAIT_DISK_VERIFIER,
@@ -194,6 +196,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_UNOBSERVANT" = TRAIT_UNOBSERVANT,
"TRAIT_TENTACLE_IMMUNE" = TRAIT_TENTACLE_IMMUNE,
"TRAIT_OVERWATCH_IMMUNE" = TRAIT_OVERWATCH_IMMUNE,
+ "TRAIT_UNDENSE" = TRAIT_UNDENSE,
+ "TRAIT_EXPANDED_FOV" = TRAIT_EXPANDED_FOV,
),
/obj/item/bodypart = list(
"TRAIT_PARALYSIS" = TRAIT_PARALYSIS,
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 31168fb1657e..364a548a131d 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -69,7 +69,7 @@
return
next_click = world.time + 1
- if(check_click_intercept(params,A) || notransform)
+ if(check_click_intercept(params,A) || HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return
var/list/modifiers = params2list(params)
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 2537133a5d86..a05d11c73d69 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -510,7 +510,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
alerttooltipstyle = "cult"
var/static/image/narnar
var/angle = 0
- var/mob/living/simple_animal/hostile/construct/Cviewer = null
+ var/mob/living/basic/construct/Cviewer
/atom/movable/screen/alert/bloodsense/Initialize(mapload)
. = ..()
@@ -615,19 +615,13 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
//GUARDIANS
-/atom/movable/screen/alert/cancharge
- name = "Charge Ready"
- desc = "You are ready to charge at a location!"
- icon_state = "guardian_charge"
- alerttooltipstyle = "parasite"
-
/atom/movable/screen/alert/canstealth
name = "Stealth Ready"
desc = "You are ready to enter stealth!"
icon_state = "guardian_canstealth"
alerttooltipstyle = "parasite"
-/atom/movable/screen/alert/instealth
+/atom/movable/screen/alert/status_effect/instealth
name = "In Stealth"
desc = "You are in stealth and your next attack will do bonus damage!"
icon_state = "guardian_instealth"
diff --git a/code/_onclick/hud/drones.dm b/code/_onclick/hud/drones.dm
index 61c006ac6fe5..4ae7b3070d97 100644
--- a/code/_onclick/hud/drones.dm
+++ b/code/_onclick/hud/drones.dm
@@ -32,19 +32,17 @@
/datum/hud/dextrous/drone/persistent_inventory_update()
if(!mymob)
return
- var/mob/living/simple_animal/drone/D = mymob
+ var/mob/living/basic/drone/drone = mymob
if(hud_shown)
- if(D.internal_storage)
- D.internal_storage.screen_loc = ui_drone_storage
- D.client.screen += D.internal_storage
- if(D.head)
- D.head.screen_loc = ui_drone_head
- D.client.screen += D.head
+ if(!isnull(drone.internal_storage))
+ drone.internal_storage.screen_loc = ui_drone_storage
+ drone.client.screen += drone.internal_storage
+ if(!isnull(drone.head))
+ drone.head.screen_loc = ui_drone_head
+ drone.client.screen += drone.head
else
- if(D.internal_storage)
- D.internal_storage.screen_loc = null
- if(D.head)
- D.head.screen_loc = null
+ drone.internal_storage?.screen_loc = null
+ drone.head?.screen_loc = null
..()
diff --git a/code/_onclick/hud/generic_dextrous.dm b/code/_onclick/hud/generic_dextrous.dm
index 9cdaea2356aa..7bb20ba10b79 100644
--- a/code/_onclick/hud/generic_dextrous.dm
+++ b/code/_onclick/hud/generic_dextrous.dm
@@ -46,7 +46,7 @@
using.hud = src
static_inventory += using
- mymob.client.clear_screen()
+ mymob.canon_client?.clear_screen()
for(var/atom/movable/screen/inventory/inv in (static_inventory + toggleable_inventory))
if(inv.slot_id)
@@ -66,10 +66,3 @@
for(var/obj/item/I in D.held_items)
I.screen_loc = null
D.client.screen -= I
-
-
-//Dextrous simple mobs can use hands!
-/mob/living/simple_animal/create_mob_hud()
- if(dextrous)
- hud_type = dextrous_hud_type
- return ..()
diff --git a/code/_onclick/hud/guardian.dm b/code/_onclick/hud/guardian.dm
index 91da0f9fdb85..cbf274cbeb05 100644
--- a/code/_onclick/hud/guardian.dm
+++ b/code/_onclick/hud/guardian.dm
@@ -1,7 +1,7 @@
/datum/hud/guardian
ui_style = 'icons/hud/guardian.dmi'
-/datum/hud/guardian/New(mob/living/simple_animal/hostile/guardian/owner)
+/datum/hud/guardian/New(mob/living/basic/guardian/owner)
..()
var/atom/movable/screen/using
@@ -41,10 +41,10 @@
using.hud = src
static_inventory += using
-/datum/hud/dextrous/guardian/New(mob/living/simple_animal/hostile/guardian/owner) //for a dextrous guardian
+/datum/hud/dextrous/guardian/New(mob/living/basic/guardian/owner) //for a dextrous guardian
..()
var/atom/movable/screen/using
- if(istype(owner, /mob/living/simple_animal/hostile/guardian/dextrous))
+ if(istype(owner, /mob/living/basic/guardian/dextrous))
var/atom/movable/screen/inventory/inv_box
inv_box = new /atom/movable/screen/inventory()
@@ -102,8 +102,8 @@
/datum/hud/dextrous/guardian/persistent_inventory_update()
if(!mymob)
return
- if(istype(mymob, /mob/living/simple_animal/hostile/guardian/dextrous))
- var/mob/living/simple_animal/hostile/guardian/dextrous/dex_guardian = mymob
+ if(istype(mymob, /mob/living/basic/guardian/dextrous))
+ var/mob/living/basic/guardian/dextrous/dex_guardian = mymob
if(hud_shown)
if(dex_guardian.internal_storage)
@@ -125,7 +125,7 @@
/atom/movable/screen/guardian/manifest/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.manifest()
@@ -136,7 +136,7 @@
/atom/movable/screen/guardian/recall/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.recall()
/atom/movable/screen/guardian/toggle_mode
@@ -146,7 +146,7 @@
/atom/movable/screen/guardian/toggle_mode/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.toggle_modes()
/atom/movable/screen/guardian/toggle_mode/inactive
@@ -169,7 +169,7 @@
/atom/movable/screen/guardian/communicate/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.communicate()
@@ -180,5 +180,5 @@
/atom/movable/screen/guardian/toggle_light/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.toggle_light()
diff --git a/code/_onclick/hud/ooze.dm b/code/_onclick/hud/ooze.dm
index d3ebe3e72885..c13123bb43bb 100644
--- a/code/_onclick/hud/ooze.dm
+++ b/code/_onclick/hud/ooze.dm
@@ -1,4 +1,4 @@
-///Hud type with targetting dol and a nutrition bar
+///Hud type with targeting dol and a nutrition bar
/datum/hud/ooze/New(mob/living/owner)
. = ..()
diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm
index 75e83a00b593..44bd2873ccdb 100644
--- a/code/_onclick/hud/radial.dm
+++ b/code/_onclick/hud/radial.dm
@@ -345,9 +345,13 @@ GLOBAL_LIST_EMPTY(radial_menus)
Choices should be a list where list keys are movables or text used for element names and return value
and list values are movables/icons/images used for element icons
*/
-/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice")
+/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE, no_repeat_close = FALSE, radial_slice_icon = "radial_slice", autopick_single_option = TRUE)
if(!user || !anchor || !length(choices))
return
+
+ if(length(choices)==1 && autopick_single_option)
+ return choices[1]
+
if(!uniqueid)
uniqueid = "defmenu_[REF(user)]_[REF(anchor)]"
diff --git a/code/_onclick/hud/screentip.dm b/code/_onclick/hud/screentip.dm
index 8f3b9d63014d..50162e6e3a8e 100644
--- a/code/_onclick/hud/screentip.dm
+++ b/code/_onclick/hud/screentip.dm
@@ -15,7 +15,7 @@
/atom/movable/screen/screentip/proc/update_view(datum/source)
SIGNAL_HANDLER
- if(!hud || !hud.mymob.client.view_size) //Might not have been initialized by now
+ if(!hud || !hud.mymob.canon_client?.view_size) //Might not have been initialized by now
return
maptext_width = view_to_pixels(hud.mymob.client.view_size.getView())[1]
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index 8c12b83daac1..62fa07070976 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -156,6 +156,17 @@
return ..() || ((obj_flags & CAN_BE_HIT) && attacking_item.attack_atom(src, user, params))
/mob/living/attackby(obj/item/attacking_item, mob/living/user, params)
+ for(var/datum/surgery/operations as anything in surgeries)
+ if(user.istate & ISTATE_HARM)
+ break
+ if(IS_IN_INVALID_SURGICAL_POSITION(src, operations))
+ continue
+ if(!(operations.surgery_flags & SURGERY_SELF_OPERABLE) && (user == src))
+ continue
+ var/list/modifiers = params2list(params)
+ if(operations.next_step(user, modifiers))
+ return TRUE
+
if(..())
return TRUE
user.changeNext_move(attacking_item.attack_speed)
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index aa08d7889fa8..c7ddad9056d3 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -245,14 +245,14 @@
Drones
*/
-/mob/living/simple_animal/drone/resolve_unarmed_attack(atom/attack_target, proximity_flag, list/modifiers)
+/mob/living/basic/drone/resolve_unarmed_attack(atom/attack_target, proximity_flag, list/modifiers)
attack_target.attack_drone(src, modifiers)
-/mob/living/simple_animal/drone/resolve_right_click_attack(atom/target, list/modifiers)
+/mob/living/basic/drone/resolve_right_click_attack(atom/target, list/modifiers)
return target.attack_drone_secondary(src, modifiers)
/// Defaults to attack_hand. Override it when you don't want drones to do same stuff as humans.
-/atom/proc/attack_drone(mob/living/simple_animal/drone/user, list/modifiers)
+/atom/proc/attack_drone(mob/living/basic/drone/user, list/modifiers)
attack_hand(user, modifiers)
/**
@@ -260,7 +260,7 @@
* Defaults to attack_hand_secondary.
* When overriding it, remember that it ought to return a SECONDARY_ATTACK_* value.
*/
-/atom/proc/attack_drone_secondary(mob/living/simple_animal/drone/user, list/modifiers)
+/atom/proc/attack_drone_secondary(mob/living/basic/drone/user, list/modifiers)
return attack_hand_secondary(user, modifiers)
/*
@@ -296,14 +296,14 @@
*/
/mob/living/simple_animal/resolve_unarmed_attack(atom/attack_target, list/modifiers)
- if(dextrous && (isitem(attack_target) || !(istate & ISTATE_HARM)))
+ if((isitem(attack_target) || !(istate & ISTATE_HARM)))
attack_target.attack_hand(src, modifiers)
update_held_items()
else
return ..()
/mob/living/simple_animal/resolve_right_click_attack(atom/target, list/modifiers)
- if(dextrous && (isitem(target) || !(istate & ISTATE_HARM)))
+ if((isitem(target) || !(istate & ISTATE_HARM)))
. = target.attack_hand_secondary(src, modifiers)
update_held_items()
else
@@ -315,10 +315,7 @@
/mob/living/simple_animal/hostile/resolve_unarmed_attack(atom/attack_target, list/modifiers)
GiveTarget(attack_target)
- if(dextrous && (isitem(attack_target) || !(istate & ISTATE_HARM)))
- return ..()
- else
- AttackingTarget(attack_target)
+ INVOKE_ASYNC(src, PROC_REF(AttackingTarget), attack_target)
#undef LIVING_UNARMED_ATTACK_BLOCKED
diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm
index 0b5d39b77380..96efadf80811 100644
--- a/code/controllers/subsystem/atoms.dm
+++ b/code/controllers/subsystem/atoms.dm
@@ -156,7 +156,8 @@ SUBSYSTEM_DEF(atoms)
BadInitializeCalls[the_type] |= BAD_INIT_DIDNT_INIT
else
SEND_SIGNAL(A, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZE)
- var/atom/movable/location = A.loc
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_ATOM_AFTER_POST_INIT, A)
+ var/atom/location = A.loc
if(location)
/// Sends a signal that the new atom `src`, has been created at `loc`
SEND_SIGNAL(location, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, A, arguments[1])
diff --git a/code/controllers/subsystem/npcpool.dm b/code/controllers/subsystem/npcpool.dm
index 34cad5d7f861..6fbfdadf6828 100644
--- a/code/controllers/subsystem/npcpool.dm
+++ b/code/controllers/subsystem/npcpool.dm
@@ -29,7 +29,7 @@ SUBSYSTEM_DEF(npcpool)
stack_trace("Found a null in simple_animals active list [SA.type]!")
continue
- if(!SA.ckey && !SA.notransform)
+ if(!SA.ckey && !HAS_TRAIT(SA, TRAIT_NO_TRANSFORM))
if(SA.stat != DEAD)
SA.handle_automated_movement()
if(SA.stat != DEAD)
diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm
deleted file mode 100644
index cf4908c92f7d..000000000000
--- a/code/controllers/subsystem/persistence.dm
+++ /dev/null
@@ -1,559 +0,0 @@
-#define FILE_RECENT_MAPS "data/RecentMaps.json"
-
-#define KEEP_ROUNDS_MAP 3
-
-SUBSYSTEM_DEF(persistence)
- name = "Persistence"
- init_order = INIT_ORDER_PERSISTENCE
- flags = SS_NO_FIRE
-
- ///instantiated wall engraving components
- var/list/wall_engravings = list()
- ///tattoo stories that we're saving.
- var/list/prison_tattoos_to_save = list()
- ///tattoo stories that have been selected for this round.
- var/list/prison_tattoos_to_use = list()
- var/list/saved_messages = list()
- var/list/saved_modes = list(1,2,3)
- var/list/saved_maps = list()
- var/list/blocked_maps = list()
- var/list/saved_trophies = list()
- var/list/picture_logging_information = list()
- var/list/obj/structure/sign/picture_frame/photo_frames
- var/list/obj/item/storage/photo_album/photo_albums
- var/rounds_since_engine_exploded = 0
-
-/datum/controller/subsystem/persistence/Initialize()
- load_poly()
- load_wall_engravings()
- load_prisoner_tattoos()
- load_trophies()
- load_recent_maps()
- load_photo_persistence()
- load_randomized_recipes()
- load_custom_outfits()
- load_delamination_counter()
-
- load_adventures()
- return SS_INIT_SUCCESS
-
-///Collects all data to persist.
-/datum/controller/subsystem/persistence/proc/collect_data()
- save_wall_engravings()
- save_prisoner_tattoos()
- collect_trophies()
- collect_maps()
- save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
- save_randomized_recipes()
- save_scars()
- save_custom_outfits()
- save_delamination_counter()
- if(GLOB.interviews)
- save_keys(GLOB.interviews.approved_ckeys)
-
-///Loads up Poly's speech buffer.
-/datum/controller/subsystem/persistence/proc/load_poly()
- for(var/mob/living/simple_animal/parrot/poly/P in GLOB.alive_mob_list)
- twitterize(P.speech_buffer, "polytalk")
- break //Who's been duping the bird?!
-
-///Loads all engravings, and places a select amount in maintenance and the prison.
-/datum/controller/subsystem/persistence/proc/load_wall_engravings()
- var/json_file = file(ENGRAVING_SAVE_FILE)
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
-
- if(json["version"] < ENGRAVING_PERSISTENCE_VERSION)
- update_wall_engravings(json)
-
- var/successfully_loaded_engravings = 0
-
- var/list/viable_turfs = get_area_turfs(/area/station/maintenance, subtypes = TRUE) + get_area_turfs(/area/station/security/prison, subtypes = TRUE)
- var/list/turfs_to_pick_from = list()
-
- for(var/turf/T as anything in viable_turfs)
- if(!isclosedturf(T))
- continue
- turfs_to_pick_from += T
-
- var/list/engraving_entries = json["entries"]
-
- if(engraving_entries.len)
- for(var/iteration in 1 to rand(MIN_PERSISTENT_ENGRAVINGS, MAX_PERSISTENT_ENGRAVINGS))
- var/engraving = engraving_entries[rand(1, engraving_entries.len)] //This means repeats will happen for now, but its something I can live with. Just make more engravings!
- if(!islist(engraving))
- stack_trace("something's wrong with the engraving data! one of the saved engravings wasn't a list!")
- continue
-
- var/turf/closed/engraved_wall = pick(turfs_to_pick_from)
-
- if(HAS_TRAIT(engraved_wall, TRAIT_NOT_ENGRAVABLE))
- continue
-
- engraved_wall.AddComponent(/datum/component/engraved, engraving["story"], FALSE, engraving["story_value"])
- successfully_loaded_engravings++
- turfs_to_pick_from -= engraved_wall
-
- log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]")
-
-///Saves all new engravings in the world.
-/datum/controller/subsystem/persistence/proc/save_wall_engravings()
- var/list/saved_data = list()
-
- saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
- saved_data["entries"] = list()
-
-
- var/json_file = file(ENGRAVING_SAVE_FILE)
- if(fexists(json_file))
- var/list/old_json = json_decode(file2text(json_file))
- if(old_json)
- saved_data["entries"] = old_json["entries"]
-
- for(var/datum/component/engraved/engraving in wall_engravings)
- if(!engraving.persistent_save)
- continue
- var/area/engraved_area = get_area(engraving.parent)
- if(!(engraved_area.area_flags & PERSISTENT_ENGRAVINGS))
- continue
- saved_data["entries"] += engraving.save_persistent()
-
- fdel(json_file)
-
- WRITE_FILE(json_file, json_encode(saved_data))
-
-///This proc can update entries if the format has changed at some point.
-/datum/controller/subsystem/persistence/proc/update_wall_engravings(json)
-
-
- for(var/engraving_entry in json["entries"])
- continue //no versioning yet
-
- //Save it to the file
- var/json_file = file(ENGRAVING_SAVE_FILE)
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(json))
-
- return json
-
-///Loads all tattoos, and select a few based on the amount of prisoner spawn positions.
-/datum/controller/subsystem/persistence/proc/load_prisoner_tattoos()
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
-
- if(json["version"] < TATTOO_PERSISTENCE_VERSION)
- update_prisoner_tattoos(json)
-
- var/datum/job/prisoner_datum = SSjob.name_occupations[JOB_PRISONER]
- if(!prisoner_datum)
- return
- var/iterations_allowed = prisoner_datum.spawn_positions
-
- var/list/entries = json["entries"]
- if(entries.len)
- for(var/index in 1 to iterations_allowed)
- prison_tattoos_to_use += list(entries[rand(1, entries.len)])
-
- log_world("Loaded [prison_tattoos_to_use.len] prison tattoos")
-
-///Saves all tattoos, so they can appear on prisoners in future rounds
-/datum/controller/subsystem/persistence/proc/save_prisoner_tattoos()
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- var/list/saved_data = list()
- var/list/entries = list()
-
- if(fexists(json_file))
- var/list/old_json = json_decode(file2text(json_file))
- if(old_json)
- entries += old_json["entries"] //Save the old if its there
-
- entries += prison_tattoos_to_save
-
- saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
- saved_data["entries"] = entries
-
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(saved_data))
-
-///This proc can update entries if the format has changed at some point.
-/datum/controller/subsystem/persistence/proc/update_prisoner_tattoos(json)
-
- for(var/tattoo_entry in json["entries"])
- continue //no versioning yet
-
- //Save it to the file
- var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(json))
-
- return json
-
-/// Loads the trophies from the source file, and places a few in trophy display cases.
-/datum/controller/subsystem/persistence/proc/load_trophies()
- var/list/raw_saved_trophies = list()
- if(fexists("data/npc_saves/TrophyItems.json"))
- var/json_file = file("data/npc_saves/TrophyItems.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- raw_saved_trophies = json["data"]
- fdel("data/npc_saves/TrophyItems.json")
- else
- var/json_file = file("data/trophy_items.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- raw_saved_trophies = json["data"]
-
- for(var/raw_json in raw_saved_trophies)
- var/datum/trophy_data/parsed_trophy_data = new
- parsed_trophy_data.load_from_json(raw_json)
- saved_trophies += parsed_trophy_data
-
- set_up_trophies()
-
-/datum/controller/subsystem/persistence/proc/save_keys(list/approved_ckeys)
- var/json_file = file("data/approved_keys.json")
- var/list/keys = list()
- if(fexists(json_file))
- fdel(json_file)
- keys = json_encode(approved_ckeys)
- WRITE_FILE(json_file, keys)
-
-///trophy data datum, for admin manipulation
-/datum/trophy_data
- ///path of the item the trophy will try to mimic, null if path_string is invalid
- var/path
- ///the message that appears under the item
- var/message
- ///the key of the one who placed the item in the trophy case
- var/placer_key
-
-/datum/trophy_data/proc/load_from_json(list/json_data)
- path = json_data["path"]
- message = json_data["message"]
- placer_key = json_data["placer_key"]
-
-/datum/trophy_data/proc/to_json()
- var/list/new_data = list()
- new_data["path"] = path
- new_data["message"] = message
- new_data["placer_key"] = placer_key
- new_data["is_valid"] = text2path(path) ? TRUE : FALSE
- return new_data
-
-/// Returns a list for the admin trophy panel.
-/datum/controller/subsystem/persistence/proc/trophy_ui_data()
- var/list/ui_data = list()
- for(var/datum/trophy_data/data in saved_trophies)
- var/list/pdata = data.to_json()
- pdata["ref"] = REF(data)
- ui_data += list(pdata)
-
- return ui_data
-
-/// Loads up the amount of times maps appeared to alter their appearance in voting and rotation.
-/datum/controller/subsystem/persistence/proc/load_recent_maps()
- var/map_sav = FILE_RECENT_MAPS
- if(!fexists(FILE_RECENT_MAPS))
- return
- var/list/json = json_decode(file2text(map_sav))
- if(!json)
- return
- saved_maps = json["data"]
-
- //Convert the mapping data to a shared blocking list, saves us doing this in several places later.
- for(var/map in config.maplist)
- var/datum/map_config/VM = config.maplist[map]
- var/run = 0
- if(VM.map_name == SSmapping.config.map_name)
- run++
- for(var/name in SSpersistence.saved_maps)
- if(VM.map_name == name)
- run++
- if(run >= 2) //If run twice in the last KEEP_ROUNDS_MAP + 1 (including current) rounds, disable map for voting and rotation.
- blocked_maps += VM.map_name
-
-/// Puts trophies into trophy cases.
-/datum/controller/subsystem/persistence/proc/set_up_trophies()
-
- var/list/valid_trophies = list()
-
- for(var/datum/trophy_data/data in saved_trophies)
-
- if(!data) //sanity for incorrect deserialization
- continue
-
- var/path = text2path(data.path)
- if(!path) //If the item no longer exist, ignore it
- continue
-
- valid_trophies += data
-
- for(var/obj/structure/displaycase/trophy/trophy_case in GLOB.trophy_cases)
- if(!valid_trophies.len)
- break
-
- if(trophy_case.showpiece)
- continue
-
- trophy_case.set_up_trophy(pick_n_take(valid_trophies))
-
-///Loads up the photo album source file.
-/datum/controller/subsystem/persistence/proc/get_photo_albums()
- var/album_path = file("data/photo_albums.json")
- if(fexists(album_path))
- return json_decode(file2text(album_path))
-
-///Loads up the photo frames source file.
-/datum/controller/subsystem/persistence/proc/get_photo_frames()
- var/frame_path = file("data/photo_frames.json")
- if(fexists(frame_path))
- return json_decode(file2text(frame_path))
-
-/// Removes the identifier of a persistent photo frame from the json.
-/datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier)
- var/frame_path = file("data/photo_frames.json")
- if(!fexists(frame_path))
- return
-
- var/frame_json = json_decode(file2text(frame_path))
- frame_json -= identifier
-
- frame_json = json_encode(frame_json)
- fdel(frame_path)
- WRITE_FILE(frame_path, frame_json)
-
-///Loads photo albums, and populates them; also loads and applies frames to picture frames.
-/datum/controller/subsystem/persistence/proc/load_photo_persistence()
- var/album_path = file("data/photo_albums.json")
- var/frame_path = file("data/photo_frames.json")
- if(fexists(album_path))
- var/list/json = json_decode(file2text(album_path))
- if(json.len)
- for(var/i in photo_albums)
- var/obj/item/storage/photo_album/A = i
- if(!A.persistence_id)
- continue
- if(json[A.persistence_id])
- A.populate_from_id_list(json[A.persistence_id])
-
- if(fexists(frame_path))
- var/list/json = json_decode(file2text(frame_path))
- if(json.len)
- for(var/i in photo_frames)
- var/obj/structure/sign/picture_frame/PF = i
- if(!PF.persistence_id)
- continue
- if(json[PF.persistence_id])
- PF.load_from_id(json[PF.persistence_id])
-
-///Saves the contents of photo albums and the picture frames.
-/datum/controller/subsystem/persistence/proc/save_photo_persistence()
- var/album_path = file("data/photo_albums.json")
- var/frame_path = file("data/photo_frames.json")
-
- var/list/frame_json = list()
- var/list/album_json = list()
-
- if(fexists(album_path))
- album_json = json_decode(file2text(album_path))
- fdel(album_path)
-
- for(var/i in photo_albums)
- var/obj/item/storage/photo_album/A = i
- if(!istype(A) || !A.persistence_id)
- continue
- var/list/L = A.get_picture_id_list()
- album_json[A.persistence_id] = L
-
- album_json = json_encode(album_json)
-
- WRITE_FILE(album_path, album_json)
-
- if(fexists(frame_path))
- frame_json = json_decode(file2text(frame_path))
- fdel(frame_path)
-
- for(var/i in photo_frames)
- var/obj/structure/sign/picture_frame/F = i
- if(!istype(F) || !F.persistence_id)
- continue
- frame_json[F.persistence_id] = F.get_photo_id()
-
- frame_json = json_encode(frame_json)
-
- WRITE_FILE(frame_path, frame_json)
-
-///Collects trophies from all existing trophy cases.
-/datum/controller/subsystem/persistence/proc/collect_trophies()
- for(var/trophy_case in GLOB.trophy_cases)
- save_trophy(trophy_case)
-
- var/json_file = file("data/trophy_items.json")
- var/list/file_data = list()
- var/list/converted_data = list()
-
- for(var/datum/trophy_data/data in saved_trophies)
- converted_data += list(data.to_json())
-
- converted_data = remove_duplicate_trophies(converted_data)
-
- file_data["data"] = converted_data
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///gets the list of json trophies, and deletes the ones with an identical path and message
-/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
- var/list/ukeys = list()
- . = list()
- for(var/trophy in trophies)
- var/tkey = "[trophy["path"]]-[trophy["message"]]"
- if(ukeys[tkey])
- continue
- else
- . += list(trophy)
- ukeys[tkey] = TRUE
-
-///If there is a trophy in the trophy case, saved it, if the trophy was not a holo trophy and has a message attached.
-/datum/controller/subsystem/persistence/proc/save_trophy(obj/structure/displaycase/trophy/trophy_case)
- if(!trophy_case.holographic_showpiece && trophy_case.showpiece && trophy_case.trophy_message)
- var/datum/trophy_data/data = new
- data.path = trophy_case.showpiece.type
- data.message = trophy_case.trophy_message
- data.placer_key = trophy_case.placer_key
- saved_trophies += data
-
-///Updates the list of the most recent maps.
-/datum/controller/subsystem/persistence/proc/collect_maps()
- if(length(saved_maps) > KEEP_ROUNDS_MAP) //Get rid of extras from old configs.
- saved_maps.Cut(KEEP_ROUNDS_MAP+1)
- var/mapstosave = min(length(saved_maps)+1, KEEP_ROUNDS_MAP)
- if(length(saved_maps) < mapstosave) //Add extras if too short, one per round.
- saved_maps += mapstosave
- for(var/i = mapstosave; i > 1; i--)
- saved_maps[i] = saved_maps[i-1]
- saved_maps[1] = SSmapping.config.map_name
- var/json_file = file(FILE_RECENT_MAPS)
- var/list/file_data = list()
- file_data["data"] = saved_maps
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///Loads all randomized recipes.
-/datum/controller/subsystem/persistence/proc/load_randomized_recipes()
- var/json_file = file("data/RandomizedChemRecipes.json")
- var/json
- if(fexists(json_file))
- json = json_decode(file2text(json_file))
-
- for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
- var/datum/chemical_reaction/randomized/R = new randomized_type
- var/loaded = FALSE
- if(R.persistent && json)
- var/list/recipe_data = json["[R.type]"]
- if(recipe_data)
- if(R.LoadOldRecipe(recipe_data) && (daysSince(R.created) <= R.persistence_period))
- loaded = TRUE
- if(!loaded) //We do not have information for whatever reason, just generate new one
- if(R.persistent)
- log_game("Resetting persistent [randomized_type] random recipe.")
- R.GenerateRecipe()
-
- if(!R.HasConflicts()) //Might want to try again if conflicts happened in the future.
- add_chemical_reaction(R)
- else
- log_game("Randomized recipe [randomized_type] resulted in conflicting recipes.")
-
-///Saves all randomized recipes.
-/datum/controller/subsystem/persistence/proc/save_randomized_recipes()
- var/json_file = file("data/RandomizedChemRecipes.json")
- var/list/file_data = list()
-
- //asert globchems done
- for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
- var/datum/chemical_reaction/randomized/R = get_chemical_reaction(randomized_type) //ew, would be nice to add some simple tracking
- if(R?.persistent)
- var/list/recipe_data = R.SaveOldRecipe()
- file_data["[R.type]"] = recipe_data
-
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-///Saves all scars for everyone's original characters
-/datum/controller/subsystem/persistence/proc/save_scars()
- for(var/i in GLOB.joined_player_list)
- var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i)
- if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client?.prefs.read_preference(/datum/preference/toggle/persistent_scars))
- continue
-
- var/mob/living/carbon/human/original_human = ending_human.mind.original_character.resolve()
-
- if(!original_human)
- continue
-
- if(original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human)
- original_human.save_persistent_scars(TRUE)
- else
- original_human.save_persistent_scars()
-
-///Loads the custom outfits of every admin.
-/datum/controller/subsystem/persistence/proc/load_custom_outfits()
- var/file = file("data/custom_outfits.json")
- if(!fexists(file))
- return
- var/outfits_json = file2text(file)
- var/list/outfits = json_decode(outfits_json)
- if(!islist(outfits))
- return
-
- for(var/outfit_data in outfits)
- if(!islist(outfit_data))
- continue
-
- var/outfittype = text2path(outfit_data["outfit_type"])
- if(!ispath(outfittype, /datum/outfit))
- continue
- var/datum/outfit/outfit = new outfittype
- if(!outfit.load_from(outfit_data))
- continue
- GLOB.custom_outfits += outfit
-
-///Saves each admin's custom outfit list
-/datum/controller/subsystem/persistence/proc/save_custom_outfits()
- var/file = file("data/custom_outfits.json")
- fdel(file)
-
- var/list/data = list()
- for(var/datum/outfit/outfit in GLOB.custom_outfits)
- data += list(outfit.get_json_data())
-
- WRITE_FILE(file, json_encode(data))
-
-/// Location where we save the information about how many rounds it has been since the engine blew up
-#define DELAMINATION_COUNT_FILEPATH "data/rounds_since_delamination.txt"
-
-/datum/controller/subsystem/persistence/proc/load_delamination_counter()
- if (!fexists(DELAMINATION_COUNT_FILEPATH))
- return
- rounds_since_engine_exploded = text2num(file2text(DELAMINATION_COUNT_FILEPATH))
- for (var/obj/structure/sign/delamination_counter/sign as anything in GLOB.map_delamination_counters)
- sign.update_count(rounds_since_engine_exploded)
-
-/datum/controller/subsystem/persistence/proc/save_delamination_counter()
- rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_COUNT_FILEPATH)
-
-#undef DELAMINATION_COUNT_FILEPATH
-#undef FILE_RECENT_MAPS
-#undef KEEP_ROUNDS_MAP
diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
new file mode 100644
index 000000000000..3153c72eb2c7
--- /dev/null
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -0,0 +1,111 @@
+#define FILE_RECENT_MAPS "data/RecentMaps.json"
+#define KEEP_ROUNDS_MAP 3
+
+SUBSYSTEM_DEF(persistence)
+ name = "Persistence"
+ init_order = INIT_ORDER_PERSISTENCE
+ flags = SS_NO_FIRE
+
+ ///instantiated wall engraving components
+ var/list/wall_engravings = list()
+ ///all saved persistent engravings loaded from JSON
+ var/list/saved_engravings = list()
+ ///tattoo stories that we're saving.
+ var/list/prison_tattoos_to_save = list()
+ ///tattoo stories that have been selected for this round.
+ var/list/prison_tattoos_to_use = list()
+ var/list/saved_messages = list()
+ var/list/saved_modes = list(1,2,3)
+ var/list/saved_maps = list()
+ var/list/blocked_maps = list()
+ var/list/saved_trophies = list()
+ var/list/picture_logging_information = list()
+ var/list/obj/structure/sign/picture_frame/photo_frames
+ var/list/obj/item/storage/photo_album/photo_albums
+ var/rounds_since_engine_exploded = 0
+ var/delam_highscore = 0
+ var/tram_hits_this_round = 0
+ var/tram_hits_last_round = 0
+
+/datum/controller/subsystem/persistence/Initialize()
+ load_poly()
+ load_wall_engravings()
+ load_prisoner_tattoos()
+ load_trophies()
+ load_recent_maps()
+ load_photo_persistence()
+ load_randomized_recipes()
+ load_custom_outfits()
+ load_delamination_counter()
+ load_adventures()
+ return SS_INIT_SUCCESS
+
+///Collects all data to persist.
+/datum/controller/subsystem/persistence/proc/collect_data()
+ save_wall_engravings()
+ save_prisoner_tattoos()
+ collect_trophies()
+ collect_maps()
+ save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
+ save_randomized_recipes()
+ save_scars()
+ save_custom_outfits()
+ save_delamination_counter()
+ if(GLOB.interviews)
+ save_keys(GLOB.interviews.approved_ckeys)
+
+///Loads up Poly's speech buffer.
+/datum/controller/subsystem/persistence/proc/load_poly()
+ for(var/mob/living/basic/parrot/poly/bird in GLOB.alive_mob_list)
+ var/list/list_to_read = bird.get_static_list_of_phrases()
+ twitterize(list_to_read, "polytalk")
+ break //Who's been duping the bird?!
+
+/// Loads up the amount of times maps appeared to alter their appearance in voting and rotation.
+/datum/controller/subsystem/persistence/proc/load_recent_maps()
+ var/map_sav = FILE_RECENT_MAPS
+ if(!fexists(FILE_RECENT_MAPS))
+ return
+ var/list/json = json_decode(file2text(map_sav))
+ if(!json)
+ return
+ saved_maps = json["data"]
+
+ //Convert the mapping data to a shared blocking list, saves us doing this in several places later.
+ for(var/map in config.maplist)
+ var/datum/map_config/VM = config.maplist[map]
+ var/run = 0
+ if(VM.map_name == SSmapping.config.map_name)
+ run++
+ for(var/name in SSpersistence.saved_maps)
+ if(VM.map_name == name)
+ run++
+ if(run >= 2) //If run twice in the last KEEP_ROUNDS_MAP + 1 (including current) rounds, disable map for voting and rotation.
+ blocked_maps += VM.map_name
+
+///Updates the list of the most recent maps.
+/datum/controller/subsystem/persistence/proc/collect_maps()
+ if(length(saved_maps) > KEEP_ROUNDS_MAP) //Get rid of extras from old configs.
+ saved_maps.Cut(KEEP_ROUNDS_MAP+1)
+ var/mapstosave = min(length(saved_maps)+1, KEEP_ROUNDS_MAP)
+ if(length(saved_maps) < mapstosave) //Add extras if too short, one per round.
+ saved_maps += mapstosave
+ for(var/i = mapstosave; i > 1; i--)
+ saved_maps[i] = saved_maps[i-1]
+ saved_maps[1] = SSmapping.config.map_name
+ var/json_file = file(FILE_RECENT_MAPS)
+ var/list/file_data = list()
+ file_data["data"] = saved_maps
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/save_keys(list/approved_ckeys)
+ var/json_file = file("data/approved_keys.json")
+ var/list/keys = list()
+ if(fexists(json_file))
+ fdel(json_file)
+ keys = json_encode(approved_ckeys)
+ WRITE_FILE(json_file, keys)
+
+#undef FILE_RECENT_MAPS
+#undef KEEP_ROUNDS_MAP
diff --git a/code/controllers/subsystem/persistence/counter_delamination.dm b/code/controllers/subsystem/persistence/counter_delamination.dm
new file mode 100644
index 000000000000..57ea7bde9f35
--- /dev/null
+++ b/code/controllers/subsystem/persistence/counter_delamination.dm
@@ -0,0 +1,19 @@
+/// Location where we save the information about how many rounds it has been since the engine blew up
+#define DELAMINATION_COUNT_FILEPATH "data/rounds_since_delamination.txt"
+#define DELAMINATION_HIGHSCORE_FILEPATH "data/delamination_highscore.txt"
+
+/datum/controller/subsystem/persistence/proc/load_delamination_counter()
+ if (!fexists(DELAMINATION_COUNT_FILEPATH))
+ return
+ rounds_since_engine_exploded = text2num(file2text(DELAMINATION_COUNT_FILEPATH))
+ for (var/obj/structure/sign/delamination_counter/sign as anything in GLOB.map_delamination_counters)
+ sign.update_count(rounds_since_engine_exploded)
+
+
+/datum/controller/subsystem/persistence/proc/save_delamination_counter()
+ rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_COUNT_FILEPATH)
+ if((rounds_since_engine_exploded + 1) > delam_highscore)
+ rustg_file_write("[rounds_since_engine_exploded + 1]", DELAMINATION_HIGHSCORE_FILEPATH)
+
+#undef DELAMINATION_COUNT_FILEPATH
+#undef DELAMINATION_HIGHSCORE_FILEPATH
diff --git a/code/controllers/subsystem/persistence/custom_outfits.dm b/code/controllers/subsystem/persistence/custom_outfits.dm
new file mode 100644
index 000000000000..942f874fde5e
--- /dev/null
+++ b/code/controllers/subsystem/persistence/custom_outfits.dm
@@ -0,0 +1,32 @@
+///Loads the custom outfits of every admin.
+/datum/controller/subsystem/persistence/proc/load_custom_outfits()
+ var/file = file("data/custom_outfits.json")
+ if(!fexists(file))
+ return
+ var/outfits_json = file2text(file)
+ var/list/outfits = json_decode(outfits_json)
+ if(!islist(outfits))
+ return
+
+ for(var/outfit_data in outfits)
+ if(!islist(outfit_data))
+ continue
+
+ var/outfittype = text2path(outfit_data["outfit_type"])
+ if(!ispath(outfittype, /datum/outfit))
+ continue
+ var/datum/outfit/outfit = new outfittype
+ if(!outfit.load_from(outfit_data))
+ continue
+ GLOB.custom_outfits += outfit
+
+///Saves each admin's custom outfit list
+/datum/controller/subsystem/persistence/proc/save_custom_outfits()
+ var/file = file("data/custom_outfits.json")
+ fdel(file)
+
+ var/list/data = list()
+ for(var/datum/outfit/outfit in GLOB.custom_outfits)
+ data += list(outfit.get_json_data())
+
+ WRITE_FILE(file, json_encode(data))
diff --git a/code/controllers/subsystem/persistence/engravings.dm b/code/controllers/subsystem/persistence/engravings.dm
new file mode 100644
index 000000000000..f47fc7fbba12
--- /dev/null
+++ b/code/controllers/subsystem/persistence/engravings.dm
@@ -0,0 +1,84 @@
+///Loads all engravings, and places a select amount in maintenance and the prison.
+/datum/controller/subsystem/persistence/proc/load_wall_engravings()
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ if(!fexists(json_file))
+ return
+
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+
+ if(json["version"] < ENGRAVING_PERSISTENCE_VERSION)
+ update_wall_engravings(json)
+
+ saved_engravings = json["entries"]
+
+ if(!saved_engravings.len)
+ log_world("Failed to load engraved messages on map [SSmapping.config.map_name]")
+ return
+
+ var/list/viable_turfs = get_area_turfs(/area/station/maintenance, subtypes = TRUE) + get_area_turfs(/area/station/security/prison, subtypes = TRUE)
+ var/list/turfs_to_pick_from = list()
+
+ for(var/turf/T as anything in viable_turfs)
+ if(!isclosedturf(T))
+ continue
+ turfs_to_pick_from += T
+
+ var/successfully_loaded_engravings = 0
+
+ for(var/iteration in 1 to rand(MIN_PERSISTENT_ENGRAVINGS, MAX_PERSISTENT_ENGRAVINGS))
+ var/engraving = pick_n_take(saved_engravings)
+ if(!islist(engraving))
+ stack_trace("something's wrong with the engraving data! one of the saved engravings wasn't a list!")
+ continue
+
+ var/turf/closed/engraved_wall = pick(turfs_to_pick_from)
+
+ if(HAS_TRAIT(engraved_wall, TRAIT_NOT_ENGRAVABLE))
+ continue
+
+ engraved_wall.AddComponent(/datum/component/engraved, engraving["story"], FALSE, engraving["story_value"])
+ successfully_loaded_engravings++
+ turfs_to_pick_from -= engraved_wall
+
+ log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]")
+
+///Saves all new engravings in the world.
+/datum/controller/subsystem/persistence/proc/save_wall_engravings()
+ var/list/saved_data = list()
+
+ saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
+ saved_data["entries"] = list()
+
+
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ if(fexists(json_file))
+ var/list/old_json = json_decode(file2text(json_file))
+ if(old_json)
+ saved_data["entries"] = old_json["entries"]
+
+ for(var/datum/component/engraved/engraving in wall_engravings)
+ if(!engraving.persistent_save)
+ continue
+ var/area/engraved_area = get_area(engraving.parent)
+ if(!(engraved_area.area_flags & PERSISTENT_ENGRAVINGS))
+ continue
+ saved_data["entries"] += engraving.save_persistent()
+
+ fdel(json_file)
+
+ WRITE_FILE(json_file, json_encode(saved_data))
+
+///This proc can update entries if the format has changed at some point.
+/datum/controller/subsystem/persistence/proc/update_wall_engravings(json)
+ for(var/engraving_entry in json["entries"])
+ continue //no versioning yet
+
+ //Save it to the file
+ var/json_file = file(ENGRAVING_SAVE_FILE)
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(json))
+
+ return json
+
diff --git a/code/controllers/subsystem/persistence/photo_albums.dm b/code/controllers/subsystem/persistence/photo_albums.dm
new file mode 100644
index 000000000000..3aee856b2c21
--- /dev/null
+++ b/code/controllers/subsystem/persistence/photo_albums.dm
@@ -0,0 +1,86 @@
+///Loads up the photo album source file.
+/datum/controller/subsystem/persistence/proc/get_photo_albums()
+ var/album_path = file("data/photo_albums.json")
+ if(fexists(album_path))
+ return json_decode(file2text(album_path))
+
+///Loads up the photo frames source file.
+/datum/controller/subsystem/persistence/proc/get_photo_frames()
+ var/frame_path = file("data/photo_frames.json")
+ if(fexists(frame_path))
+ return json_decode(file2text(frame_path))
+
+/// Removes the identifier of a persistent photo frame from the json.
+/datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier)
+ var/frame_path = file("data/photo_frames.json")
+ if(!fexists(frame_path))
+ return
+
+ var/frame_json = json_decode(file2text(frame_path))
+ frame_json -= identifier
+
+ frame_json = json_encode(frame_json)
+ fdel(frame_path)
+ WRITE_FILE(frame_path, frame_json)
+
+///Loads photo albums, and populates them; also loads and applies frames to picture frames.
+/datum/controller/subsystem/persistence/proc/load_photo_persistence()
+ var/album_path = file("data/photo_albums.json")
+ var/frame_path = file("data/photo_frames.json")
+ if(fexists(album_path))
+ var/list/json = json_decode(file2text(album_path))
+ if(json.len)
+ for(var/i in photo_albums)
+ var/obj/item/storage/photo_album/A = i
+ if(!A.persistence_id)
+ continue
+ if(json[A.persistence_id])
+ A.populate_from_id_list(json[A.persistence_id])
+
+ if(fexists(frame_path))
+ var/list/json = json_decode(file2text(frame_path))
+ if(json.len)
+ for(var/i in photo_frames)
+ var/obj/structure/sign/picture_frame/PF = i
+ if(!PF.persistence_id)
+ continue
+ if(json[PF.persistence_id])
+ PF.load_from_id(json[PF.persistence_id])
+
+///Saves the contents of photo albums and the picture frames.
+/datum/controller/subsystem/persistence/proc/save_photo_persistence()
+ var/album_path = file("data/photo_albums.json")
+ var/frame_path = file("data/photo_frames.json")
+
+ var/list/frame_json = list()
+ var/list/album_json = list()
+
+ if(fexists(album_path))
+ album_json = json_decode(file2text(album_path))
+ fdel(album_path)
+
+ for(var/i in photo_albums)
+ var/obj/item/storage/photo_album/A = i
+ if(!istype(A) || !A.persistence_id)
+ continue
+ var/list/L = A.get_picture_id_list()
+ album_json[A.persistence_id] = L
+
+ album_json = json_encode(album_json)
+
+ WRITE_FILE(album_path, album_json)
+
+ if(fexists(frame_path))
+ frame_json = json_decode(file2text(frame_path))
+ fdel(frame_path)
+
+ for(var/i in photo_frames)
+ var/obj/structure/sign/picture_frame/F = i
+ if(!istype(F) || !F.persistence_id)
+ continue
+ frame_json[F.persistence_id] = F.get_photo_id()
+
+ frame_json = json_encode(frame_json)
+
+ WRITE_FILE(frame_path, frame_json)
+
diff --git a/code/controllers/subsystem/persistence/recipes.dm b/code/controllers/subsystem/persistence/recipes.dm
new file mode 100644
index 000000000000..84145b26aabc
--- /dev/null
+++ b/code/controllers/subsystem/persistence/recipes.dm
@@ -0,0 +1,39 @@
+///Loads all randomized recipes.
+/datum/controller/subsystem/persistence/proc/load_randomized_recipes()
+ var/json_file = file("data/RandomizedChemRecipes.json")
+ var/json
+ if(fexists(json_file))
+ json = json_decode(file2text(json_file))
+
+ for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
+ var/datum/chemical_reaction/randomized/R = new randomized_type
+ var/loaded = FALSE
+ if(R.persistent && json)
+ var/list/recipe_data = json["[R.type]"]
+ if(recipe_data)
+ if(R.LoadOldRecipe(recipe_data) && (daysSince(R.created) <= R.persistence_period))
+ loaded = TRUE
+ if(!loaded) //We do not have information for whatever reason, just generate new one
+ if(R.persistent)
+ log_game("Resetting persistent [randomized_type] random recipe.")
+ R.GenerateRecipe()
+
+ if(!R.HasConflicts()) //Might want to try again if conflicts happened in the future.
+ add_chemical_reaction(R)
+ else
+ log_game("Randomized recipe [randomized_type] resulted in conflicting recipes.")
+
+///Saves all randomized recipes.
+/datum/controller/subsystem/persistence/proc/save_randomized_recipes()
+ var/json_file = file("data/RandomizedChemRecipes.json")
+ var/list/file_data = list()
+
+ //asert globchems done
+ for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
+ var/datum/chemical_reaction/randomized/R = get_chemical_reaction(randomized_type) //ew, would be nice to add some simple tracking
+ if(R?.persistent)
+ var/list/recipe_data = R.SaveOldRecipe()
+ file_data["[R.type]"] = recipe_data
+
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
diff --git a/code/controllers/subsystem/persistence/scars.dm b/code/controllers/subsystem/persistence/scars.dm
new file mode 100644
index 000000000000..fa378f24dd43
--- /dev/null
+++ b/code/controllers/subsystem/persistence/scars.dm
@@ -0,0 +1,17 @@
+///Saves all scars for everyone's original characters
+/datum/controller/subsystem/persistence/proc/save_scars()
+ for(var/i in GLOB.joined_player_list)
+ var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i)
+ if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client?.prefs.read_preference(/datum/preference/toggle/persistent_scars))
+ continue
+
+ var/mob/living/carbon/human/original_human = ending_human.mind.original_character.resolve()
+
+ if(!original_human)
+ continue
+
+ if(original_human.stat == DEAD || !original_human.all_scars || original_human != ending_human)
+ original_human.save_persistent_scars(TRUE)
+ else
+ original_human.save_persistent_scars()
+
diff --git a/code/controllers/subsystem/persistence/tattoos.dm b/code/controllers/subsystem/persistence/tattoos.dm
new file mode 100644
index 000000000000..45dd31c7f5c9
--- /dev/null
+++ b/code/controllers/subsystem/persistence/tattoos.dm
@@ -0,0 +1,55 @@
+///Loads all tattoos, and select a few based on the amount of prisoner spawn positions.
+/datum/controller/subsystem/persistence/proc/load_prisoner_tattoos()
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+
+ if(json["version"] < TATTOO_PERSISTENCE_VERSION)
+ update_prisoner_tattoos(json)
+
+ var/datum/job/prisoner_datum = SSjob.name_occupations[JOB_PRISONER]
+ if(!prisoner_datum)
+ return
+ var/iterations_allowed = prisoner_datum.spawn_positions
+
+ var/list/entries = json["entries"]
+ if(entries.len)
+ for(var/index in 1 to iterations_allowed)
+ prison_tattoos_to_use += list(entries[rand(1, entries.len)])
+
+ log_world("Loaded [prison_tattoos_to_use.len] prison tattoos")
+
+///Saves all tattoos, so they can appear on prisoners in future rounds
+/datum/controller/subsystem/persistence/proc/save_prisoner_tattoos()
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ var/list/saved_data = list()
+ var/list/entries = list()
+
+ if(fexists(json_file))
+ var/list/old_json = json_decode(file2text(json_file))
+ if(old_json)
+ entries += old_json["entries"] //Save the old if its there
+
+ entries += prison_tattoos_to_save
+
+ saved_data["version"] = ENGRAVING_PERSISTENCE_VERSION
+ saved_data["entries"] = entries
+
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(saved_data))
+
+///This proc can update entries if the format has changed at some point.
+/datum/controller/subsystem/persistence/proc/update_prisoner_tattoos(json)
+ for(var/tattoo_entry in json["entries"])
+ continue //no versioning yet
+
+ //Save it to the file
+ var/json_file = file(PRISONER_TATTOO_SAVE_FILE)
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(json))
+
+ return json
+
diff --git a/code/controllers/subsystem/persistence/trophies.dm b/code/controllers/subsystem/persistence/trophies.dm
new file mode 100644
index 000000000000..515ed3860850
--- /dev/null
+++ b/code/controllers/subsystem/persistence/trophies.dm
@@ -0,0 +1,125 @@
+///trophy data datum, for admin manipulation
+/datum/trophy_data
+ ///path of the item the trophy will try to mimic, null if path_string is invalid
+ var/path
+ ///the message that appears under the item
+ var/message
+ ///the key of the one who placed the item in the trophy case
+ var/placer_key
+
+/// Loads the trophies from the source file, and places a few in trophy display cases.
+/datum/controller/subsystem/persistence/proc/load_trophies()
+ var/list/raw_saved_trophies = list()
+ if(fexists("data/npc_saves/TrophyItems.json"))
+ var/json_file = file("data/npc_saves/TrophyItems.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ raw_saved_trophies = json["data"]
+ fdel("data/npc_saves/TrophyItems.json")
+ else
+ var/json_file = file("data/trophy_items.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ raw_saved_trophies = json["data"]
+
+ for(var/raw_json in raw_saved_trophies)
+ var/datum/trophy_data/parsed_trophy_data = new
+ parsed_trophy_data.load_from_json(raw_json)
+ saved_trophies += parsed_trophy_data
+
+ set_up_trophies()
+
+/datum/trophy_data/proc/load_from_json(list/json_data)
+ path = json_data["path"]
+ message = json_data["message"]
+ placer_key = json_data["placer_key"]
+
+/datum/trophy_data/proc/to_json()
+ var/list/new_data = list()
+ new_data["path"] = path
+ new_data["message"] = message
+ new_data["placer_key"] = placer_key
+ new_data["is_valid"] = text2path(path) ? TRUE : FALSE
+ return new_data
+
+/// Returns a list for the admin trophy panel.
+/datum/controller/subsystem/persistence/proc/trophy_ui_data()
+ var/list/ui_data = list()
+ for(var/datum/trophy_data/data in saved_trophies)
+ var/list/pdata = data.to_json()
+ pdata["ref"] = REF(data)
+ ui_data += list(pdata)
+
+ return ui_data
+
+
+/// Puts trophies into trophy cases.
+/datum/controller/subsystem/persistence/proc/set_up_trophies()
+
+ var/list/valid_trophies = list()
+
+ for(var/datum/trophy_data/data in saved_trophies)
+
+ if(!data) //sanity for incorrect deserialization
+ continue
+
+ var/path = text2path(data.path)
+ if(!path) //If the item no longer exist, ignore it
+ continue
+
+ valid_trophies += data
+
+ for(var/obj/structure/displaycase/trophy/trophy_case in GLOB.trophy_cases)
+ if(!valid_trophies.len)
+ break
+
+ if(trophy_case.showpiece)
+ continue
+
+ trophy_case.set_up_trophy(pick_n_take(valid_trophies))
+
+///Collects trophies from all existing trophy cases.
+/datum/controller/subsystem/persistence/proc/collect_trophies()
+ for(var/trophy_case in GLOB.trophy_cases)
+ save_trophy(trophy_case)
+
+ var/json_file = file("data/trophy_items.json")
+ var/list/file_data = list()
+ var/list/converted_data = list()
+
+ for(var/datum/trophy_data/data in saved_trophies)
+ converted_data += list(data.to_json())
+
+ converted_data = remove_duplicate_trophies(converted_data)
+
+ file_data["data"] = converted_data
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+///gets the list of json trophies, and deletes the ones with an identical path and message
+/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
+ var/list/ukeys = list()
+ . = list()
+ for(var/trophy in trophies)
+ var/tkey = "[trophy["path"]]-[trophy["message"]]"
+ if(ukeys[tkey])
+ continue
+ else
+ . += list(trophy)
+ ukeys[tkey] = TRUE
+
+///If there is a trophy in the trophy case, saved it, if the trophy was not a holo trophy and has a message attached.
+/datum/controller/subsystem/persistence/proc/save_trophy(obj/structure/displaycase/trophy/trophy_case)
+ if(!trophy_case.holographic_showpiece && trophy_case.showpiece && trophy_case.trophy_message)
+ var/datum/trophy_data/data = new
+ data.path = trophy_case.showpiece.type
+ data.message = trophy_case.trophy_message
+ data.placer_key = trophy_case.placer_key
+ saved_trophies += data
+
diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm
index 4ec698db32bb..e92da2474bb7 100644
--- a/code/controllers/subsystem/processing/ai_behaviors.dm
+++ b/code/controllers/subsystem/processing/ai_behaviors.dm
@@ -8,9 +8,12 @@ PROCESSING_SUBSYSTEM_DEF(ai_behaviors)
wait = 1
///List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors()
var/list/ai_behaviors
+ ///List of all targeting_strategy singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors()
+ var/list/targeting_strategies
/datum/controller/subsystem/processing/ai_behaviors/Initialize()
SetupAIBehaviors()
+ SetupTargetingStrats()
return SS_INIT_SUCCESS
/datum/controller/subsystem/processing/ai_behaviors/proc/SetupAIBehaviors()
@@ -18,3 +21,9 @@ PROCESSING_SUBSYSTEM_DEF(ai_behaviors)
for(var/behavior_type in subtypesof(/datum/ai_behavior))
var/datum/ai_behavior/ai_behavior = new behavior_type
ai_behaviors[behavior_type] = ai_behavior
+
+/datum/controller/subsystem/processing/ai_behaviors/proc/SetupTargetingStrats()
+ targeting_strategies = list()
+ for(var/target_type in subtypesof(/datum/targeting_strategy))
+ var/datum/targeting_strategy/target_start = new target_type
+ targeting_strategies[target_type] = target_start
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index c94405a1afa3..fde6cd4fed0d 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -1,4 +1,5 @@
#define ROUND_START_MUSIC_LIST "strings/round_start_sounds.txt"
+#define SS_TICKER_TRAIT "SS_Ticker"
SUBSYSTEM_DEF(ticker)
name = "Ticker"
@@ -482,7 +483,7 @@ SUBSYSTEM_DEF(ticker)
var/mob/living = player.transfer_character()
if(living)
qdel(player)
- living.notransform = TRUE
+ ADD_TRAIT(living, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT)
if(living.client)
var/atom/movable/screen/splash/S = new(null, living.client, TRUE)
S.Fade(TRUE)
@@ -497,12 +498,11 @@ SUBSYSTEM_DEF(ticker)
if(living.job == processing_reward_jobs)
living.client.reward_this_person += 150
if(livings.len)
- addtimer(CALLBACK(src, PROC_REF(release_characters), livings), 30, TIMER_CLIENT_TIME)
+ addtimer(CALLBACK(src, PROC_REF(release_characters), livings), 3 SECONDS, TIMER_CLIENT_TIME)
/datum/controller/subsystem/ticker/proc/release_characters(list/livings)
- for(var/I in livings)
- var/mob/living/L = I
- L.notransform = FALSE
+ for(var/mob/living/living_mob as anything in livings)
+ REMOVE_TRAIT(living_mob, TRAIT_NO_TRANSFORM, SS_TICKER_TRAIT)
/datum/controller/subsystem/ticker/proc/check_queue()
if(!queued_players.len)
@@ -766,3 +766,4 @@ SUBSYSTEM_DEF(ticker)
return "[global.config.directory]/reboot_themes/[pick(possible_themes)]"
#undef ROUND_START_MUSIC_LIST
+#undef SS_TICKER_TRAIT
diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm
index 015df7d310b3..d4d02901dda2 100644
--- a/code/datums/actions/cooldown_action.dm
+++ b/code/datums/actions/cooldown_action.dm
@@ -177,6 +177,8 @@
/// Starts a cooldown time for other abilities that share a cooldown with this. Has some niche usage with more complicated attack ai!
/// Will use default cooldown time if an override is not specified
/datum/action/cooldown/proc/StartCooldownOthers(override_cooldown_time)
+ if(!length(owner.actions))
+ return // Possible if they have an action they don't control
for(var/datum/action/cooldown/shared_ability in owner.actions - src)
if(!(shared_cooldown & shared_ability.shared_cooldown))
continue
diff --git a/code/datums/actions/mobs/charge.dm b/code/datums/actions/mobs/charge.dm
index 17f1dd2aeb16..4bb3c70e7444 100644
--- a/code/datums/actions/mobs/charge.dm
+++ b/code/datums/actions/mobs/charge.dm
@@ -152,17 +152,23 @@
SSexplosions.med_mov_atom += target
INVOKE_ASYNC(src, PROC_REF(DestroySurroundings), source)
- hit_target(source, target, charge_damage)
-
-/datum/action/cooldown/mob_cooldown/charge/proc/hit_target(atom/movable/source, atom/target, damage_dealt)
- if(!isliving(target))
- return
- var/mob/living/living_target = target
- living_target.visible_message("[source] slams into [living_target]!", "[source] tramples you into the ground!")
- source.forceMove(get_turf(living_target))
- living_target.apply_damage(damage_dealt, BRUTE, wound_bonus = CANT_WOUND)
- playsound(get_turf(living_target), 'sound/effects/meteorimpact.ogg', 100, TRUE)
- shake_camera(living_target, 4, 3)
+ try_hit_target(source, target)
+
+/// Attempt to hit someone with our charge
+/datum/action/cooldown/mob_cooldown/charge/proc/try_hit_target(atom/movable/source, atom/target)
+ if (can_hit_target(source, target))
+ hit_target(source, target, charge_damage)
+
+/// Returns true if we're allowed to charge into this target
+/datum/action/cooldown/mob_cooldown/charge/proc/can_hit_target(atom/movable/source, atom/target)
+ return isliving(target)
+
+/// Actually hit someone
+/datum/action/cooldown/mob_cooldown/charge/proc/hit_target(atom/movable/source, mob/living/target, damage_dealt)
+ target.visible_message(span_danger("[source] slams into [target]!"), span_userdanger("[source] tramples you into the ground!"))
+ target.apply_damage(damage_dealt, BRUTE, wound_bonus = CANT_WOUND)
+ playsound(get_turf(target), 'sound/effects/meteorimpact.ogg', 100, TRUE)
+ shake_camera(target, 4, 3)
shake_camera(source, 2, 3)
/datum/action/cooldown/mob_cooldown/charge/basic_charge
@@ -183,18 +189,21 @@
/datum/action/cooldown/mob_cooldown/charge/basic_charge/do_charge_indicator(atom/charger, atom/charge_target)
charger.Shake(shake_pixel_shift, shake_pixel_shift, shake_duration)
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/can_hit_target(atom/movable/source, atom/target)
+ if(!isliving(target))
+ if(!target.density || target.CanPass(source, get_dir(target, source)))
+ return FALSE
+ return TRUE
+ return ..()
+
/datum/action/cooldown/mob_cooldown/charge/basic_charge/hit_target(atom/movable/source, atom/target, damage_dealt)
var/mob/living/living_source
if(isliving(source))
living_source = source
if(!isliving(target))
- if(!target.density || target.CanPass(source, get_dir(target, source)))
- return
source.visible_message(span_danger("[source] smashes into [target]!"))
- if(!living_source)
- return
- living_source.Stun(recoil_duration, ignore_canstun = TRUE)
+ living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
return
var/mob/living/living_target = target
@@ -204,10 +213,9 @@
living_source.Stun(recoil_duration, ignore_canstun = TRUE)
return
- living_target.visible_message(span_danger("[source] charges on [living_target]!"), span_userdanger("[source] charges into you!"))
+ living_target.visible_message(span_danger("[source] charges into [living_target]!"), span_userdanger("[source] charges into you!"))
living_target.Knockdown(knockdown_duration)
-
/datum/status_effect/tired_post_charge
id = "tired_post_charge"
duration = 1 SECONDS
diff --git a/code/datums/actions/mobs/fire_breath.dm b/code/datums/actions/mobs/fire_breath.dm
index 57962035a24d..165b87b02860 100644
--- a/code/datums/actions/mobs/fire_breath.dm
+++ b/code/datums/actions/mobs/fire_breath.dm
@@ -2,62 +2,121 @@
name = "Fire Breath"
button_icon = 'icons/obj/wizard.dmi'
button_icon_state = "fireball"
- desc = "Allows you to shoot fire towards a target."
+ desc = "Breathe a line of flames towards the target."
cooldown_time = 3 SECONDS
/// The range of the fire
var/fire_range = 15
/// The sound played when you use this ability
var/fire_sound = 'sound/magic/fireball.ogg'
- /// If the fire should be icey fire
- var/ice_breath = FALSE
+ /// Time to wait between spawning each fire turf
+ var/fire_delay = 1.5 DECISECONDS
+ /// How hot is our fire
+ var/fire_temperature = DRAKE_FIRE_TEMP
+ /// 'How much' fire do we expose the turf to?
+ var/fire_volume = DRAKE_FIRE_EXPOSURE
+ /// How much damage do you take when engulfed?
+ var/fire_damage = 20
+ /// How much damage to mechs take when engulfed?
+ var/mech_damage = 45
/datum/action/cooldown/mob_cooldown/fire_breath/Activate(atom/target_atom)
- StartCooldown(360 SECONDS, 360 SECONDS)
attack_sequence(target_atom)
StartCooldown()
return TRUE
+/// Apply our specific fire breathing shape, in proc form so we can override it in subtypes
/datum/action/cooldown/mob_cooldown/fire_breath/proc/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
- fire_line(target, 0)
+ fire_line(target)
+/// Breathe fire in a line towards the target, optionally rotated at an offset from the target
/datum/action/cooldown/mob_cooldown/fire_breath/proc/fire_line(atom/target, offset)
- SLEEP_CHECK_DEATH(0, owner)
- var/list/turfs = line_target(offset, fire_range, target)
- dragon_fire_line(owner, turfs, ice_breath)
+ if (isnull(target))
+ return
+ var/turf/target_turf = get_ranged_target_turf_direct(owner, target, fire_range, offset)
+ var/list/turfs = get_line(owner, target_turf) - get_turf(owner)
+ INVOKE_ASYNC(src, PROC_REF(progressive_fire_line), turfs)
-/datum/action/cooldown/mob_cooldown/fire_breath/proc/line_target(offset, range, atom/target)
- if(!target)
+/// Creates fire with a delay on the list of targeted turfs
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/progressive_fire_line(list/burn_turfs)
+ if (QDELETED(owner) || owner.stat == DEAD)
return
- var/turf/T = get_ranged_target_turf_direct(owner, target, range, offset)
- return (get_line(owner, T) - get_turf(owner))
+ // Guys we have already hit, no double dipping
+ var/list/hit_list = list(owner) // also don't burn ourselves
+ for(var/turf/target_turf in burn_turfs)
+ if (target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ return
+ burn_turf(target_turf, hit_list, owner)
+ sleep(fire_delay)
+
+/// Finally spawn the actual fire, spawns the fire hotspot in case you want to recolour it or something
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/burn_turf(turf/fire_turf, list/hit_list, mob/living/source)
+ var/obj/effect/hotspot/fire_hotspot = new /obj/effect/hotspot(fire_turf)
+ fire_turf.hotspot_expose(fire_temperature, fire_volume, TRUE)
+
+ for(var/mob/living/barbecued in fire_turf.contents)
+ if(barbecued in hit_list)
+ continue
+ hit_list |= barbecued
+ on_burn_mob(barbecued, source)
+
+ for(var/obj/vehicle/sealed/mecha/robotron in fire_turf.contents)
+ if(robotron in hit_list)
+ continue
+ hit_list |= robotron
+ robotron.take_damage(mech_damage, BURN, FIRE)
+
+ return fire_hotspot
+/// Do something unpleasant to someone we set on fire
+/datum/action/cooldown/mob_cooldown/fire_breath/proc/on_burn_mob(mob/living/barbecued, mob/living/source)
+ to_chat(barbecued, span_userdanger("You are burned by [source]'s fire breath!"))
+ barbecued.adjustFireLoss(fire_damage)
+
+/// Shoot three lines of fire in a sort of fork pattern approximating a cone
/datum/action/cooldown/mob_cooldown/fire_breath/cone
name = "Fire Cone"
- desc = "Allows you to shoot fire towards a target with surrounding lines of fire."
+ desc = "Breathe several lines of fire directed at a target."
/// The angles relative to the target that shoot lines of fire
var/list/angles = list(-40, 0, 40)
/datum/action/cooldown/mob_cooldown/fire_breath/cone/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
for(var/offset in angles)
- INVOKE_ASYNC(src, PROC_REF(fire_line), target, offset)
+ fire_line(target, offset)
+/// Shoot fire in a whole bunch of directions
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire
name = "Mass Fire"
button_icon = 'icons/effects/fire.dmi'
button_icon_state = "1"
- desc = "Allows you to shoot fire in all directions."
+ desc = "Breathe flames in all directions."
cooldown_time = 3 SECONDS
+ click_to_activate = FALSE
+ /// How many fire lines do we produce to turn a full circle?
+ var/sectors = 12
+ /// How long do we wait between each spin?
+ var/breath_delay = 2.5 SECONDS
+ /// How many full circles do we perform?
+ var/total_spins = 3
+
+/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/Activate(atom/target_atom)
+ target_atom = get_step(owner, owner.dir) // Just shoot it forwards, we don't need to click on someone for this one
+ return ..()
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/attack_sequence(atom/target)
- shoot_mass_fire(target, 12, 2.5 SECONDS, 3)
-
-/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/proc/shoot_mass_fire(atom/target, spiral_count, delay_time, times)
- SLEEP_CHECK_DEATH(0, owner)
- for(var/i = 1 to times)
- playsound(owner.loc, fire_sound, 200, TRUE)
- var/increment = 360 / spiral_count
- for(var/j = 1 to spiral_count)
- INVOKE_ASYNC(src, PROC_REF(fire_line), target, j * increment + i * increment / 2)
- SLEEP_CHECK_DEATH(delay_time, owner)
+ var/queued_spins = 0
+ for (var/i in 1 to total_spins)
+ var/delay = queued_spins * breath_delay
+ queued_spins++
+ addtimer(CALLBACK(src, PROC_REF(fire_spin), target, queued_spins), delay)
+
+/// Breathe fire in a circle, with a slight angle offset based on which of our several circles it is
+/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/proc/fire_spin(target, spin_count)
+ if (QDELETED(owner) || owner.stat == DEAD)
+ return // Too dead to spin
+ playsound(owner.loc, fire_sound, 200, TRUE)
+ var/angle_increment = 360 / sectors
+ var/additional_offset = spin_count * angle_increment / 2
+ for (var/i in 1 to sectors)
+ fire_line(target, (angle_increment * i) + (additional_offset))
diff --git a/code/datums/actions/mobs/lava_swoop.dm b/code/datums/actions/mobs/lava_swoop.dm
index d23252c73aac..d18ad2f3af3b 100644
--- a/code/datums/actions/mobs/lava_swoop.dm
+++ b/code/datums/actions/mobs/lava_swoop.dm
@@ -38,7 +38,7 @@
return
// stop swooped target movement
swooping = TRUE
- owner.set_density(FALSE)
+ ADD_TRAIT(owner, TRAIT_UNDENSE, SWOOPING_TRAIT)
owner.visible_message(span_boldwarning("[owner] swoops up high!"))
var/negative
@@ -115,7 +115,7 @@
for(var/mob/M in range(7, owner))
shake_camera(M, 15, 1)
- owner.set_density(TRUE)
+ REMOVE_TRAIT(owner, TRAIT_UNDENSE, SWOOPING_TRAIT)
SLEEP_CHECK_DEATH(1, owner)
swooping = FALSE
if(!lava_success)
diff --git a/code/datums/actions/mobs/mobcooldown.dm b/code/datums/actions/mobs/mobcooldown.dm
index 17561f2f45a5..a495859494ff 100644
--- a/code/datums/actions/mobs/mobcooldown.dm
+++ b/code/datums/actions/mobs/mobcooldown.dm
@@ -3,7 +3,7 @@
button_icon = 'icons/mob/actions/actions_items.dmi'
button_icon_state = "sniper_zoom"
desc = "Click this ability to attack."
- check_flags = AB_CHECK_CONSCIOUS
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
cooldown_time = 5 SECONDS
text_cooldown = TRUE
click_to_activate = TRUE
diff --git a/code/datums/actions/mobs/small_sprite.dm b/code/datums/actions/mobs/small_sprite.dm
deleted file mode 100644
index 335d6c9aff39..000000000000
--- a/code/datums/actions/mobs/small_sprite.dm
+++ /dev/null
@@ -1,57 +0,0 @@
-//Small sprites
-/datum/action/small_sprite
- name = "Toggle Giant Sprite"
- desc = "Others will always see you as giant."
- button_icon = 'icons/mob/actions/actions_xeno.dmi'
- button_icon_state = "smallqueen"
- background_icon_state = "bg_alien"
- overlay_icon_state = "bg_alien_border"
- var/small = FALSE
- var/small_icon
- var/small_icon_state
-
-/datum/action/small_sprite/queen
- small_icon = 'icons/mob/nonhuman-player/alien.dmi'
- small_icon_state = "alienq"
-
-/datum/action/small_sprite/megafauna
- button_icon = 'icons/mob/actions/actions_xeno.dmi'
- small_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
-
-/datum/action/small_sprite/megafauna/drake
- small_icon_state = "ash_whelp"
-
-/datum/action/small_sprite/megafauna/colossus
- small_icon_state = "basilisk"
-
-/datum/action/small_sprite/megafauna/bubblegum
- small_icon_state = "goliath2"
-
-/datum/action/small_sprite/megafauna/legion
- small_icon_state = "mega_legion"
-
-/datum/action/small_sprite/mega_arachnid
- small_icon = 'icons/mob/simple/jungle/arachnid.dmi'
- small_icon_state = "arachnid_mini"
- background_icon_state = "bg_demon"
- overlay_icon_state = "bg_demon_border"
-
-
-/datum/action/small_sprite/space_dragon
- small_icon = 'icons/mob/simple/carp.dmi'
- small_icon_state = "carp"
- button_icon = 'icons/mob/simple/carp.dmi'
- button_icon_state = "carp"
-
-/datum/action/small_sprite/Trigger(trigger_flags)
- ..()
- if(!small)
- var/image/I = image(icon = small_icon, icon_state = small_icon_state, loc = owner)
- I.override = TRUE
- I.pixel_x -= owner.pixel_x
- I.pixel_y -= owner.pixel_y
- owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic, "smallsprite", I, AA_TARGET_SEE_APPEARANCE | AA_MATCH_TARGET_OVERLAYS)
- small = TRUE
- else
- owner.remove_alt_appearance("smallsprite")
- small = FALSE
diff --git a/code/datums/actions/mobs/sneak.dm b/code/datums/actions/mobs/sneak.dm
index 13ae5a784b8c..4e254daa8684 100644
--- a/code/datums/actions/mobs/sneak.dm
+++ b/code/datums/actions/mobs/sneak.dm
@@ -5,7 +5,9 @@
button_icon_state = "sniper_zoom"
background_icon_state = "bg_alien"
overlay_icon_state = "bg_alien_border"
- check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED | AB_CHECK_INCAPACITATED
+ cooldown_time = 0.5 SECONDS
+ melee_cooldown_time = 0 SECONDS
+ click_to_activate = FALSE
/// The alpha we go to when sneaking.
var/sneak_alpha = 75
diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm
index 1c7a778ae229..8795f9ef5317 100644
--- a/code/datums/ai/_ai_controller.dm
+++ b/code/datums/ai/_ai_controller.dm
@@ -346,6 +346,15 @@ multiple modular subtrees with behaviors
minimum_distance = iter_behavior.required_distance
return minimum_distance
+/// Returns true if we have a blackboard key with the provided key and it is not qdeleting
+/datum/ai_controller/proc/blackboard_key_exists(key)
+ var/datum/key_value = blackboard[key]
+ if (isdatum(key_value))
+ return !QDELETED(key_value)
+ if (islist(key_value))
+ return length(key_value) > 0
+ return !!key_value
+
/**
* Used to manage references to datum by AI controllers
*
@@ -611,6 +620,7 @@ multiple modular subtrees with behaviors
// We found the value that's been deleted, it was an assoc value. Clear it out entirely
else if(associated_value == source)
next_to_clear -= inner_value
+ SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value))
index += 1
diff --git a/code/datums/ai/babies/babies_behaviors.dm b/code/datums/ai/babies/babies_behaviors.dm
index aeb295ff2204..daaa3b407b75 100644
--- a/code/datums/ai/babies/babies_behaviors.dm
+++ b/code/datums/ai/babies/babies_behaviors.dm
@@ -2,74 +2,77 @@
* Find a compatible, living partner, if we're also alone.
*/
/datum/ai_behavior/find_partner
- action_cooldown = 40 SECONDS
-
/// Range to look.
var/range = 7
-
/// Maximum number of children
var/max_children = 3
/datum/ai_behavior/find_partner/perform(seconds_per_tick, datum/ai_controller/controller, target_key, partner_types_key, child_types_key)
. = ..()
-
+ max_children = controller.blackboard[BB_MAX_CHILDREN] || max_children
var/mob/pawn_mob = controller.pawn
var/list/partner_types = controller.blackboard[partner_types_key]
var/list/child_types = controller.blackboard[child_types_key]
+ var/mob/living/living_pawn = controller.pawn
- var/mob/living/partner
var/children = 0
- for(var/mob/other in oview(range, pawn_mob))
+ for(var/mob/living/other in oview(range, pawn_mob))
+ if(!pawn_mob.faction_check_atom(other))
+ finish_action(controller, FALSE)
+ return
+
+ if(children >= max_children)
+ finish_action(controller, FALSE)
+ return
+
if(other.stat != CONSCIOUS) //Check if it's conscious FIRST.
continue
- var/is_child = is_type_in_list(other, child_types)
- if(is_child) //Check for children SECOND.
+
+ if(is_type_in_list(other, child_types)) //Check for children SECOND.
children++
- else if(is_type_in_list(other, partner_types))
- if(other.ckey)
- continue
- else if(!is_child && other.gender == MALE && !(other.flags_1 & HOLOGRAM_1)) //Better safe than sorry ;_;
- partner = other
+ continue
- //shyness check. we're not shy in front of things that share a faction with us.
- else if(isliving(other) && !pawn_mob.faction_check_mob(other))
- finish_action(controller, FALSE)
- return
+ if(!is_type_in_list(other, partner_types) || !HAS_TRAIT(other, TRAIT_MOB_BREEDER))
+ continue
+
+ if(other.ckey)
+ continue
+
+ if(other.gender != living_pawn.gender && !(other.flags_1 & HOLOGRAM_1)) //Better safe than sorry ;_;
+ controller.set_blackboard_key(target_key, other)
+ finish_action(controller, TRUE)
- if(partner && children < max_children)
- controller.set_blackboard_key(target_key, partner)
+ finish_action(controller, FALSE)
- finish_action(controller, TRUE)
/**
* Reproduce.
*/
/datum/ai_behavior/make_babies
- action_cooldown = 40 SECONDS
- behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/make_babies/setup(datum/ai_controller/controller, target_key, child_types_key)
+ . = ..()
var/atom/target = controller.blackboard[target_key]
if(!target)
return FALSE
set_movement_target(controller, target)
- return TRUE
/datum/ai_behavior/make_babies/perform(seconds_per_tick, datum/ai_controller/controller, target_key, child_types_key)
. = ..()
var/mob/target = controller.blackboard[target_key]
- if(!target || target.stat != CONSCIOUS)
+ if(QDELETED(target) || target.stat != CONSCIOUS)
finish_action(controller, FALSE, target_key)
return
-
- var/child_type = pick_weight(controller.blackboard[child_types_key])
- var/turf/turf_loc = get_turf(controller.pawn.loc)
- if(turf_loc)
- new child_type(turf_loc)
-
+ var/mob/living/basic/living_pawn = controller.pawn
+ living_pawn.set_combat_mode(FALSE)
+ living_pawn.melee_attack(target)
finish_action(controller, TRUE, target_key)
/datum/ai_behavior/make_babies/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
-
controller.clear_blackboard_key(target_key)
+ if(!succeeded)
+ return
+ var/mob/living/living_pawn = controller.pawn
+ living_pawn.istate = initial(living_pawn.istate)
diff --git a/code/datums/ai/babies/babies_subtrees.dm b/code/datums/ai/babies/babies_subtrees.dm
index 0c71b6d757b0..8310cd955733 100644
--- a/code/datums/ai/babies/babies_subtrees.dm
+++ b/code/datums/ai/babies/babies_subtrees.dm
@@ -3,11 +3,19 @@
*/
/datum/ai_planning_subtree/make_babies
var/chance = 5
+ operational_datums = list(/datum/component/breed)
/datum/ai_planning_subtree/make_babies/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if(controller.pawn.gender != FEMALE || !SPT_PROB(chance, seconds_per_tick))
+ if(!SPT_PROB(chance, seconds_per_tick))
+ return
+
+ if(controller.blackboard_key_exists(BB_BABIES_TARGET))
+ controller.queue_behavior(/datum/ai_behavior/make_babies, BB_BABIES_TARGET, BB_BABIES_CHILD_TYPES)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if(controller.pawn.gender == FEMALE || !controller.blackboard[BB_BREED_READY])
return
var/partner_types = controller.blackboard[BB_BABIES_PARTNER_TYPES]
@@ -20,13 +28,5 @@
if(is_type_in_list(controller.pawn, baby_types))
return
- var/atom/target = controller.blackboard[BB_BABIES_TARGET]
-
// Find target
- if(QDELETED(target))
- controller.queue_behavior(/datum/ai_behavior/find_partner, BB_BABIES_TARGET, BB_BABIES_PARTNER_TYPES, BB_BABIES_CHILD_TYPES)
- return
-
- // Do target
- controller.queue_behavior(/datum/ai_behavior/make_babies, BB_BABIES_TARGET, BB_BABIES_CHILD_TYPES)
- return SUBTREE_RETURN_FINISH_PLANNING
+ controller.queue_behavior(/datum/ai_behavior/find_partner, BB_BABIES_TARGET, BB_BABIES_PARTNER_TYPES, BB_BABIES_CHILD_TYPES)
diff --git a/code/datums/ai/basic_mobs/base_basic_controller.dm b/code/datums/ai/basic_mobs/base_basic_controller.dm
index b4f7c27c2f91..cd025b28bcb2 100644
--- a/code/datums/ai/basic_mobs/base_basic_controller.dm
+++ b/code/datums/ai/basic_mobs/base_basic_controller.dm
@@ -15,10 +15,16 @@
/datum/ai_controller/basic_controller/able_to_run()
. = ..()
- if(isliving(pawn))
- var/mob/living/living_pawn = pawn
- if(IS_DEAD_OR_INCAP(living_pawn))
- return FALSE
+ if(!isliving(pawn))
+ return
+ var/mob/living/living_pawn = pawn
+ var/incap_flags = NONE
+ if (ai_traits & CAN_ACT_IN_STASIS)
+ incap_flags |= IGNORE_STASIS
+ if(!(ai_traits & CAN_ACT_WHILE_DEAD) && (living_pawn.incapacitated(incap_flags) || living_pawn.stat))
+ return FALSE
+ if(ai_traits & PAUSE_DURING_DO_AFTER && LAZYLEN(living_pawn.do_afters))
+ return FALSE
/datum/ai_controller/basic_controller/proc/update_speed(mob/living/basic/basic_mob)
SIGNAL_HANDLER
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
index cfd53164bb21..15ddc7b264d7 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
@@ -1,11 +1,12 @@
/datum/ai_behavior/basic_melee_attack
- action_cooldown = 2 SECONDS
- behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ ///do we finish this action after hitting once?
+ var/terminate_after_action = FALSE
-/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
- if(isnull(targetting_datum))
+ if(!controller.blackboard[targeting_strategy_key])
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
//Hiding location is priority
@@ -15,18 +16,23 @@
set_movement_target(controller, target)
-/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ if (isliving(controller.pawn))
+ var/mob/living/pawn = controller.pawn
+ if (world.time < pawn.next_move)
+ return
+
. = ..()
var/mob/living/basic/basic_mob = controller.pawn
- //targetting datum will kill the action if not real anymore
+ //targeting strategy will kill the action if not real anymore
var/atom/target = controller.blackboard[target_key]
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
- if(!targetting_datum.can_attack(basic_mob, target))
+ if(!targeting_strategy.can_attack(basic_mob, target))
finish_action(controller, FALSE, target_key)
return
- var/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
+ var/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
controller.set_blackboard_key(hiding_location_key, hiding_target)
@@ -35,61 +41,111 @@
else
basic_mob.melee_attack(target)
+ if(terminate_after_action)
+ finish_action(controller, TRUE, target_key)
-/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(target_key)
-/datum/ai_behavior/basic_melee_attack/average_speed
- action_cooldown = 1 SECONDS
+/datum/ai_behavior/basic_melee_attack/interact_once
+ terminate_after_action = TRUE
+
+/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
/datum/ai_behavior/basic_ranged_attack
action_cooldown = 0.6 SECONDS
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 3
- /// How many shots to fire
- var/shots = 1
- /// The interval between individual shots in a burst
- var/burst_interval = 0.2 SECONDS
+ /// range we will try chasing the target before giving up
+ var/chase_range = 9
+ ///do we care about avoiding friendly fire?
+ var/avoid_friendly_fire = FALSE
-/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
-/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
- . = ..()
+/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/basic/basic_mob = controller.pawn
- //targetting datum will kill the action if not real anymore
+ //targeting strategy will kill the action if not real anymore
var/atom/target = controller.blackboard[target_key]
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
- if(!targetting_datum.can_attack(basic_mob, target))
+ if(!targeting_strategy.can_attack(basic_mob, target, chase_range))
finish_action(controller, FALSE, target_key)
return
- var/atom/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
+ var/atom/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
var/atom/final_target = hiding_target ? hiding_target : target
if(!can_see(basic_mob, final_target, required_distance))
- finish_action(controller, FALSE, target_key)
return
- controller.set_blackboard_key(hiding_location_key, hiding_target)
+ if(avoid_friendly_fire && check_friendly_in_path(basic_mob, target, targeting_strategy))
+ adjust_position(basic_mob, target)
+ return ..()
- if(shots>1)
- var/atom/burst_target = final_target
- var/datum/callback/callback = CALLBACK(basic_mob, TYPE_PROC_REF(/mob/living/basic,RangedAttack), burst_target)
- for(var/i in 2 to shots)
- addtimer(callback, (i - 1) * burst_interval)
- callback.Invoke()
- else
- basic_mob.RangedAttack(final_target)
+ controller.set_blackboard_key(hiding_location_key, hiding_target)
+ basic_mob.RangedAttack(final_target)
+ return ..() //only start the cooldown when the shot is shot
-/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/basic_ranged_attack/proc/check_friendly_in_path(mob/living/source, atom/target, datum/targeting_strategy/targeting_strategy)
+ var/list/turfs_list = calculate_trajectory(source, target)
+ for(var/turf/possible_turf as anything in turfs_list)
+
+ for(var/mob/living/potential_friend in possible_turf)
+ if(!targeting_strategy.can_attack(source, potential_friend))
+ return TRUE
+
+ return FALSE
+
+/datum/ai_behavior/basic_ranged_attack/proc/adjust_position(mob/living/living_pawn, atom/target)
+ var/turf/our_turf = get_turf(living_pawn)
+ var/list/possible_turfs = list()
+
+ for(var/direction in GLOB.alldirs)
+ var/turf/target_turf = get_step(our_turf, direction)
+ if(isnull(target_turf))
+ continue
+ if(target_turf.is_blocked_turf() || get_dist(target_turf, target) > get_dist(living_pawn, target))
+ continue
+ possible_turfs += target_turf
+
+ if(!length(possible_turfs))
+ return
+ var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target)
+ step(living_pawn, get_dir(living_pawn, picked_turf))
+
+/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target)
+ var/list/turf_list = get_line(source, target)
+ var/list_length = length(turf_list) - 1
+ for(var/i in 1 to list_length)
+ var/turf/current_turf = turf_list[i]
+ var/turf/next_turf = turf_list[i+1]
+ var/direction_to_turf = get_dir(current_turf, next_turf)
+ if(!ISDIAGONALDIR(direction_to_turf))
+ continue
+
+ for(var/cardinal_direction in GLOB.cardinals)
+ if(cardinal_direction & direction_to_turf)
+ turf_list += get_step(current_turf, cardinal_direction)
+
+ turf_list -= get_turf(source)
+ turf_list -= get_turf(target)
+
+ return turf_list
+
+/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire
+ avoid_friendly_fire = TRUE
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm
similarity index 100%
rename from code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targetting.dm
rename to code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
index 7674c48e54ba..2e922d62f09a 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
@@ -4,7 +4,7 @@
action_cooldown = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How far do we try to run? Further makes for smoother running, but potentially weirder pathfinding
- var/run_distance = 9
+ var/run_distance = DEFAULT_BASIC_FLEE_DISTANCE
/// Clear target if we finish the action unsuccessfully
var/clear_failed_targets = TRUE
@@ -12,20 +12,22 @@
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
+ run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || initial(run_distance)
if(!plot_path_away_from(controller, target))
return FALSE
return ..()
/datum/ai_behavior/run_away_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
. = ..()
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ return
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
- var/escaped = QDELETED(target) || !can_see(controller.pawn, target, run_distance) // If we can't see it we got away
- if (escaped)
+ if (QDELETED(target) || !can_see(controller.pawn, target, run_distance))
finish_action(controller, succeeded = TRUE, target_key = target_key, hiding_location_key = hiding_location_key)
return
if (get_dist(controller.pawn, controller.current_movement_target) > required_distance)
- return
- if(plot_path_away_from(controller, target))
+ return // Still heading over
+ if (plot_path_away_from(controller, target))
return
finish_action(controller, succeeded = FALSE, target_key = target_key, hiding_location_key = hiding_location_key)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm
new file mode 100644
index 000000000000..207df4424577
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/set_travel_destination.dm
@@ -0,0 +1,13 @@
+/datum/ai_behavior/set_travel_destination
+
+/datum/ai_behavior/set_travel_destination/perform(seconds_per_tick, datum/ai_controller/controller, target_key, location_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ controller.set_blackboard_key(location_key, target)
+
+ finish_action(controller, TRUE, target_key)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
index ca1efe6f169f..a39200d0cc16 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_ability.dm
@@ -11,7 +11,8 @@
finish_action(controller, FALSE, ability_key, target_key)
return
var/mob/pawn = controller.pawn
- var/result = ability.InterceptClickOn(pawn, null, target)
+ pawn.face_atom(target)
+ var/result = ability.Trigger(target = target)
finish_action(controller, result, ability_key, target_key)
/datum/ai_behavior/targeted_mob_ability/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
@@ -19,12 +20,6 @@
var/atom/target = controller.blackboard[target_key]
if (QDELETED(target))
controller.clear_blackboard_key(target_key)
- return
- if (!isliving(target))
- return
- var/mob/living/living_target = target
- if(living_target.stat >= UNCONSCIOUS)
- controller.clear_blackboard_key(target_key)
/**
* # Try Mob Ability and plan execute
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
similarity index 65%
rename from code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
rename to code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
index 9d7587c712b3..28f89bcf2974 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
@@ -2,27 +2,31 @@
action_cooldown = 2 SECONDS
/// How far can we see stuff?
var/vision_range = 9
+ /// Blackboard key for aggro range, uses vision range if not specified
+ var/aggro_range_key = BB_AGGRO_RANGE
/// Static typecache list of potentially dangerous objs
var/static/list/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/vehicle/sealed/mecha))
-/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
var/mob/living/living_mob = controller.pawn
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
- if(!targetting_datum)
+ if(!targeting_strategy)
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
var/atom/current_target = controller.blackboard[target_key]
- if (targetting_datum.can_attack(living_mob, current_target, vision_range))
+ if (targeting_strategy.can_attack(living_mob, current_target, vision_range))
finish_action(controller, succeeded = FALSE)
return
+ var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+
controller.clear_blackboard_key(target_key)
- var/list/potential_targets = hearers(vision_range, controller.pawn) - living_mob //Remove self, so we don't suicide
+ var/list/potential_targets = hearers(aggro_range, controller.pawn) - living_mob //Remove self, so we don't suicide
- for(var/HM in typecache_filter_list(range(vision_range, living_mob), hostile_machines)) //Can we see any hostile machines?
- if(can_see(living_mob, HM, vision_range))
+ for(var/HM in typecache_filter_list(range(aggro_range, living_mob), hostile_machines)) //Can we see any hostile machines?
+ if(can_see(living_mob, HM, aggro_range))
potential_targets += HM
if(!potential_targets.len)
@@ -32,7 +36,7 @@
var/list/filtered_targets = list()
for(var/atom/pot_target in potential_targets)
- if(targetting_datum.can_attack(living_mob, pot_target))//Can we attack it?
+ if(targeting_strategy.can_attack(living_mob, pot_target))//Can we attack it?
filtered_targets += pot_target
continue
@@ -43,7 +47,7 @@
var/atom/target = pick_final_target(controller, filtered_targets)
controller.set_blackboard_key(target_key, target)
- var/atom/potential_hiding_location = targetting_datum.find_hidden_mobs(living_mob, target)
+ var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
index bbc1a43e3224..55f6ef4c4c00 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
@@ -6,6 +6,8 @@
/datum/ai_behavior/travel_towards
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// If true we will get rid of our target on completion
+ var/clear_target = FALSE
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
. = ..()
@@ -16,7 +18,15 @@
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
. = ..()
- finish_action(controller, TRUE)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ . = ..()
+ if (clear_target)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/travel_towards/stop_on_arrival
+ clear_target = TRUE
/**
* # Travel Towards Atom
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/unbuckle_mob.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/unbuckle_mob.dm
new file mode 100644
index 000000000000..d0fb7e3e8c70
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/unbuckle_mob.dm
@@ -0,0 +1,14 @@
+/datum/ai_behavior/unbuckle_mob
+
+/datum/ai_behavior/unbuckle_mob/perform(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+
+ var/mob/living/living_pawn = controller.pawn
+ var/atom/movable/buckled_too = living_pawn.buckled
+
+ if(isnull(buckled_too))
+ finish_action(controller, FALSE)
+ return
+
+ buckled_too.unbuckle_mob(living_pawn)
+ finish_action(controller, TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm
index 5ec5f24066cd..e162cc612990 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/ventcrawling.dm
@@ -18,7 +18,7 @@
. = ..()
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent = controller.blackboard[target_key] || controller.blackboard[BB_ENTRY_VENT_TARGET]
var/mob/living/cached_pawn = controller.pawn
- if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING) || !controller.blackboard[BB_CURRENTLY_TARGETTING_VENT] || !is_vent_valid(entry_vent))
+ if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING) || !controller.blackboard[BB_CURRENTLY_TARGETING_VENT] || !is_vent_valid(entry_vent))
return
if(!cached_pawn.can_enter_vent(entry_vent, provide_feedback = FALSE)) // we're an AI we scoff at feedback
@@ -30,7 +30,7 @@
finish_action(controller, FALSE, target_key)
return
- controller.set_blackboard_key(BB_CURRENTLY_TARGETTING_VENT, FALSE) // must be done here because we have a do_after sleep in handle_ventcrawl unfortunately and double dipping could lead to erroneous suicide pill calls.
+ controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, FALSE) // must be done here because we have a do_after sleep in handle_ventcrawl unfortunately and double dipping could lead to erroneous suicide pill calls.
cached_pawn.handle_ventcrawl(entry_vent)
if(!HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING)) //something failed and we ARE NOT IN THE VENT even though the earlier check said we were good to go! odd.
finish_action(controller, FALSE, target_key)
@@ -134,4 +134,5 @@
controller.clear_blackboard_key(target_key)
controller.clear_blackboard_key(BB_ENTRY_VENT_TARGET)
controller.clear_blackboard_key(BB_EXIT_VENT_TARGET)
- controller.set_blackboard_key(BB_CURRENTLY_TARGETTING_VENT, FALSE) // just in case
+ controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, FALSE) // just in case
+
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targeting.dm
new file mode 100644
index 000000000000..46037fdc076e
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/wounded_targeting.dm
@@ -0,0 +1,11 @@
+/// Picks targets based on which one has the lowest health
+/datum/ai_behavior/find_potential_targets/most_wounded
+
+/datum/ai_behavior/find_potential_targets/most_wounded/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
+ var/list/living_targets = list()
+ for(var/mob/living/living_target in filtered_targets)
+ living_targets += filtered_targets
+ if(living_targets.len)
+ sortTim(living_targets, GLOBAL_PROC_REF(cmp_mob_health))
+ return pop(living_targets)
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/attack_adjacent_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/attack_adjacent_target.dm
new file mode 100644
index 000000000000..411690043546
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/attack_adjacent_target.dm
@@ -0,0 +1,33 @@
+/// Attack something which is already adjacent to us, without ending planning
+/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic
+ melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/opportunistic
+ end_planning = FALSE
+
+/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if(QDELETED(target) || !controller.pawn.Adjacent(target))
+ return
+ if (isliving(controller.pawn))
+ var/mob/living/pawn = controller.pawn
+ if (LAZYLEN(pawn.do_afters))
+ return
+ controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+
+/// Attack something which is already adjacent to us without moving
+/datum/ai_behavior/basic_melee_attack/opportunistic
+ action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/basic_melee_attack/opportunistic/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ if (!controller.blackboard_key_exists(targeting_strategy_key))
+ CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
+ return controller.blackboard_key_exists(target_key)
+
+/datum/ai_behavior/basic_melee_attack/opportunistic/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ var/atom/movable/atom_pawn = controller.pawn
+ if(!atom_pawn.CanReach(controller.blackboard[target_key]))
+ finish_action(controller, TRUE, target_key) // Don't clear target
+ return FALSE
+ . = ..()
+ finish_action(controller, TRUE, target_key) // Try doing something else
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
index 9f1053f29d3f..7059bec93fe2 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm
@@ -74,7 +74,7 @@
return FALSE
if (basic_mob.see_invisible < object.invisibility)
return FALSE
- var/list/whitelist = basic_mob.ai_controller.blackboard[BB_OBSTACLE_TARGETTING_WHITELIST]
+ var/list/whitelist = basic_mob.ai_controller.blackboard[BB_OBSTACLE_TARGETING_WHITELIST]
if(whitelist && !is_type_in_typecache(object, whitelist))
return FALSE
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
new file mode 100644
index 000000000000..59ff88b4879b
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm
@@ -0,0 +1,50 @@
+#define REINFORCEMENTS_COOLDOWN (30 SECONDS)
+
+/// Calls all nearby mobs that share a faction to give backup in combat
+/datum/ai_planning_subtree/call_reinforcements
+ /// Blackboard key containing something to say when calling reinforcements (takes precedence over emotes)
+ var/say_key = BB_REINFORCEMENTS_SAY
+ /// Blackboard key containing an emote to perform when calling reinforcements
+ var/emote_key = BB_REINFORCEMENTS_EMOTE
+ /// Reinforcement-calling behavior to use
+ var/call_type = /datum/ai_behavior/call_reinforcements
+
+/datum/ai_planning_subtree/call_reinforcements/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if (!decide_to_call(controller) || controller.blackboard[BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN] > world.time)
+ return
+
+ var/call_say = controller.blackboard[BB_REINFORCEMENTS_SAY]
+ var/call_emote = controller.blackboard[BB_REINFORCEMENTS_EMOTE]
+
+ if(!isnull(call_say))
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, call_say)
+ else if(!isnull(call_emote))
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, call_emote)
+ else
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, "cries for help!")
+
+ controller.queue_behavior(call_type)
+
+/// Decides when to call reinforcements, can be overridden for alternate behavior
+/datum/ai_planning_subtree/call_reinforcements/proc/decide_to_call(datum/ai_controller/controller)
+ return controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && istype(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], /mob)
+
+/// Call out to all mobs in the specified range for help
+/datum/ai_behavior/call_reinforcements
+ /// Range to call reinforcements from
+ var/reinforcements_range = 15
+
+/datum/ai_behavior/call_reinforcements/perform(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+
+ var/mob/pawn_mob = controller.pawn
+ for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
+ if(pawn_mob.faction_check_atom(other_mob) && !isnull(other_mob.ai_controller))
+ // Add our current target to their retaliate list so that they'll attack our aggressor
+ other_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
+
+ controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + REINFORCEMENTS_COOLDOWN)
+
+#undef REINFORCEMENTS_COOLDOWN
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
new file mode 100644
index 000000000000..4d5319bca86e
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
@@ -0,0 +1,69 @@
+/// Add or remove people to our retaliation shitlist just on an arbitrary whim
+/datum/ai_planning_subtree/capricious_retaliate
+ /// Blackboard key which tells us how to select valid targets
+ var/targeting_strategy_key = BB_TARGETING_STRATEGY
+ /// Whether we should skip checking faction for our decision
+ var/ignore_faction = TRUE
+
+/datum/ai_planning_subtree/capricious_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ controller.queue_behavior(/datum/ai_behavior/capricious_retaliate, targeting_strategy_key, ignore_faction)
+
+/// Add or remove people to our retaliation shitlist just on an arbitrary whim
+/datum/ai_behavior/capricious_retaliate
+ action_cooldown = 1 SECONDS
+
+/datum/ai_behavior/capricious_retaliate/perform(seconds_per_tick, datum/ai_controller/controller, targeting_strategy_key, ignore_faction)
+ . = ..()
+ var/atom/pawn = controller.pawn
+ if (controller.blackboard_key_exists(BB_BASIC_MOB_RETALIATE_LIST))
+ var/deaggro_chance = controller.blackboard[BB_RANDOM_DEAGGRO_CHANCE] || 10
+ if (!SPT_PROB(deaggro_chance, seconds_per_tick))
+ finish_action(controller, TRUE, ignore_faction) // "true" here means "don't clear our ignoring factions status"
+ return
+ pawn.visible_message(span_notice("[pawn] calms down.")) // We can blackboard key this if anyone else actually wants to customise it
+ controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST)
+ finish_action(controller, FALSE, ignore_faction)
+ controller.CancelActions() // Otherwise they will try and get one last kick in
+ return
+
+ var/aggro_chance = controller.blackboard[BB_RANDOM_AGGRO_CHANCE] || 0.5
+ if (!SPT_PROB(aggro_chance, seconds_per_tick))
+ finish_action(controller, FALSE, ignore_faction)
+ return
+
+ var/aggro_range = controller.blackboard[BB_AGGRO_RANGE] || 9
+ var/list/potential_targets = hearers(aggro_range, get_turf(pawn)) - pawn
+ if (!length(potential_targets))
+ failed_targeting(controller, pawn, ignore_faction)
+ return
+
+ var/datum/targeting_strategy/target_helper = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+
+ var/mob/living/final_target = null
+ if (ignore_faction)
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
+ while (isnull(final_target) && length(potential_targets))
+ var/mob/living/test_target = pick_n_take(potential_targets)
+ if (target_helper.can_attack(pawn, test_target, vision_range = aggro_range))
+ final_target = test_target
+
+ if (isnull(final_target))
+ failed_targeting(controller, pawn, ignore_faction)
+ return
+
+ controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target)
+ pawn.visible_message(span_warning("[pawn] glares grumpily at [final_target]!"))
+ finish_action(controller, TRUE, ignore_faction)
+
+/// Called if we try but fail to target something
+/datum/ai_behavior/capricious_retaliate/proc/failed_targeting(datum/ai_controller/controller, atom/pawn, ignore_faction)
+ finish_action(controller, FALSE, ignore_faction)
+ pawn.visible_message(span_notice("[pawn] grumbles.")) // We're pissed off but with no outlet to vent our frustration upon
+
+/datum/ai_behavior/capricious_retaliate/finish_action(datum/ai_controller/controller, succeeded, ignore_faction)
+ . = ..()
+ if (succeeded || !ignore_faction)
+ return
+ var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm b/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
index 6827f3eee993..bad349030f1b 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/climb_tree.dm
@@ -1,4 +1,5 @@
/datum/ai_planning_subtree/climb_trees
+ operational_datums = list(/datum/component/tree_climber)
///chance to climb a tree
var/climb_chance = 35
@@ -7,11 +8,8 @@
if(!SPT_PROB(climb_chance, seconds_per_tick))
return
- var/obj/structure/flora/tree/target = controller.blackboard[BB_CLIMBED_TREE]
+ if(controller.blackboard_key_exists(BB_CLIMBED_TREE))
+ controller.queue_behavior(/datum/ai_behavior/climb_tree, BB_CLIMBED_TREE)
+ return SUBTREE_RETURN_FINISH_PLANNING
- if(QDELETED(target))
- controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_tree, BB_CLIMBED_TREE, /obj/structure/flora/tree)
- return
-
- controller.queue_behavior(/datum/ai_behavior/climb_tree, BB_CLIMBED_TREE)
- return SUBTREE_RETURN_FINISH_PLANNING
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_tree, BB_CLIMBED_TREE, /obj/structure/flora/tree)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
index 8d20df7b9c79..b02ec8eaa85a 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
@@ -3,8 +3,7 @@
/datum/ai_planning_subtree/find_food/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
- if(!QDELETED(target))
+ if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
// Busy with something
return
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/find_paper_and_write.dm b/code/datums/ai/basic_mobs/basic_subtrees/find_paper_and_write.dm
index 900d13e0a046..0d2d0c3e3b44 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/find_paper_and_write.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/find_paper_and_write.dm
@@ -1,10 +1,9 @@
/datum/ai_planning_subtree/find_paper_and_write
/datum/ai_planning_subtree/find_paper_and_write/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- var/obj/item/inhand_paper = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
var/mob/living/basic/wizard = controller.pawn
- if(!QDELETED(inhand_paper))
+ if(controller.blackboard_key_exists(BB_SIMPLE_CARRY_ITEM))
controller.queue_behavior(/datum/ai_behavior/write_on_paper, BB_SIMPLE_CARRY_ITEM, BB_WRITING_LIST)
return SUBTREE_RETURN_FINISH_PLANNING
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
index 6ae7885618b9..4a2f5b476c75 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
@@ -9,16 +9,18 @@
/datum/ai_planning_subtree/flee_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ var/atom/flee_from = controller.blackboard[target_key]
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING] || QDELETED(flee_from))
return
- var/atom/target = controller.blackboard[target_key]
- if(QDELETED(target))
+ var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
+ if (get_dist(controller.pawn, flee_from) >= flee_distance)
return
+
controller.queue_behavior(flee_behaviour, target_key, hiding_place_key)
return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here.
/// Try to escape from your current target, without performing any other actions.
-/// Reads from some fleeing-specific targetting keys rather than the current mob target.
+/// Reads from some fleeing-specific targeting keys rather than the current mob target.
/datum/ai_planning_subtree/flee_target/from_flee_key
target_key = BB_BASIC_MOB_FLEE_TARGET
hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
index b80a28836a06..2a85e9e902b2 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm
@@ -3,11 +3,13 @@
/// Blackboard key holding atom we want to stay away from
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// How close will we allow our target to get?
- var/minimum_distance = 3
+ var/minimum_distance = 4
/// How far away will we allow our target to get?
var/maximum_distance = 6
/// How far do we look for our target?
var/view_distance = 10
+ /// the run away behavior we will use
+ var/run_away_behavior = /datum/ai_behavior/step_away
/datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
@@ -16,12 +18,15 @@
return // Don't run away from cucumbers, they're not snakes
var/range = get_dist(controller.pawn, target)
if (range < minimum_distance)
- controller.queue_behavior(/datum/ai_behavior/step_away, target_key)
+ controller.queue_behavior(run_away_behavior, target_key, minimum_distance)
return
if (range > maximum_distance)
controller.queue_behavior(/datum/ai_behavior/pursue_to_range, target_key, maximum_distance)
return
+/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance
+ run_away_behavior = /datum/ai_behavior/cover_minimum_distance
+
/// Take one step away
/datum/ai_behavior/step_away
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
@@ -80,3 +85,32 @@
if (!QDELETED(current_target) && get_dist(controller.pawn, current_target) > range)
return
finish_action(controller, succeeded = TRUE)
+
+///instead of taking a single step, we cover the entire distance
+/datum/ai_behavior/cover_minimum_distance
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ required_distance = 0
+ action_cooldown = 0.2 SECONDS
+
+/datum/ai_behavior/cover_minimum_distance/setup(datum/ai_controller/controller, target_key, minimum_distance)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ var/required_distance = minimum_distance - get_dist(controller.pawn, target) //the distance we need to move
+ var/distance = 0
+ var/turf/chosen_turf
+ for(var/turf/open/potential_turf in oview(required_distance, controller.pawn))
+ var/new_distance_from_target = get_dist(potential_turf, target)
+ if(potential_turf.is_blocked_turf())
+ continue
+ if(new_distance_from_target > distance)
+ chosen_turf = potential_turf
+ distance = new_distance_from_target
+ if(isnull(chosen_turf))
+ return FALSE
+ set_movement_target(controller, target = chosen_turf)
+
+/datum/ai_behavior/cover_minimum_distance/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ finish_action(controller, succeeded = TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
new file mode 100644
index 000000000000..3c03702b6994
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm
@@ -0,0 +1,73 @@
+//behavior to find mineable mineral walls
+
+/datum/ai_planning_subtree/mine_walls
+ var/find_wall_behavior = /datum/ai_behavior/find_mineral_wall
+
+/datum/ai_planning_subtree/mine_walls/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard_key_exists(BB_TARGET_MINERAL_WALL))
+ controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ controller.queue_behavior(find_wall_behavior, BB_TARGET_MINERAL_WALL)
+
+/datum/ai_behavior/mine_wall
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ action_cooldown = 15 SECONDS
+
+/datum/ai_behavior/mine_wall/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/turf/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/mine_wall/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/turf/closed/mineral/target = controller.blackboard[target_key]
+ var/is_gibtonite_turf = istype(target, /turf/closed/mineral/gibtonite)
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+ living_pawn.melee_attack(target)
+ if(is_gibtonite_turf)
+ living_pawn.manual_emote("sighs...") //accept whats about to happen to us
+
+ finish_action(controller, TRUE, target_key)
+ return
+
+/datum/ai_behavior/mine_wall/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/find_mineral_wall
+
+/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
+ . = ..()
+ var/mob/living_pawn = controller.pawn
+
+ for(var/turf/closed/mineral/potential_wall in oview(9, living_pawn))
+ if(!check_if_mineable(controller, potential_wall)) //check if its surrounded by walls
+ continue
+ controller.set_blackboard_key(found_wall_key, potential_wall) //closest wall first!
+ finish_action(controller, TRUE)
+ return
+
+ finish_action(controller, FALSE)
+
+/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
+ var/mob/living/source = controller.pawn
+ var/direction_to_turf = get_dir(target_wall, source)
+ if(!ISDIAGONALDIR(direction_to_turf))
+ return TRUE
+ var/list/directions_to_check = list()
+ for(var/direction_check in GLOB.cardinals)
+ if(direction_check & direction_to_turf)
+ directions_to_check += direction_check
+
+ for(var/direction in directions_to_check)
+ var/turf/test_turf = get_step(target_wall, direction)
+ if(isnull(test_turf))
+ continue
+ if(!test_turf.is_blocked_turf(ignore_atoms = list(source)))
+ return TRUE
+ return FALSE
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/move_to_cardinal.dm b/code/datums/ai/basic_mobs/basic_subtrees/move_to_cardinal.dm
new file mode 100644
index 000000000000..c98878e0fd71
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/move_to_cardinal.dm
@@ -0,0 +1,71 @@
+/// Try to line up with a cardinal direction of your target
+/datum/ai_planning_subtree/move_to_cardinal
+ /// Behaviour to execute to line ourselves up
+ var/move_behaviour = /datum/ai_behavior/move_to_cardinal
+ /// Blackboard key in which to store selected target
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+/datum/ai_planning_subtree/move_to_cardinal/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if(!controller.blackboard_key_exists(target_key))
+ return
+ controller.queue_behavior(move_behaviour, target_key)
+
+/// Try to line up with a cardinal direction of your target
+/datum/ai_behavior/move_to_cardinal
+ required_distance = 0
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// How close to our target is too close?
+ var/minimum_distance = 1
+ /// How far away is too far?
+ var/maximum_distance = 9
+
+/datum/ai_behavior/move_to_cardinal/setup(datum/ai_controller/controller, target_key)
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ target_nearest_cardinal(controller, target)
+ return TRUE
+
+/// Set our movement target to the closest cardinal space to our target
+/datum/ai_behavior/move_to_cardinal/proc/target_nearest_cardinal(datum/ai_controller/controller, atom/target)
+ var/atom/move_target
+ var/closest = INFINITY
+
+ for (var/dir in GLOB.cardinals)
+ var/turf/cardinal_turf = get_ranged_target_turf(target, dir, minimum_distance)
+ if (cardinal_turf.is_blocked_turf())
+ continue
+ var/distance_to = get_dist(controller.pawn, cardinal_turf)
+ if (distance_to >= closest)
+ continue
+ closest = distance_to
+ move_target = cardinal_turf
+
+ if (isnull(move_target))
+ move_target = target
+ if (controller.current_movement_target == move_target)
+ return
+ set_movement_target(controller, move_target)
+
+/datum/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ var/atom/target = controller.blackboard[target_key]
+ if (QDELETED(target))
+ finish_action(controller = controller, succeeded = FALSE, target_key = target_key)
+ return
+ if (!(get_dir(controller.pawn, target) in GLOB.cardinals))
+ target_nearest_cardinal(controller, target)
+ return
+ var/distance_to_target = get_dist(controller.pawn, target)
+ if (distance_to_target < minimum_distance)
+ target_nearest_cardinal(controller, target)
+ return
+ if (distance_to_target > maximum_distance)
+ return
+ finish_action(controller = controller, succeeded = TRUE, target_key = target_key)
+ return
+
+/datum/ai_behavior/move_to_cardinal/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ if (!succeeded)
+ controller.clear_blackboard_key(target_key)
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/opportunistic_ventcrawler.dm b/code/datums/ai/basic_mobs/basic_subtrees/opportunistic_ventcrawler.dm
index 63a745305b20..240272d1ef48 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/opportunistic_ventcrawler.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/opportunistic_ventcrawler.dm
@@ -15,6 +15,6 @@
controller.queue_behavior(/datum/ai_behavior/travel_towards, BB_ENTRY_VENT_TARGET)
return
- controller.set_blackboard_key(BB_CURRENTLY_TARGETTING_VENT, TRUE)
+ controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, TRUE)
controller.queue_behavior(/datum/ai_behavior/crawl_through_vents, BB_ENTRY_VENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING // we are going into this vent... no distractions
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm
new file mode 100644
index 000000000000..2718ca630ff7
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/prepare_travel_to_destination.dm
@@ -0,0 +1,23 @@
+
+///Subtree that checks if we are on the target atom's tile, and sets it as a travel target if not
+///The target is taken from the blackboard. This one always requires a specific implementation.
+/datum/ai_planning_subtree/prepare_travel_to_destination
+ var/target_key
+ var/travel_destination_key = BB_TRAVEL_DESTINATION
+
+/datum/ai_planning_subtree/prepare_travel_to_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[target_key]
+
+ //Target is deleted, or we are already standing on it
+ if(QDELETED(target) || (isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc))
+ return
+
+ //Already set with this value, return
+ if(controller.blackboard[target_key] == controller.blackboard[travel_destination_key])
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/set_travel_destination, target_key, travel_destination_key)
+ return //continue planning regardless of success
+
+/datum/ai_planning_subtree/prepare_travel_to_destination/trader
+ target_key = BB_SHOP_SPOT
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
index 07fb6f8b0dcd..95a125eea5ce 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm
@@ -4,41 +4,40 @@
/// Blackboard key holding target atom
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// What AI behaviour do we actually run?
- var/datum/ai_behavior/ranged_skirmish/attack_behavior = /datum/ai_behavior/ranged_skirmish
+ var/attack_behavior = /datum/ai_behavior/ranged_skirmish
+ /// If target is further away than this we don't fire
+ var/max_range = 9
+ /// If target is closer than this we don't fire
+ var/min_range = 2
/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- var/atom/target = controller.blackboard[target_key]
- if(QDELETED(target))
+ if(!controller.blackboard_key_exists(target_key))
return
- controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(attack_behavior, target_key, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, max_range, min_range)
/// How often will we try to perform our ranged attack?
/datum/ai_behavior/ranged_skirmish
action_cooldown = 1 SECONDS
- /// If target is further away than this we don't fire
- var/max_range = 9
- /// If target is closer than this we don't fire
- var/min_range = 2
-/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
. = ..()
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
return !QDELETED(target)
-/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
. = ..()
var/atom/target = controller.blackboard[target_key]
if (QDELETED(target))
finish_action(controller, succeeded = FALSE)
return
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
- if(!targetting_datum.can_attack(controller.pawn, target))
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+ if(!targeting_strategy.can_attack(controller.pawn, target))
finish_action(controller, succeeded = FALSE)
return
- var/hiding_target = targetting_datum.find_hidden_mobs(controller.pawn, target)
+ var/hiding_target = targeting_strategy.find_hidden_mobs(controller.pawn, target)
controller.set_blackboard_key(hiding_location_key, hiding_target)
target = hiding_target || target
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm b/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm
new file mode 100644
index 000000000000..6f2f5cdc2035
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/run_emote.dm
@@ -0,0 +1,33 @@
+/// Intermittently run an emote
+/datum/ai_planning_subtree/run_emote
+ var/emote_key = BB_EMOTE_KEY
+ var/emote_chance_key = BB_EMOTE_CHANCE
+
+/datum/ai_planning_subtree/run_emote/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/emote_chance = controller.blackboard[emote_chance_key] || 0
+ if (!SPT_PROB(emote_chance, seconds_per_tick))
+ return
+ controller.queue_behavior(/datum/ai_behavior/run_emote, emote_key)
+
+/// Emote from a blackboard key
+/datum/ai_behavior/run_emote
+
+/datum/ai_behavior/run_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote_key)
+ var/mob/living/living_pawn = controller.pawn
+ if (!isliving(living_pawn))
+ finish_action(controller, FALSE)
+ return
+
+ var/list/emote_list = controller.blackboard[emote_key]
+ var/emote
+ if (islist(emote_list))
+ emote = length(emote_list) ? pick(emote_list) : null
+ else
+ emote = emote_list
+
+ if(isnull(emote))
+ finish_action(controller, FALSE)
+ return
+
+ living_pawn.emote(emote)
+ finish_action(controller, TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm b/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm
new file mode 100644
index 000000000000..ff01eb804ff7
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/shapechange_ambush.dm
@@ -0,0 +1,41 @@
+/// Shapeshift when we have no target, until someone has been nearby for long enough
+/datum/ai_planning_subtree/shapechange_ambush
+ operational_datums = list(/datum/component/ai_target_timer)
+ /// Key where we keep our ability
+ var/ability_key = BB_SHAPESHIFT_ACTION
+ /// Key where we keep our target
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// How long to lull our target into a false sense of security
+ var/minimum_target_time = 8 SECONDS
+
+/datum/ai_planning_subtree/shapechange_ambush/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ var/is_shifted = ismob(living_pawn.loc)
+ var/has_target = controller.blackboard_key_exists(target_key)
+ var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
+
+ if (!is_shifted)
+ if (has_target)
+ return // We're busy
+
+ if (using_action?.IsAvailable())
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Shift
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if (!has_target || !using_action?.IsAvailable())
+ return SUBTREE_RETURN_FINISH_PLANNING // Lie in wait
+ var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
+ if (time_on_target < minimum_target_time)
+ return // Wait a bit longer
+ controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Surprise!
+
+/// Selects a random shapeshift ability before shifting
+/datum/ai_behavior/use_mob_ability/shapeshift
+
+/datum/ai_behavior/use_mob_ability/shapeshift/setup(datum/ai_controller/controller, ability_key)
+ var/datum/action/cooldown/spell/shapeshift/using_action = controller.blackboard[ability_key]
+ if (!using_action?.IsAvailable())
+ return FALSE
+ if (isnull(using_action.shapeshift_type)) // If we don't have a shape then pick one, AI can't use context wheels
+ using_action.shapeshift_type = pick(using_action.possible_shapes)
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm
index 90379fcac0a7..5b1f5ffbff9e 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm
@@ -1,16 +1,16 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree
+ /// What do we do in order to attack someone?
var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
+ /// Is this the last thing we do? (if we set a movement target, this will usually be yes)
+ var/end_planning = TRUE
/datum/ai_planning_subtree/basic_melee_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
- if(QDELETED(target))
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
- controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
- return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
-
-/datum/ai_planning_subtree/basic_melee_attack_subtree/average_speed
- melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/average_speed
+ controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ if (end_planning)
+ return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
/datum/ai_planning_subtree/basic_ranged_attack_subtree
operational_datums = list(/datum/component/ranged_attacks)
@@ -18,8 +18,7 @@
/datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
- if(QDELETED(target))
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
- controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
index 42a361c25cd8..6630f7d193d9 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
@@ -3,19 +3,19 @@
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/// Find the nearest thing on our list of 'things which have done damage to me' and set it as the flee target
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee
- var/targeting_key = BB_TARGETTING_DATUM
+ var/targeting_key = BB_TARGETING_STRATEGY
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- if (!controller.blackboard[BB_BASIC_MOB_FLEEING])
+ if (controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list/nearest, BB_BASIC_MOB_RETALIATE_LIST, BB_BASIC_MOB_CURRENT_TARGET, targeting_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/from_flee_key
- targeting_key = BB_FLEE_TARGETTING_DATUM
+ targeting_key = BB_FLEE_TARGETING_STRATEGY
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
index ec4ef1863adc..1c7d8de9120b 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm
@@ -2,7 +2,7 @@
/datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(/datum/ai_behavior/find_potential_targets, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
// Prevents finding a target if a human is nearby
/datum/ai_planning_subtree/simple_find_target/not_while_observed
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm
new file mode 100644
index 000000000000..7a230014d9ab
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_wounded_target.dm
@@ -0,0 +1,6 @@
+/// Selects the most wounded potential target that we can see
+/datum/ai_planning_subtree/simple_find_wounded_target
+
+/datum/ai_planning_subtree/simple_find_wounded_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ controller.queue_behavior(/datum/ai_behavior/find_potential_targets/most_wounded, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm
index ccd67d6d1e72..93499cf673c4 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/sleep_with_no_target.dm
@@ -16,7 +16,7 @@
/datum/ai_behavior/sleep_after_targetless_time/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
- finish_action(controller, succeeded = !target, seconds_per_tick = seconds_per_tick)
+ finish_action(controller, succeeded = QDELETED(target), seconds_per_tick = seconds_per_tick)
/datum/ai_behavior/sleep_after_targetless_time/finish_action(datum/ai_controller/controller, succeeded, seconds_per_tick)
. = ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
index a201191d31c3..8cebe0e9f149 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
@@ -7,32 +7,42 @@
var/list/emote_see = list()
///Possible lines of speech the AI can have
var/list/speak = list()
+ ///The sound effects associated with this speech, if any
+ var/list/sound = list()
/datum/ai_planning_subtree/random_speech/New()
. = ..()
if(speak)
speak = string_list(speak)
+ if(sound)
+ sound = string_list(sound)
if(emote_hear)
emote_hear = string_list(emote_hear)
if(emote_see)
emote_see = string_list(emote_see)
/datum/ai_planning_subtree/random_speech/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- if(SPT_PROB(speech_chance, seconds_per_tick))
- var/audible_emotes_length = emote_hear?.len
- var/non_audible_emotes_length = emote_see?.len
- var/speak_lines_length = speak?.len
+ if(!SPT_PROB(speech_chance, seconds_per_tick))
+ return
+ speak(controller)
+
+/// Actually perform an action
+/datum/ai_planning_subtree/random_speech/proc/speak(datum/ai_controller/controller)
+ var/audible_emotes_length = emote_hear?.len
+ var/non_audible_emotes_length = emote_see?.len
+ var/speak_lines_length = speak?.len
- var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
+ var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
- var/random_number_in_range = rand(1, total_choices_length)
+ var/random_number_in_range = rand(1, total_choices_length)
+ var/sound_to_play = length(sound) > 0 ? pick(sound) : null
- if(random_number_in_range <= audible_emotes_length)
- controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear))
- else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
- controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
- else
- controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak))
+ if(random_number_in_range <= audible_emotes_length)
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear), sound_to_play)
+ else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
+ controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
+ else
+ controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play)
/datum/ai_planning_subtree/random_speech/insect
speech_chance = 5
@@ -60,6 +70,7 @@
/datum/ai_planning_subtree/random_speech/sheep
speech_chance = 5
speak = list("baaa","baaaAAAAAH!","baaah")
+ sound = list('sound/creatures/sheep1.ogg', 'sound/creatures/sheep2.ogg', 'sound/creatures/sheep3.ogg')
emote_hear = list("bleats.")
emote_see = list("shakes her head.", "stares into the distance.")
@@ -94,6 +105,7 @@
/datum/ai_planning_subtree/random_speech/cow
speech_chance = 1
speak = list("moo?","moo","MOOOOOO")
+ sound = list('sound/creatures/cow.ogg')
emote_hear = list("brays.")
emote_see = list("shakes her head.")
@@ -104,6 +116,7 @@
/datum/ai_planning_subtree/random_speech/cow/wisdom/New()
. = ..()
speak = GLOB.wisdoms //Done here so it's setup properly
+ sound = list()
/datum/ai_planning_subtree/random_speech/deer
speech_chance = 1
@@ -143,6 +156,9 @@
speak = list("oink?","oink","snurf")
emote_hear = list("snorts.")
emote_see = list("sniffs around.")
+ sound = list('sound/creatures/pig1.ogg', 'sound/creatures/pig2.ogg')
+ emote_hear = list("snorts.")
+ emote_see = list("sniffs around.")
/datum/ai_planning_subtree/random_speech/killer_tomato
speech_chance = 3
@@ -176,3 +192,21 @@
speech_chance = 5
emote_hear = list("rawrs.","grumbles.","grawls.", "stomps!")
emote_see = list("stares ferociously.")
+
+/datum/ai_planning_subtree/random_speech/blackboard //literal tower of babel, subtree form
+ speech_chance = 1
+
+/datum/ai_planning_subtree/random_speech/blackboard/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/list/speech_lines = controller.blackboard[BB_BASIC_MOB_SPEAK_LINES]
+ if(isnull(speech_lines))
+ return ..()
+
+ // Note to future developers: this behaviour a singleton so this probably doesn't work as you would expect
+ // The whole speech tree really needs to be refactored because this isn't how we use AI data these days
+ speak = speech_lines[BB_EMOTE_SAY] || list()
+ emote_see = speech_lines[BB_EMOTE_SEE] || list()
+ emote_hear = speech_lines[BB_EMOTE_HEAR] || list()
+ sound = speech_lines[BB_EMOTE_SOUND] || list()
+ speech_chance = speech_lines[BB_SPEAK_CHANCE] ? speech_lines[BB_SPEAK_CHANCE] : initial(speech_chance)
+
+ return ..()
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
index 2d553fe4b2f7..55ec73876131 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
@@ -2,21 +2,26 @@
/datum/ai_planning_subtree/target_retaliate
operational_datums = list(/datum/element/ai_retaliate, /datum/component/ai_retaliate_advanced)
/// Blackboard key which tells us how to select valid targets
- var/targetting_datum_key = BB_TARGETTING_DATUM
+ var/targeting_strategy_key = BB_TARGETING_STRATEGY
/// Blackboard key in which to store selected target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Blackboard key in which to store selected target's hiding place
var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
+ /// do we check for faction?
+ var/check_faction = FALSE
/datum/ai_planning_subtree/target_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targetting_datum_key, hiding_place_key)
+ controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction)
+
+/datum/ai_planning_subtree/target_retaliate/check_faction
+ check_faction = TRUE
/// Places a mob which you can see and who has recently attacked you into some 'run away from this' AI keys
-/// Can use a different targetting datum than you use to select attack targets
+/// Can use a different targeting strategy than you use to select attack targets
/// Not required if fleeing is the only target behaviour or uses the same target datum
/datum/ai_planning_subtree/target_retaliate/to_flee
- targetting_datum_key = BB_FLEE_TARGETTING_DATUM
+ targeting_strategy_key = BB_FLEE_TARGETING_STRATEGY
target_key = BB_BASIC_MOB_FLEE_TARGET
hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION
@@ -29,42 +34,51 @@
/// How far can we see stuff?
var/vision_range = 9
-/datum/ai_behavior/target_from_retaliate_list/perform(seconds_per_tick, datum/ai_controller/controller, shitlist_key, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/target_from_retaliate_list/perform(seconds_per_tick, datum/ai_controller/controller, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
. = ..()
var/mob/living/living_mob = controller.pawn
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
- if(!targetting_datum)
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+ if(!targeting_strategy)
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
- var/list/enemies_list = controller.blackboard[shitlist_key]
- if (!length(enemies_list))
- finish_action(controller, succeeded = FALSE)
+ var/list/shitlist = controller.blackboard[shitlist_key]
+ var/atom/existing_target = controller.blackboard[target_key]
+
+ if (!check_faction)
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
+
+ if (!QDELETED(existing_target) && (locate(existing_target) in shitlist) && targeting_strategy.can_attack(living_mob, existing_target, vision_range))
+ finish_action(controller, succeeded = TRUE, check_faction = check_faction)
return
- if (controller.blackboard[target_key] in enemies_list) // Don't bother changing
- finish_action(controller, succeeded = FALSE)
+ var/list/enemies_list = list()
+ for(var/mob/living/potential_target as anything in shitlist)
+ if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range))
+ continue
+ enemies_list += potential_target
+
+ if(!length(enemies_list))
+ controller.clear_blackboard_key(target_key)
+ finish_action(controller, succeeded = FALSE, check_faction = check_faction)
return
var/atom/new_target = pick_final_target(controller, enemies_list)
controller.set_blackboard_key(target_key, new_target)
- var/atom/potential_hiding_location = targetting_datum.find_hidden_mobs(living_mob, new_target)
+ var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, new_target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)
- finish_action(controller, succeeded = TRUE)
-
-/// Returns true if this target is valid for attacking based on current conditions
-/datum/ai_behavior/target_from_retaliate_list/proc/can_attack_target(mob/living/living_mob, atom/target, datum/targetting_datum/targetting_datum)
- if (!target)
- return FALSE
- if (target == living_mob)
- return FALSE
- if (!can_see(living_mob, target, vision_range))
- return FALSE
- return targetting_datum.can_attack(living_mob, target)
+ finish_action(controller, succeeded = TRUE, check_faction = check_faction)
/// Returns the desired final target from the filtered list of enemies
/datum/ai_behavior/target_from_retaliate_list/proc/pick_final_target(datum/ai_controller/controller, list/enemies_list)
return pick(enemies_list)
+
+/datum/ai_behavior/target_from_retaliate_list/finish_action(datum/ai_controller/controller, succeeded, check_faction)
+ . = ..()
+ if (succeeded || check_faction)
+ return
+ var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
index b6b4d286df1a..cd809804ba36 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm
@@ -1,7 +1,7 @@
/// Attempts to use a mob ability on a target
/datum/ai_planning_subtree/targeted_mob_ability
/// Blackboard key for the ability
- var/ability_key = BB_TARGETTED_ACTION
+ var/ability_key = BB_TARGETED_ACTION
/// Blackboard key for where the target ref is stored
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Behaviour to perform using ability
@@ -13,11 +13,16 @@
if (!ability_key)
CRASH("You forgot to tell this mob where to find its ability")
- var/mob/living/target = controller.blackboard[target_key]
+ if (!controller.blackboard_key_exists(target_key))
+ return
+
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
- if (QDELETED(target) || QDELETED(using_action) || !using_action.IsAvailable())
+ if (!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behaviour, ability_key, target_key)
if (finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_planning_subtree/targeted_mob_ability/continue_planning
+ finish_planning = FALSE
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm
new file mode 100644
index 000000000000..dadba992e9f1
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/teleport_away_from_target.dm
@@ -0,0 +1,56 @@
+///behavior to activate ability to escape from target
+/datum/ai_planning_subtree/teleport_away_from_target
+ ///minimum distance away from the target before we execute behavior
+ var/minimum_distance = 2
+ ///the ability we will execute
+ var/ability_key
+
+/datum/ai_planning_subtree/teleport_away_from_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ return
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ var/distance_from_target = get_dist(target, controller.pawn)
+ if(distance_from_target >= minimum_distance)
+ controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
+ return
+ var/datum/action/cooldown/ability = controller.blackboard[ability_key]
+ if(!ability?.IsAvailable())
+ return
+ var/turf/location_turf = controller.blackboard[BB_ESCAPE_DESTINATION]
+
+ if(isnull(location_turf))
+ controller.queue_behavior(/datum/ai_behavior/find_furthest_turf_from_target, BB_BASIC_MOB_CURRENT_TARGET, BB_ESCAPE_DESTINATION, minimum_distance)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if(get_dist(location_turf, target) < minimum_distance || !can_see(controller.pawn, location_turf)) //target moved close too close or we moved too far since finding the target turf
+ controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, ability_key, BB_ESCAPE_DESTINATION)
+
+///find furtherst turf target so we may teleport to it
+/datum/ai_behavior/find_furthest_turf_from_target
+
+/datum/ai_behavior/find_furthest_turf_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, set_key, range)
+ var/mob/living/living_target = controller.blackboard[target_key]
+ if(QDELETED(living_target))
+ return
+
+ var/distance = 0
+ var/turf/chosen_turf
+ for(var/turf/open/potential_destination in oview(range, living_target))
+ if(potential_destination.is_blocked_turf())
+ continue
+ var/new_distance_to_target = get_dist(potential_destination, living_target)
+ if(new_distance_to_target > distance)
+ chosen_turf = potential_destination
+ distance = new_distance_to_target
+ if(distance == range)
+ break //we have already found the max distance
+
+ if(isnull(chosen_turf))
+ finish_action(controller, FALSE)
+ return
+
+ controller.set_blackboard_key(set_key, chosen_turf)
+ finish_action(controller, TRUE)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
new file mode 100644
index 000000000000..0b5e5d4776f9
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
@@ -0,0 +1,21 @@
+/// Simply walk to a location
+/datum/ai_planning_subtree/travel_to_point
+ /// Blackboard key where we travel a place we walk to
+ var/location_key = BB_TRAVEL_DESTINATION
+ /// What do we do in order to travel
+ var/travel_behaviour = /datum/ai_behavior/travel_towards
+
+/datum/ai_planning_subtree/travel_to_point/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/atom/target = controller.blackboard[location_key]
+ if (QDELETED(target))
+ return
+ controller.queue_behavior(travel_behaviour, location_key)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+
+/datum/ai_planning_subtree/travel_to_point/and_clear_target
+ travel_behaviour = /datum/ai_behavior/travel_towards/stop_on_arrival
+
+/datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce
+ location_key = BB_BASIC_MOB_REINFORCEMENT_TARGET
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm b/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm
index 2c618e98e72b..5ceef67bedb2 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm
@@ -1,10 +1,10 @@
/**
* Simple behaviours which simply try to use an ability whenever it is available.
- * For something which wants a target try `targetted_mob_ability`.
+ * For something which wants a target try `targeted_mob_ability`.
*/
/datum/ai_planning_subtree/use_mob_ability
/// Blackboard key for the ability
- var/ability_key
+ var/ability_key = BB_GENERIC_ACTION
/// Behaviour to perform using ability
var/use_ability_behaviour = /datum/ai_behavior/use_mob_ability
/// If true we terminate planning after trying to use the ability.
@@ -15,7 +15,7 @@
CRASH("You forgot to tell this mob where to find its ability")
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
- if (QDELETED(using_action) || !using_action.IsAvailable())
+ if (!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behaviour, ability_key)
diff --git a/code/datums/ai/basic_mobs/generic_controllers.dm b/code/datums/ai/basic_mobs/generic_controllers.dm
new file mode 100644
index 000000000000..dae1b944dd32
--- /dev/null
+++ b/code/datums/ai/basic_mobs/generic_controllers.dm
@@ -0,0 +1,26 @@
+/// The most basic AI tree which just finds a guy and then runs at them to click them
+/datum/ai_controller/basic_controller/simple_hostile
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Find a target, walk at target, attack intervening obstacles
+/datum/ai_controller/basic_controller/simple_hostile_obstacles
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm b/code/datums/ai/basic_mobs/pet_commands/pet_use_targeted_ability.dm
similarity index 100%
rename from code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm
rename to code/datums/ai/basic_mobs/pet_commands/pet_use_targeted_ability.dm
diff --git a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm b/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
similarity index 51%
rename from code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm
rename to code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
index 643089bb5eae..2505b87a717f 100644
--- a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm
+++ b/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
@@ -1,27 +1,35 @@
-///Datum for basic mobs to define what they can attack.
-/datum/targetting_datum
+///Datum for basic mobs to define what they can attack.GET_TARGETING_STRATEGY\((/[^,]*)\),
+///Global, just like ai_behaviors
+/datum/targeting_strategy
///Returns true or false depending on if the target can be attacked by the mob
-/datum/targetting_datum/proc/can_attack(mob/living/living_mob, atom/target, vision_range)
+/datum/targeting_strategy/proc/can_attack(mob/living/living_mob, atom/target, vision_range)
return
///Returns something the target might be hiding inside of
-/datum/targetting_datum/proc/find_hidden_mobs(mob/living/living_mob, atom/target)
+/datum/targeting_strategy/proc/find_hidden_mobs(mob/living/living_mob, atom/target)
var/atom/target_hiding_location
if(istype(target.loc, /obj/structure/closet) || istype(target.loc, /obj/machinery/disposal) || istype(target.loc, /obj/machinery/sleeper))
target_hiding_location = target.loc
return target_hiding_location
-/datum/targetting_datum/basic
+/datum/targeting_strategy/basic
/// When we do our basic faction check, do we look for exact faction matches?
var/check_factions_exactly = FALSE
- /// Minimum status to attack living beings
- var/stat_attack = CONSCIOUS
- ///Whether we care for seeing the target or not
+ /// Whether we care for seeing the target or not
var/ignore_sight = FALSE
+ /// Blackboard key containing the minimum stat of a living mob to target
+ var/minimum_stat_key = BB_TARGET_MINIMUM_STAT
+ /// If this blackboard key is TRUE, makes us only target wounded mobs
+ var/target_wounded_key
-/datum/targetting_datum/basic/can_attack(mob/living/living_mob, atom/the_target, vision_range)
- if(isturf(the_target) || !the_target) // bail out on invalids
+/datum/targeting_strategy/basic/can_attack(mob/living/living_mob, atom/the_target, vision_range)
+ var/datum/ai_controller/basic_controller/our_controller = living_mob.ai_controller
+
+ if(isnull(our_controller))
+ return FALSE
+
+ if(isturf(the_target) || isnull(the_target)) // bail out on invalids
return FALSE
if(isobj(the_target.loc))
@@ -30,6 +38,8 @@
return FALSE
if(ismob(the_target)) //Target is in godmode, ignore it.
+ if(living_mob.loc == the_target)
+ return FALSE // We've either been eaten or are shapeshifted, let's assume the latter because we're still alive
var/mob/M = the_target
if(M.status_flags & GODMODE)
return FALSE
@@ -40,13 +50,18 @@
if(living_mob.see_invisible < the_target.invisibility) //Target's invisible to us, forget it
return FALSE
- if(isturf(the_target.loc) && living_mob.z != the_target.z) // z check will always fail if target is in a mech
+ if(isturf(living_mob.loc) && isturf(the_target.loc) && living_mob.z != the_target.z) // z check will always fail if target is in a mech or pawn is shapeshifted or jaunting
return FALSE
if(isliving(the_target)) //Targeting vs living mobs
- var/mob/living/L = the_target
- if(faction_check(living_mob, L) || (L.stat > stat_attack))
+ var/mob/living/living_target = the_target
+ if(faction_check(our_controller, living_mob, living_target))
return FALSE
+ if(living_target.stat > our_controller.blackboard[minimum_stat_key])
+ return FALSE
+ if(target_wounded_key && our_controller.blackboard[target_wounded_key] && living_target.health == living_target.maxHealth)
+ return FALSE
+
return TRUE
if(ismecha(the_target)) //Targeting vs mechas
@@ -68,34 +83,29 @@
return FALSE
/// Returns true if the mob and target share factions
-/datum/targetting_datum/basic/proc/faction_check(mob/living/living_mob, mob/living/the_target)
- return living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly)
+/datum/targeting_strategy/basic/proc/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target)
+ if (controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || controller.blackboard[BB_TEMPORARILY_IGNORE_FACTION])
+ return FALSE
+ return living_mob.faction_check_atom(the_target, exact_match = check_factions_exactly)
/// Subtype more forgiving for items.
/// Careful, this can go wrong and keep a mob hyper-focused on an item it can't lose aggro on
-/datum/targetting_datum/basic/allow_items
+/datum/targeting_strategy/basic/allow_items
-/datum/targetting_datum/basic/allow_items/can_attack(mob/living/living_mob, atom/the_target)
+/datum/targeting_strategy/basic/allow_items/can_attack(mob/living/living_mob, atom/the_target, vision_range)
. = ..()
if(isitem(the_target))
// trust fall exercise
return TRUE
-/// Subtype which doesn't care about faction
-/// Mobs which retaliate but don't otherwise target seek should just attack anything which annoys them
-/datum/targetting_datum/basic/ignore_faction
-
-/datum/targetting_datum/basic/ignore_faction/faction_check(mob/living/living_mob, mob/living/the_target)
- return FALSE
-
/// Subtype which searches for mobs of a size relative to ours
-/datum/targetting_datum/basic/of_size
+/datum/targeting_strategy/basic/of_size
/// If true, we will return mobs which are smaller than us. If false, larger.
var/find_smaller = TRUE
/// If true, we will return mobs which are the same size as us.
var/inclusive = TRUE
-/datum/targetting_datum/basic/of_size/can_attack(mob/living/owner, atom/target)
+/datum/targeting_strategy/basic/of_size/can_attack(mob/living/owner, atom/target, vision_range)
if(!isliving(target))
return FALSE
. = ..()
@@ -110,8 +120,14 @@
return !find_smaller
// This is just using the default values but the subtype makes it clearer
-/datum/targetting_datum/basic/of_size/ours_or_smaller
+/datum/targeting_strategy/basic/of_size/ours_or_smaller
-/datum/targetting_datum/basic/of_size/larger
+/datum/targeting_strategy/basic/of_size/larger
find_smaller = FALSE
inclusive = FALSE
+
+/// Makes the mob only attack their own faction. Useful mostly if their attacks do something helpful (e.g. healing touch).
+/datum/targeting_strategy/basic/same_faction
+
+/datum/targeting_strategy/basic/same_faction/faction_check(mob/living/living_mob, mob/living/the_target)
+ return !..() // inverts logic to ONLY target mobs that share a faction
diff --git a/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm b/code/datums/ai/basic_mobs/targeting_strategies/dont_target_friends.dm
similarity index 55%
rename from code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm
rename to code/datums/ai/basic_mobs/targeting_strategies/dont_target_friends.dm
index 8e6c79fda3fa..7231d88dde08 100644
--- a/code/datums/ai/basic_mobs/targetting_datums/dont_target_friends.dm
+++ b/code/datums/ai/basic_mobs/targeting_strategies/dont_target_friends.dm
@@ -1,20 +1,14 @@
/// Don't target an atom in our friends list (or turfs), anything else is fair game
-/datum/targetting_datum/not_friends
+/datum/targeting_strategy/basic/not_friends
/// Stop regarding someone as a valid target once they pass this stat level, setting it to DEAD means you will happily attack corpses
var/attack_until_past_stat = HARD_CRIT
/// If we can try to closed turfs or not
var/attack_closed_turf = FALSE
///Returns true or false depending on if the target can be attacked by the mob
-/datum/targetting_datum/not_friends/can_attack(mob/living/living_mob, atom/target)
- if (!target)
- return FALSE
- if (attack_closed_turf)
- if (isopenturf(target))
- return FALSE
- else
- if (isturf(target))
- return FALSE
+/datum/targeting_strategy/basic/not_friends/can_attack(mob/living/living_mob, atom/target, vision_range)
+ if(attack_closed_turf && isclosedturf(target))
+ return TRUE
if (ismob(target))
var/mob/mob_target = target
@@ -35,7 +29,18 @@
// OR This is not our friend, fire at will
return TRUE
+///friends dont care about factions
+/datum/targeting_strategy/basic/not_friends/faction_check(mob/living/living_mob, mob/living/the_target)
return FALSE
-/datum/targetting_datum/not_friends/attack_closed_turfs
+/datum/targeting_strategy/basic/not_friends/attack_closed_turfs
attack_closed_turf = TRUE
+
+/// Subtype that allows us to target items while deftly avoiding attacking our allies. Be careful when it comes to targeting items as an AI could get trapped targeting something it can't destroy.
+/datum/targeting_strategy/basic/not_friends/allow_items
+
+/datum/targeting_strategy/basic/not_friends/allow_items/can_attack(mob/living/living_mob, atom/the_target, vision_range)
+ . = ..()
+ if(isitem(the_target))
+ // trust fall exercise
+ return TRUE
diff --git a/code/datums/ai/basic_mobs/targeting_strategies/with_object.dm b/code/datums/ai/basic_mobs/targeting_strategies/with_object.dm
new file mode 100644
index 000000000000..7cc76d3010c9
--- /dev/null
+++ b/code/datums/ai/basic_mobs/targeting_strategies/with_object.dm
@@ -0,0 +1,29 @@
+/**
+ * Find mobs who are holding the bb configurable object type
+ *
+ * This is an extension of basic targeting behaviour, that allows you to
+ * only target the mob if they have a specific item in their hand.
+ *
+ */
+/datum/targeting_strategy/basic/holding_object
+ /// BB key that holds the target typepath to use
+ var/target_item_key = BB_TARGET_HELD_ITEM
+
+///Returns true or false depending on if the target can be attacked by the mob
+/datum/targeting_strategy/basic/holding_object/can_attack(mob/living/living_mob, atom/target, vision_range)
+ var/datum/ai_controller/controller = living_mob.ai_controller
+ var/object_type_path = controller.blackboard[target_item_key]
+
+ if (object_type_path == null)
+ return FALSE // no op
+ if(!ismob(target))
+ return FALSE // no hands no problems
+
+ // Look at me, type casting like a grown up
+ var/mob/targetmob = target
+ // Check if our parent behaviour agrees we can attack this target (we ignore faction by default)
+ var/can_attack = ..()
+ if(can_attack && targetmob.is_holding_item_of_type(object_type_path))
+ return TRUE // they have the item
+ // No valid target
+ return FALSE
diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm
index 2f6ae2d9720a..c6bdc46afe72 100644
--- a/code/datums/ai/dog/dog_behaviors.dm
+++ b/code/datums/ai/dog/dog_behaviors.dm
@@ -4,22 +4,21 @@
* Adds a floor to the melee damage of the dog, as most pet dogs don't actually have any melee strength
*/
/datum/ai_behavior/basic_melee_attack/dog
- action_cooldown = 0.8 SECONDS
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 3
-/datum/ai_behavior/basic_melee_attack/dog/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
+/datum/ai_behavior/basic_melee_attack/dog/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
controller.behavior_cooldowns[src] = world.time + action_cooldown
var/mob/living/living_pawn = controller.pawn
if(!(isturf(living_pawn.loc) || HAS_TRAIT(living_pawn, TRAIT_AI_BAGATTACK))) // Void puppies can attack from inside bags
- finish_action(controller, FALSE, target_key, targetting_datum_key, hiding_location_key)
+ finish_action(controller, FALSE, target_key, targeting_strategy_key, hiding_location_key)
return
// Unfortunately going to repeat this check in parent call but what can you do
var/atom/target = controller.blackboard[target_key]
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
- if (!targetting_datum.can_attack(living_pawn, target))
- finish_action(controller, FALSE, target_key, targetting_datum_key, hiding_location_key)
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+ if (!targeting_strategy.can_attack(living_pawn, target))
+ finish_action(controller, FALSE, target_key, targeting_strategy_key, hiding_location_key)
return
if (!in_range(living_pawn, target))
diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm
index 5a42cb43a1eb..a5794ff9b5f4 100644
--- a/code/datums/ai/dog/dog_controller.dm
+++ b/code/datums/ai/dog/dog_controller.dm
@@ -2,7 +2,7 @@
blackboard = list(
BB_DOG_HARASS_HARM = TRUE,
BB_VISION_RANGE = AI_DOG_VISION_RANGE,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_dog
@@ -19,7 +19,11 @@
blackboard = list(
BB_DOG_HARASS_HARM = TRUE,
BB_VISION_RANGE = AI_DOG_VISION_RANGE,
- BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(),
+ BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
+ // Find nearby mobs ...
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/holding_object,
+ // With tongs in hand!
+ BB_TARGET_HELD_ITEM = /obj/item/kitchen/tongs,
BB_BABIES_PARTNER_TYPES = list(/mob/living/basic/pet/dog),
BB_BABIES_CHILD_TYPES = list(/mob/living/basic/pet/dog/corgi/puppy = 95, /mob/living/basic/pet/dog/corgi/puppy/void = 5),
)
@@ -29,6 +33,10 @@
/datum/ai_planning_subtree/make_babies, // Ian WILL prioritise sex over following your instructions
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/dog_harassment,
+ // Find targets to run away from (uses the targeting strategy from above)
+ /datum/ai_planning_subtree/simple_find_target,
+ // Flee from that target
+ /datum/ai_planning_subtree/flee_target,
)
/datum/ai_controller/basic_controller/dog/corgi/get_access()
@@ -36,4 +44,21 @@
if(!istype(corgi_pawn))
return
- return corgi_pawn.access_card
+ return corgi_pawn.access_card.GetAccess()
+
+/datum/ai_controller/basic_controller/dog/puppy
+ blackboard = list(
+ BB_DOG_HARASS_HARM = TRUE,
+ BB_VISION_RANGE = AI_DOG_VISION_RANGE,
+ BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/holding_object,
+ // With tongs in hand!
+ BB_TARGET_HELD_ITEM = /obj/item/kitchen/tongs,
+ )
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/random_speech/dog,
+ /datum/ai_planning_subtree/pet_planning,
+ /datum/ai_planning_subtree/dog_harassment,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/flee_target,
+ )
diff --git a/code/datums/ai/dog/dog_subtrees.dm b/code/datums/ai/dog/dog_subtrees.dm
index 66198b61ac30..62f63da54bdd 100644
--- a/code/datums/ai/dog/dog_subtrees.dm
+++ b/code/datums/ai/dog/dog_subtrees.dm
@@ -4,27 +4,27 @@
/datum/ai_planning_subtree/dog_harassment/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(10, seconds_per_tick))
return
- controller.queue_behavior(/datum/ai_behavior/find_hated_dog_target, BB_DOG_HARASS_TARGET, BB_PET_TARGETTING_DATUM)
+ controller.queue_behavior(/datum/ai_behavior/find_hated_dog_target, BB_DOG_HARASS_TARGET, BB_PET_TARGETING_STRATEGY)
var/atom/harass_target = controller.blackboard[BB_DOG_HARASS_TARGET]
if (isnull(harass_target))
return
- controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/dog, BB_DOG_HARASS_TARGET, BB_PET_TARGETTING_DATUM)
+ controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/dog, BB_DOG_HARASS_TARGET, BB_PET_TARGETING_STRATEGY)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/find_hated_dog_target
-/datum/ai_behavior/find_hated_dog_target/setup(datum/ai_controller/controller, target_key, targetting_datum_key)
+/datum/ai_behavior/find_hated_dog_target/setup(datum/ai_controller/controller, target_key, targeting_strategy_key)
. = ..()
var/mob/living/dog = controller.pawn
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
for(var/mob/living/iter_living in oview(2, dog))
if(iter_living.stat != CONSCIOUS || !HAS_TRAIT(iter_living, TRAIT_HATED_BY_DOGS))
continue
if(!isnull(dog.buckled))
dog.audible_message(span_notice("[dog] growls at [iter_living], yet [dog.p_they()] [dog.p_are()] much too comfy to move."), hearing_distance = COMBAT_MESSAGE_RANGE)
continue
- if(!targetting_datum.can_attack(dog, iter_living))
+ if(!targeting_strategy.can_attack(dog, iter_living))
continue
dog.audible_message(span_warning("[dog] growls at [iter_living], seemingly annoyed by [iter_living.p_their()] presence."), hearing_distance = COMBAT_MESSAGE_RANGE)
diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm
index 443febae2a0e..b0d34f68c224 100644
--- a/code/datums/ai/generic/find_and_set.dm
+++ b/code/datums/ai/generic/find_and_set.dm
@@ -8,8 +8,8 @@
/datum/ai_behavior/find_and_set/perform(seconds_per_tick, datum/ai_controller/controller, set_key, locate_path, search_range)
. = ..()
- var/atom/current_target = controller.blackboard[set_key]
- if (!QDELETED(current_target))
+ if (controller.blackboard_key_exists(set_key))
+ finish_action(controller, TRUE)
return
var/find_this_thing = search_tactic(controller, locate_path, search_range)
if(find_this_thing)
@@ -76,6 +76,16 @@
if(found.len)
return pick(found)
+/// Like find_and_set/in_list, but we return the turf location of the item instead of the item itself.
+/datum/ai_behavior/find_and_set/in_list/turf_location
+
+/datum/ai_behavior/find_and_set/in_list/turf_location/search_tactic(datum/ai_controller/controller, locate_paths, search_range)
+ . = ..()
+ if(isnull(.))
+ return null
+
+ return get_turf(.)
+
/**
* Variant of find and set which returns an object which can be animated with a staff of change
*/
@@ -129,9 +139,39 @@
continue
if (living_pawn.see_invisible < dead_pal.invisibility)
continue
- if (!living_pawn.faction_check_mob(dead_pal))
+ if (!living_pawn.faction_check_atom(dead_pal))
continue
nearby_bodies += dead_pal
if (nearby_bodies.len)
return pick(nearby_bodies)
+
+/**
+ * A variant that looks for a human who is not dead or incapacitated, and has a mind
+ */
+/datum/ai_behavior/find_and_set/conscious_person
+
+/datum/ai_behavior/find_and_set/conscious_person/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/list/customers = list()
+ for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn))
+ if(IS_DEAD_OR_INCAP(target) || !target.mind)
+ continue
+ customers += target
+
+ if(customers.len)
+ return pick(customers)
+
+ return null
+
+/datum/ai_behavior/find_and_set/nearby_friends
+ action_cooldown = 2 SECONDS
+
+/datum/ai_behavior/find_and_set/nearby_friends/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/atom/friend = locate(/mob/living/carbon/human) in oview(search_range, controller.pawn)
+
+ if(isnull(friend))
+ return null
+
+ var/mob/living/living_pawn = controller.pawn
+ var/potential_friend = living_pawn.faction.Find(REF(friend)) ? friend : null
+ return potential_friend
diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm
index 0091bfbf1acc..7e383d304afd 100644
--- a/code/datums/ai/generic/generic_behaviors.dm
+++ b/code/datums/ai/generic/generic_behaviors.dm
@@ -300,11 +300,25 @@
/datum/ai_behavior/perform_speech
-/datum/ai_behavior/perform_speech/perform(seconds_per_tick, datum/ai_controller/controller, speech)
+/datum/ai_behavior/perform_speech/perform(seconds_per_tick, datum/ai_controller/controller, speech, speech_sound)
+ . = ..()
+
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return
living_pawn.say(speech, forced = "AI Controller")
+ if(speech_sound)
+ playsound(living_pawn, speech_sound, 80, vary = TRUE)
+ finish_action(controller, TRUE)
+
+/datum/ai_behavior/perform_speech_radio
+
+/datum/ai_behavior/perform_speech_radio/perform(seconds_per_tick, datum/ai_controller/controller, speech, obj/item/radio/speech_radio, list/try_channels = list(RADIO_CHANNEL_COMMON))
+ var/mob/living/living_pawn = controller.pawn
+ if(!istype(living_pawn) || !istype(speech_radio) || QDELETED(speech_radio) || !length(try_channels))
+ finish_action(controller, FALSE)
+ return
+ speech_radio.talk_into(living_pawn, speech, pick(try_channels))
finish_action(controller, TRUE)
//song behaviors
diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
index a06f69965695..f5e5c209a181 100644
--- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm
+++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
@@ -16,6 +16,8 @@
var/hunt_range = 2
/// What are the chances we hunt something at any given moment
var/hunt_chance = 100
+ ///do we finish planning subtree
+ var/finish_planning = TRUE
/datum/ai_planning_subtree/find_and_hunt_target/New()
. = ..()
@@ -35,13 +37,15 @@
// We're not hunting anything, look around for something
if(isnull(hunted))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
+ return
// We ARE hunting something, execute the hunt.
// Note that if our AI controller has multiple hunting subtrees set,
// we may accidentally be executing another tree's hunt - not ideal,
// try to set a unique target key if you have multiple
- else
- controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN)
+
+ controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN)
+ if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else
/// Finds a specific atom type to hunt.
@@ -77,6 +81,7 @@
/// Do we reset the target after attacking something, so we can check for status changes.
var/always_reset_target = FALSE
+
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/atom/hunt_target = controller.blackboard[hunting_target_key]
@@ -89,7 +94,7 @@
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
- if(isnull(hunted))
+ if(QDELETED(hunted))
//Target is gone for some reason. forget about this task!
controller[hunting_target_key] = null
finish_action(controller, FALSE, hunting_target_key)
@@ -121,6 +126,37 @@
controller.clear_blackboard_key(hunting_target_key)
/datum/ai_behavior/hunt_target/unarmed_attack_target
+ ///do we toggle combat mode before interacting with the object?
+ var/switch_combat_mode = FALSE
/datum/ai_behavior/hunt_target/unarmed_attack_target/target_caught(mob/living/hunter, obj/structure/cable/hunted)
hunter.UnarmedAttack(hunted, TRUE)
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
+ . = ..()
+ if(!switch_combat_mode)
+ return
+ var/mob/living/living_pawn = controller.pawn
+ living_pawn.istate = initial(living_pawn.istate)
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/switch_combat_mode
+ switch_combat_mode = TRUE
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/reset_target
+ always_reset_target = TRUE
+
+/datum/ai_behavior/hunt_target/use_ability_on_target
+ always_reset_target = TRUE
+ ///the ability we will use
+ var/ability_key
+
+/datum/ai_behavior/hunt_target/use_ability_on_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
+ var/datum/action/cooldown/ability = controller.blackboard[ability_key]
+ if(!ability?.IsAvailable())
+ finish_action(controller, FALSE, hunting_target_key)
+ return ..()
+
+/datum/ai_behavior/hunt_target/use_ability_on_target/target_caught(mob/living/hunter, atom/hunted)
+ var/datum/action/cooldown/ability = hunter.ai_controller.blackboard[ability_key]
+ ability.InterceptClickOn(hunter, null, hunted)
+
diff --git a/code/datums/ai/hunting_behavior/hunting_corpses.dm b/code/datums/ai/hunting_behavior/hunting_corpses.dm
new file mode 100644
index 000000000000..e720e4da947a
--- /dev/null
+++ b/code/datums/ai/hunting_behavior/hunting_corpses.dm
@@ -0,0 +1,17 @@
+/// Find and attack corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target
+ hunt_targets = list(/mob/living)
+
+/// Find nearby dead mobs
+/datum/ai_behavior/find_hunt_target/corpses
+
+/datum/ai_behavior/find_hunt_target/corpses/valid_dinner(mob/living/source, mob/living/dinner, radius)
+ if (!isliving(dinner) || dinner.stat != DEAD)
+ return FALSE
+ return can_see(source, dinner, radius)
+
+/// Find and attack specifically human corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/human
+ hunt_targets = list(/mob/living/carbon/human)
diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm
index b25f983f3139..f189e89b4230 100644
--- a/code/datums/ai/idle_behaviors/idle_random_walk.dm
+++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm
@@ -14,3 +14,68 @@
/datum/idle_behavior/idle_random_walk/less_walking
walk_chance = 10
+
+/// Only walk if we don't have a target
+/datum/idle_behavior/idle_random_walk/no_target
+ /// Where do we look for a target?
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+/datum/idle_behavior/idle_random_walk/no_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ if (!controller.blackboard_key_exists(target_key))
+ return
+ return ..()
+
+/// Only walk if we are not on the target's location
+/datum/idle_behavior/idle_random_walk/not_while_on_target
+ ///What is the spot we have to stand on?
+ var/target_key
+
+/datum/idle_behavior/idle_random_walk/not_while_on_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ var/atom/target = controller.blackboard[target_key]
+
+ //Don't move, if we are are already standing on it
+ if(!QDELETED(target) && ((isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc)))
+ return
+
+ return ..()
+
+/// walk randomly however stick near a target
+/datum/idle_behavior/walk_near_target
+ /// chance to walk
+ var/walk_chance = 25
+ /// distance we are to target
+ var/minimum_distance = 20
+ /// key that holds target
+ var/target_key
+
+/datum/idle_behavior/walk_near_target/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ if(LAZYLEN(living_pawn.do_afters))
+ return
+
+ if(!SPT_PROB(walk_chance, seconds_per_tick) || !(living_pawn.mobility_flags & MOBILITY_MOVE) || !isturf(living_pawn.loc) || living_pawn.pulledby)
+ return
+
+ var/atom/target = controller.blackboard[target_key]
+ var/distance = get_dist(target, living_pawn)
+ if(isnull(target) || distance > minimum_distance) //if we are too far away from target, just walk randomly
+ var/move_dir = pick(GLOB.alldirs)
+ living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
+ return
+
+ var/list/possible_turfs = list()
+ for(var/direction in GLOB.alldirs)
+ var/turf/possible_step = get_step(living_pawn, direction)
+ if(get_dist(possible_step, target) > minimum_distance)
+ continue
+ if(possible_step.is_blocked_turf())
+ continue
+ possible_turfs += possible_step
+
+ if(!length(possible_turfs))
+ return
+
+ var/turf/picked_turf = pick(possible_turfs)
+
+ living_pawn.Move(picked_turf, get_dir(living_pawn, picked_turf))
diff --git a/code/datums/ai/learn_ai.md b/code/datums/ai/learn_ai.md
index 9906806cfbd5..e8a151140f06 100644
--- a/code/datums/ai/learn_ai.md
+++ b/code/datums/ai/learn_ai.md
@@ -25,7 +25,7 @@ First, let's look at the blackboard.
```dm
/datum/ai_controller/basic/cow
blackboard = list(
- BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items(),
+ BB_TARGETING_STRATEGY = GET_TARGETING_STRATEGY(/datum/targeting_strategy/basic/allow_items),
BB_BASIC_MOB_TIP_REACTING = FALSE,
BB_BASIC_MOB_TIPPER = null,
)
@@ -81,7 +81,7 @@ Okay, so we have blackboard variables, which are considered by subtrees to plan
//now we know we have a target but should let a hostile subtree plan attacking humans. let's check if it's actually food
if(target in wanted)
- controller.queue_behavior(/datum/ai_behavior/basic_melee_attack, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(/datum/ai_behavior/basic_melee_attack, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //this prevents further subtrees from planning since we want to focus on eating the food
```
@@ -113,7 +113,7 @@ And one of those behaviors, `basic_melee_attack`. As I have been doing so far, I
//targetting datum will kill the action if not real anymore
var/datum/weakref/weak_target = controller.blackboard[target_key]
var/atom/target = weak_target?.resolve()
- var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targetting_datum = controller.blackboard[targetting_datum_key]
if(!targetting_datum.can_attack(basic_mob, target))
///We have a target that is no longer valid to attack. Remember that returning doesn't end the behavior, JUST this single performance. So we call "finish_action" with whether it succeeded in doing what it wanted to do (it didn't, so FALSE) and the blackboard keys passed into this behavior.
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
index 3523da7ecec2..da46735ec363 100644
--- a/code/datums/ai/movement/ai_movement_jps.dm
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -2,7 +2,7 @@
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
- max_pathing_attempts = 4
+ max_pathing_attempts = 20
/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
. = ..()
@@ -12,13 +12,13 @@
var/datum/move_loop/loop = SSmove_manager.jps_move(moving,
current_movement_target,
delay,
- repath_delay = 2 SECONDS,
+ repath_delay = 0.5 SECONDS,
max_path_length = AI_MAX_PATH_LENGTH,
minimum_distance = controller.get_minimum_distance(),
id = controller.get_access(),
subsystem = SSai_movement,
extra_info = controller,
- initial_path = controller.blackboard[BB_PATH_TO_USE])
+ )
RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
diff --git a/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm b/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm
index 5bad78a35154..b4e5609531c1 100644
--- a/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm
+++ b/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm
@@ -22,7 +22,7 @@
/datum/ai_behavior/vendor_crush/proc/tiltonmob(datum/ai_controller/controller, turf/target_turf)
var/obj/machinery/vending/vendor_pawn = controller.pawn
- if(vendor_pawn.tilt(target_turf)) //We hit something
+ if(vendor_pawn.tilt(target_turf, 0) & SUCCESSFULLY_CRUSHED_MOB) //We hit something
vendor_pawn.say(pick("Supersize this!", "Eat my shiny metal ass!", "Want to consume some of my products?", "SMASH!", "Don't you love these smashing prices!"))
controller.set_blackboard_key(BB_VENDING_LAST_HIT_SUCCESFUL, TRUE)
else
diff --git a/code/datums/ai/robot_customer/robot_customer_behaviors.dm b/code/datums/ai/robot_customer/robot_customer_behaviors.dm
index 431713b94cac..e370af6f5c1f 100644
--- a/code/datums/ai/robot_customer/robot_customer_behaviors.dm
+++ b/code/datums/ai/robot_customer/robot_customer_behaviors.dm
@@ -3,7 +3,7 @@
/datum/ai_behavior/find_seat/perform(seconds_per_tick, datum/ai_controller/controller)
. = ..()
- var/mob/living/simple_animal/robot_customer/customer_pawn = controller.pawn
+ var/mob/living/basic/robot_customer/customer_pawn = controller.pawn
var/datum/customer_data/customer_data = controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
var/datum/venue/attending_venue = controller.blackboard[BB_CUSTOMER_ATTENDING_VENUE]
@@ -44,7 +44,7 @@
/datum/ai_behavior/order_food/perform(seconds_per_tick, datum/ai_controller/controller)
. = ..()
- var/mob/living/simple_animal/robot_customer/customer_pawn = controller.pawn
+ var/mob/living/basic/robot_customer/customer_pawn = controller.pawn
var/datum/customer_data/customer_data = controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
var/obj/structure/holosign/robot_seat/seat_marker = controller.blackboard[BB_CUSTOMER_MY_SEAT]
if(get_turf(seat_marker) == get_turf(customer_pawn))
@@ -75,7 +75,7 @@
// SPT_PROB 1.5 is about a 40% chance that the tourist will have vocalised at least once every minute.
if(SPT_PROB(0.85, seconds_per_tick))
- var/mob/living/simple_animal/robot_customer/customer_pawn = controller.pawn
+ var/mob/living/basic/robot_customer/customer_pawn = controller.pawn
var/datum/customer_data/customer_data = controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
customer_pawn.say(pick(customer_data.wait_for_food_lines))
@@ -98,7 +98,7 @@
/datum/ai_behavior/wait_for_food/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
- var/mob/living/simple_animal/robot_customer/customer_pawn = controller.pawn
+ var/mob/living/basic/robot_customer/customer_pawn = controller.pawn
var/datum/customer_data/customer_data = controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
var/mob/living/greytider = controller.blackboard[BB_CUSTOMER_CURRENT_TARGET]
//usually if we stop waiting, it's because we're done with the venue. but here we're either beating some dude up
diff --git a/code/datums/ai/robot_customer/robot_customer_controller.dm b/code/datums/ai/robot_customer/robot_customer_controller.dm
index dfcab76c8728..433eab6ae88c 100644
--- a/code/datums/ai/robot_customer/robot_customer_controller.dm
+++ b/code/datums/ai/robot_customer/robot_customer_controller.dm
@@ -1,14 +1,16 @@
/datum/ai_controller/robot_customer
ai_movement = /datum/ai_movement/basic_avoidance
movement_delay = 0.8 SECONDS
- blackboard = list(BB_CUSTOMER_CURRENT_ORDER = null,
- BB_CUSTOMER_MY_SEAT = null,
- BB_CUSTOMER_PATIENCE = 999,
- BB_CUSTOMER_CUSTOMERINFO = null,
- BB_CUSTOMER_EATING = FALSE,
- BB_CUSTOMER_LEAVING = FALSE,
- BB_CUSTOMER_ATTENDING_VENUE = null,
- BB_CUSTOMER_SAID_CANT_FIND_SEAT_LINE = FALSE)
+ blackboard = list(
+ BB_CUSTOMER_ATTENDING_VENUE = null,
+ BB_CUSTOMER_CURRENT_ORDER = null,
+ BB_CUSTOMER_CUSTOMERINFO = null,
+ BB_CUSTOMER_EATING = FALSE,
+ BB_CUSTOMER_LEAVING = FALSE,
+ BB_CUSTOMER_MY_SEAT = null,
+ BB_CUSTOMER_PATIENCE = 999 SECONDS,
+ BB_CUSTOMER_SAID_CANT_FIND_SEAT_LINE = FALSE,
+ )
planning_subtrees = list(/datum/ai_planning_subtree/robot_customer)
/datum/ai_controller/robot_customer/Destroy()
@@ -18,7 +20,7 @@
return ..()
/datum/ai_controller/robot_customer/TryPossessPawn(atom/new_pawn)
- if(!istype(new_pawn, /mob/living/simple_animal/robot_customer))
+ if(!istype(new_pawn, /mob/living/basic/robot_customer))
return AI_CONTROLLER_INCOMPATIBLE
new_pawn.AddElement(/datum/element/relay_attackers)
RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby))
@@ -28,6 +30,12 @@
return ..() //Run parent at end
/datum/ai_controller/robot_customer/UnpossessPawn(destroy)
+ if(isnull(pawn))
+#ifndef UNIT_TESTS
+ stack_trace("Robot Customer AI Controller UnpossessPawn called with null pawn! This shouldn't happen in normal circumstances.") // and unit tests are abnormal circumstances
+#endif
+ return
+
UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_WAS_ATTACKED, COMSIG_LIVING_GET_PULLED, COMSIG_ATOM_ATTACK_HAND))
return ..() //Run parent at end
@@ -67,7 +75,7 @@
INVOKE_ASYNC(src, PROC_REF(async_on_get_pulled), source, puller)
/datum/ai_controller/robot_customer/proc/async_on_get_pulled(datum/source, mob/living/puller)
- var/mob/living/simple_animal/robot_customer/customer = pawn
+ var/mob/living/basic/robot_customer/customer = pawn
var/datum/customer_data/customer_data = blackboard[BB_CUSTOMER_CUSTOMERINFO]
var/datum/venue/attending_venue = blackboard[BB_CUSTOMER_ATTENDING_VENUE]
@@ -81,12 +89,12 @@
/datum/ai_controller/robot_customer/proc/dont_want_that(mob/living/chef, obj/item/thing)
- var/mob/living/simple_animal/robot_customer/customer = pawn
+ var/mob/living/basic/robot_customer/customer = pawn
var/datum/customer_data/customer_data = blackboard[BB_CUSTOMER_CUSTOMERINFO]
customer.say(customer_data.wrong_item_line)
/datum/ai_controller/robot_customer/proc/warn_greytider(mob/living/greytider)
- var/mob/living/simple_animal/robot_customer/customer = pawn
+ var/mob/living/basic/robot_customer/customer = pawn
var/datum/venue/attending_venue = blackboard[BB_CUSTOMER_ATTENDING_VENUE]
var/datum/customer_data/customer_data = blackboard[BB_CUSTOMER_CUSTOMERINFO]
//Living mobs are tagged, so these will always be valid
diff --git a/code/datums/cinematics/_cinematic.dm b/code/datums/cinematics/_cinematic.dm
index 6659fde0c516..cc4fe5a0fab8 100644
--- a/code/datums/cinematics/_cinematic.dm
+++ b/code/datums/cinematics/_cinematic.dm
@@ -1,3 +1,5 @@
+#define CINEMATIC_SOURCE "cinematic"
+
/**
* Plays a cinematic, duh. Can be to a select few people, or everyone.
*
@@ -30,7 +32,7 @@
/datum/cinematic
/// A list of all clients watching the cinematic
var/list/client/watching = list()
- /// A list of all mobs who have notransform set while watching the cinematic
+ /// A list of all mobs who have TRAIT_NO_TRANSFORM set while watching the cinematic
var/list/datum/weakref/locked = list()
/// Whether the cinematic is a global cinematic or not
var/is_global = FALSE
@@ -106,13 +108,8 @@
/datum/cinematic/proc/show_to(mob/watching_mob, client/watching_client)
SIGNAL_HANDLER
- // We could technically rip people out of notransform who shouldn't be,
- // so we'll only lock down all viewing mobs who don't have it already set.
- // This does potentially mean some mobs could lose their notrasnform and
- // not be locked down by cinematics, but that should be very unlikely.
- if(!watching_mob.notransform)
- locked += WEAKREF(watching_mob)
- watching_mob.notransform = TRUE
+ if(!HAS_TRAIT_FROM(watching_mob, TRAIT_NO_TRANSFORM, CINEMATIC_SOURCE))
+ lock_mob(watching_mob)
// Only show the actual cinematic to cliented mobs.
if(!watching_client || (watching_client in watching))
@@ -146,14 +143,24 @@
remove_watcher(viewing_client)
for(var/datum/weakref/locked_ref as anything in locked)
- var/mob/locked_mob = locked_ref.resolve()
- if(QDELETED(locked_mob))
- continue
- locked_mob.notransform = FALSE
- UnregisterSignal(locked_mob, COMSIG_MOB_CLIENT_LOGIN)
+ unlock_mob(locked_ref)
qdel(src)
+
+/// Locks a mob, preventing them from moving, being hurt, or acting
+/datum/cinematic/proc/lock_mob(mob/to_lock)
+ locked += WEAKREF(to_lock)
+ ADD_TRAIT(to_lock, TRAIT_NO_TRANSFORM, CINEMATIC_SOURCE)
+
+/// Unlocks a previously locked weakref
+/datum/cinematic/proc/unlock_mob(datum/weakref/mob_ref)
+ var/mob/locked_mob = mob_ref.resolve()
+ if(isnull(locked_mob))
+ return
+ REMOVE_TRAIT(locked_mob, TRAIT_NO_TRANSFORM, CINEMATIC_SOURCE)
+ UnregisterSignal(locked_mob, COMSIG_MOB_CLIENT_LOGIN)
+
/// Removes the passed client from our watching list.
/datum/cinematic/proc/remove_watcher(client/no_longer_watching)
SIGNAL_HANDLER
@@ -163,8 +170,10 @@
UnregisterSignal(no_longer_watching, COMSIG_PARENT_QDELETING)
// We'll clear the cinematic if they have a mob which has one,
- // but we won't remove notransform. Wait for the cinematic end to do that.
+ // but we won't remove TRAIT_NO_TRANSFORM. Wait for the cinematic end to do that.
no_longer_watching.mob?.clear_fullscreen("cinematic")
no_longer_watching.screen -= screen
watching -= no_longer_watching
+
+#undef CINEMATIC_SOURCE
diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm
index 253e3c3d2a8f..3b8d83349dcf 100644
--- a/code/datums/components/_component.dm
+++ b/code/datums/components/_component.dm
@@ -169,12 +169,13 @@
return
/**
- * Called when the component has a new source registered
+ * Called when the component has a new source registered.
+ * Return COMPONENT_INCOMPATIBLE to signal that the source is incompatible and should not be added
*/
-/datum/component/proc/on_source_add(source)
+/datum/component/proc/on_source_add(source, ...)
SHOULD_CALL_PARENT(TRUE)
if(dupe_mode != COMPONENT_DUPE_SOURCES)
- CRASH("Component '[type]' does not use sources but has been given a source")
+ return COMPONENT_INCOMPATIBLE
LAZYOR(sources, source)
/**
@@ -461,7 +462,7 @@
component_type = new_component.type
raw_args[1] = src
- if(dupe_mode != COMPONENT_DUPE_ALLOWED && dupe_mode != COMPONENT_DUPE_SELECTIVE)
+ if(dupe_mode != COMPONENT_DUPE_ALLOWED && dupe_mode != COMPONENT_DUPE_SELECTIVE && dupe_mode != COMPONENT_DUPE_SOURCES)
if(!dupe_type)
old_component = GetExactComponent(component_type)
else
@@ -494,10 +495,14 @@
if(COMPONENT_DUPE_SOURCES)
if(source in old_component.sources)
return old_component // source already registered, no work to do
- old_component.on_source_add(source)
+
+ if(old_component.on_source_add(arglist(list(source) + raw_args.Copy(2))) == COMPONENT_INCOMPATIBLE)
+ stack_trace("incompatible source added to a [old_component.type]. Args: [json_encode(raw_args)]")
+ return null
else if(!new_component)
new_component = new component_type(raw_args) // There's a valid dupe mode but there's no old component, act like normal
+
else if(dupe_mode == COMPONENT_DUPE_SELECTIVE)
var/list/arguments = raw_args.Copy()
arguments[1] = new_component
@@ -509,12 +514,17 @@
break
if(!new_component && make_new_component)
new_component = new component_type(raw_args)
+
+ else if(dupe_mode == COMPONENT_DUPE_SOURCES)
+ new_component = new component_type(raw_args)
+ if(new_component.on_source_add(arglist(list(source) + raw_args.Copy(2))) == COMPONENT_INCOMPATIBLE)
+ stack_trace("incompatible source added to a [new_component.type]. Args: [json_encode(raw_args)]")
+ return null
+
else if(!new_component)
new_component = new component_type(raw_args) // Dupes are allowed, act like normal
if(!old_component && !QDELETED(new_component)) // Nothing related to duplicate components happened and the new component is healthy
- if(uses_sources) // make sure they have the source added if they use sources
- new_component.on_source_add(source)
SEND_SIGNAL(src, COMSIG_COMPONENT_ADDED, new_component)
return new_component
diff --git a/code/datums/components/appearance_on_aggro.dm b/code/datums/components/appearance_on_aggro.dm
index 85690a579ae8..8c0df88e6fdb 100644
--- a/code/datums/components/appearance_on_aggro.dm
+++ b/code/datums/components/appearance_on_aggro.dm
@@ -1,49 +1,80 @@
/**
- * component gave to basic creatures to allow them to change appearance when find a target
+ * Changes visuals of the attached mob while it has a target
*/
/datum/component/appearance_on_aggro
+ /// Blackboardey to search for a target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// Icon state to use when we have a target
+ var/aggro_state
/// path of the overlay to apply
- var/mutable_appearance/overlay_path
+ var/mutable_appearance/aggro_overlay
/// visibility of our icon when aggroed
var/alpha_on_aggro
/// visibility of our icon when deaggroed
var/alpha_on_deaggro
+ /// do we currently have a target
+ var/atom/current_target
-/datum/component/appearance_on_aggro/Initialize(overlay_icon, overlay_state, alpha_on_aggro, alpha_on_deaggro)
- if(!isliving(parent))
+/datum/component/appearance_on_aggro/Initialize(aggro_state, overlay_icon, overlay_state, alpha_on_aggro, alpha_on_deaggro)
+ if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
- if(overlay_icon && overlay_state)
- src.overlay_path = mutable_appearance(overlay_icon, overlay_state)
- if(!alpha_on_aggro || !alpha_on_deaggro)
- return
+ src.aggro_state = aggro_state
src.alpha_on_aggro = alpha_on_aggro
src.alpha_on_deaggro = alpha_on_deaggro
+ if (!isnull(overlay_icon) && !isnull(overlay_state))
+ aggro_overlay = mutable_appearance(overlay_icon, overlay_state)
/datum/component/appearance_on_aggro/RegisterWithParent()
- RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(change_appearance))
- RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(revert_appearance))
+ RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_set_target))
+ RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(on_clear_target))
+ if (!isnull(aggro_state))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_icon_state_updated))
+ if (!isnull(aggro_overlay))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_overlays_updated))
/datum/component/appearance_on_aggro/UnregisterFromParent()
. = ..()
UnregisterSignal(parent, list(COMSIG_AI_BLACKBOARD_KEY_SET(target_key), COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key)))
-/datum/component/appearance_on_aggro/proc/change_appearance(mob/living/source)
+/datum/component/appearance_on_aggro/proc/on_set_target(mob/living/source)
SIGNAL_HANDLER
var/atom/target = source.ai_controller.blackboard[target_key]
- if(isnull(target))
+ if (QDELETED(target))
return
- if(overlay_path)
- source.add_overlay(overlay_path)
- if(alpha_on_aggro)
+
+ current_target = target
+ if (!isnull(aggro_overlay) || !isnull(aggro_state))
+ source.update_appearance(UPDATE_ICON)
+ if (!isnull(alpha_on_aggro))
animate(source, alpha = alpha_on_aggro, time = 2 SECONDS)
-/datum/component/appearance_on_aggro/proc/revert_appearance(mob/living/source)
+/datum/component/appearance_on_aggro/Destroy()
+ if (!isnull(current_target))
+ revert_appearance(parent)
+ return ..()
+
+/datum/component/appearance_on_aggro/proc/on_clear_target(atom/source)
SIGNAL_HANDLER
+ revert_appearance(parent)
- if(overlay_path)
- source.cut_overlay(overlay_path)
- if(alpha_on_deaggro)
+/datum/component/appearance_on_aggro/proc/revert_appearance(mob/living/source)
+ current_target = null
+ if (!isnull(aggro_overlay) || !isnull(aggro_state))
+ source.update_appearance(UPDATE_ICON)
+ if (!isnull(alpha_on_deaggro))
animate(source, alpha = alpha_on_deaggro, time = 2 SECONDS)
+
+/datum/component/appearance_on_aggro/proc/on_icon_state_updated(mob/living/source)
+ SIGNAL_HANDLER
+ if (source.stat == DEAD)
+ return
+ source.icon_state = isnull(current_target) ? initial(source.icon_state) : aggro_state
+
+/datum/component/appearance_on_aggro/proc/on_overlays_updated(atom/source, list/overlays)
+ SIGNAL_HANDLER
+
+ if (isnull(current_target))
+ return
+ overlays += aggro_overlay
diff --git a/code/datums/components/area_based_godmode.dm b/code/datums/components/area_based_godmode.dm
new file mode 100644
index 000000000000..4f03ae57794c
--- /dev/null
+++ b/code/datums/components/area_based_godmode.dm
@@ -0,0 +1,123 @@
+#define MAP_AREA_TYPE "area_type"
+#define MAP_ALLOW_AREA_SUBTYPES "allow_area_subtypes"
+#define DEFAULT_GAIN_MESSAGE span_big(span_green("You are now invulnerable."))
+#define DEFAULT_LOSE_MESSAGE span_big(span_red("You are no longer invulnerable."))
+
+/**
+ * Area-based godmode.
+ * Gain and Lose message can only be set once, at initial component creation; adding a source will not update them.
+ */
+/datum/component/area_based_godmode
+ dupe_mode = COMPONENT_DUPE_SOURCES
+
+ /// The type of area that will trigger godmode.
+ var/list/sources_to_area_type
+
+ /// Whether or not to allow subtypes of the area type to trigger godmode.
+ var/allow_area_subtypes
+
+ /// The message to send to the mob when they gain godmode.
+ var/gain_message
+
+ /// The message to send to the mob when they lose godmode.
+ var/lose_message
+
+ /// Cached state of check_area, prevents recalculating on source add
+ var/check_area_cached_state = FALSE
+
+/datum/component/area_based_godmode/Initialize(
+ area_type,
+ allow_area_subtypes,
+ gain_message = DEFAULT_GAIN_MESSAGE,
+ lose_message = DEFAULT_LOSE_MESSAGE,
+)
+ var/mob/mob_target = parent
+ if(!istype(mob_target))
+ return COMPONENT_INCOMPATIBLE
+ if(initial(mob_target.status_flags) & GODMODE)
+ return COMPONENT_INCOMPATIBLE
+
+ sources_to_area_type = list()
+ src.gain_message = gain_message
+ src.lose_message = lose_message
+ RegisterSignal(mob_target, COMSIG_ENTER_AREA, PROC_REF(check_area))
+
+/datum/component/area_based_godmode/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ENTER_AREA)
+
+/datum/component/area_based_godmode/on_source_add(
+ source,
+ area_type,
+ allow_area_subtypes = FALSE,
+ gain_message = DEFAULT_GAIN_MESSAGE,
+ lose_message = DEFAULT_LOSE_MESSAGE,
+)
+ . = ..()
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ var/list/information_map = list(
+ MAP_AREA_TYPE = area_type,
+ MAP_ALLOW_AREA_SUBTYPES = allow_area_subtypes,
+ )
+ sources_to_area_type[source] = information_map
+
+ var/mob/mob_target = parent // no need to istype here, done at creation
+ mob_target.become_area_sensitive("[REF(src)]:[source]")
+ if(!check_area_cached_state)
+ check_area(mob_target)
+
+/datum/component/area_based_godmode/on_source_remove(source)
+ sources_to_area_type -= source
+ var/mob/mob_target = parent
+ mob_target.lose_area_sensitivity("[REF(src)]:[source]")
+ if(check_area_cached_state)
+ check_area(mob_target)
+ return ..()
+
+/datum/component/area_based_godmode/proc/check_in_valid_area(mob/checking)
+ var/list/area/allowed_areas = list()
+ for(var/source in sources_to_area_type)
+ var/list/source_map = sources_to_area_type[source]
+ var/area/top_level = source_map[MAP_AREA_TYPE]
+ if(!allowed_areas[top_level])
+ allowed_areas[top_level] = source_map[MAP_ALLOW_AREA_SUBTYPES]
+
+ if(!length(allowed_areas))
+ stack_trace("called check_in_valid_area with zero sources")
+ return FALSE
+
+ var/area/area = get_area(checking)
+ if(area.type in allowed_areas)
+ return TRUE
+
+ for(var/area/allowed_area as anything in allowed_areas)
+ if(!allowed_areas[allowed_area])
+ continue
+ if(istype(area, allowed_area))
+ return TRUE
+
+ return FALSE
+
+/datum/component/area_based_godmode/proc/check_area(mob/source)
+ SIGNAL_HANDLER
+
+ var/has_godmode = source.status_flags & GODMODE
+ if(!check_in_valid_area(source))
+ if(has_godmode)
+ to_chat(source, lose_message)
+ source.status_flags ^= GODMODE
+ check_area_cached_state = FALSE
+ return
+
+ check_area_cached_state = TRUE
+ if(has_godmode)
+ return
+
+ to_chat(source, gain_message)
+ source.status_flags ^= GODMODE
+
+#undef MAP_AREA_TYPE
+#undef MAP_ALLOW_AREA_SUBTYPES
+#undef DEFAULT_GAIN_MESSAGE
+#undef DEFAULT_LOSE_MESSAGE
diff --git a/code/datums/components/basic_inhands.dm b/code/datums/components/basic_inhands.dm
new file mode 100644
index 000000000000..ac50f618861f
--- /dev/null
+++ b/code/datums/components/basic_inhands.dm
@@ -0,0 +1,50 @@
+/**
+ * Basic handling for showing held items in a mob's hands
+ */
+/datum/component/basic_inhands
+ /// Layer index we show our inhands upon
+ var/display_layer
+ /// Y offset to apply to inhands
+ var/y_offset
+ /// X offset to apply to inhands, is inverted for the left hand
+ var/x_offset
+ /// What overlays are we currently showing?
+ var/list/cached_overlays
+
+/datum/component/basic_inhands/Initialize(display_layer = 1, y_offset = 0, x_offset = 0)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.display_layer = display_layer
+ src.y_offset = y_offset
+ src.x_offset = x_offset
+ cached_overlays = list()
+
+/datum/component/basic_inhands/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_updated_overlays))
+ RegisterSignal(parent, COMSIG_MOB_UPDATE_HELD_ITEMS, PROC_REF(on_updated_held_items))
+
+/datum/component/basic_inhands/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_ATOM_UPDATE_OVERLAYS, COMSIG_MOB_UPDATE_HELD_ITEMS))
+
+/// When your overlays update, add your held overlays
+/datum/component/basic_inhands/proc/on_updated_overlays(atom/parent_atom, list/overlays)
+ SIGNAL_HANDLER
+ overlays += cached_overlays
+
+/// When your number of held items changes, regenerate held icons
+/datum/component/basic_inhands/proc/on_updated_held_items(mob/living/holding_mob)
+ SIGNAL_HANDLER
+ var/list/held_overlays = list()
+ for(var/obj/item/held in holding_mob.held_items)
+ var/is_right = holding_mob.get_held_index_of_item(held) % 2 == 0
+ var/icon_file = is_right ? held.righthand_file : held.lefthand_file
+ var/mutable_appearance/held_overlay = held.build_worn_icon(default_layer = HANDS_LAYER, default_icon_file = icon_file, isinhands = TRUE)
+ held_overlay.pixel_y += y_offset
+ held_overlay.pixel_x += x_offset * (is_right ? 1 : -1)
+ held_overlays += held_overlay
+
+ cached_overlays = held_overlays
+ holding_mob.update_appearance(UPDATE_OVERLAYS)
diff --git a/code/datums/components/basic_mob_attack_telegraph.dm b/code/datums/components/basic_mob_attack_telegraph.dm
index 9395937deb68..e693ff61d9ab 100644
--- a/code/datums/components/basic_mob_attack_telegraph.dm
+++ b/code/datums/components/basic_mob_attack_telegraph.dm
@@ -66,7 +66,7 @@
return
ADD_TRAIT(source, TRAIT_BASIC_ATTACK_FORECAST, REF(src))
forget_target(target)
- source.melee_attack(target)
+ source.melee_attack(target, ignore_cooldown = TRUE) // We already started the cooldown when we triggered the forecast
/// The guy we're trying to attack moved, is he still in range?
/datum/component/basic_mob_attack_telegraph/proc/target_moved(atom/target)
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
new file mode 100644
index 000000000000..876ddc87cfeb
--- /dev/null
+++ b/code/datums/components/blob_minion.dm
@@ -0,0 +1,154 @@
+/**
+ * Common behaviour shared by things which are minions to a blob
+ */
+/datum/component/blob_minion
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Overmind who is our boss
+ var/mob/camera/blob/overmind
+ /// Callback to run if overmind strain changes
+ var/datum/callback/on_strain_changed
+
+/datum/component/blob_minion/Initialize(mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/InheritComponent(datum/component/new_comp, i_am_original, mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ if (!isnull(on_strain_changed))
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/proc/register_overlord(mob/camera/blob/overmind)
+ if (isnull(overmind))
+ return
+ src.overmind = overmind
+ overmind.register_new_minion(parent)
+ RegisterSignal(overmind, COMSIG_PARENT_QDELETING, PROC_REF(overmind_deleted))
+ RegisterSignal(overmind, COMSIG_BLOB_SELECTED_STRAIN, PROC_REF(overmind_properties_changed))
+ overmind_properties_changed(overmind, overmind.blobstrain)
+
+/// Our overmind is gone, uh oh!
+/datum/component/blob_minion/proc/overmind_deleted()
+ SIGNAL_HANDLER
+ overmind = null
+ overmind_properties_changed()
+
+/// Our overmind has changed colour and properties
+/datum/component/blob_minion/proc/overmind_properties_changed(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ SIGNAL_HANDLER
+ var/mob/living/living_parent = parent
+ living_parent.update_appearance(UPDATE_ICON)
+ on_strain_changed?.Invoke(overmind, new_strain)
+
+/datum/component/blob_minion/RegisterWithParent()
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags |= PASSBLOB
+ living_parent.faction |= ROLE_BLOB
+ ADD_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ remove_verb(parent, /mob/living/verb/pulled) // No dragging people into the blob
+ RegisterSignal(parent, COMSIG_MOB_MIND_INITIALIZED, PROC_REF(on_mind_init))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_update_appearance))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_update_status_tab))
+ RegisterSignal(parent, COMSIG_ATOM_BLOB_ACT, PROC_REF(on_blob_touched))
+ RegisterSignal(parent, COMSIG_ATOM_FIRE_ACT, PROC_REF(on_burned))
+ RegisterSignal(parent, COMSIG_ATOM_TRIED_PASS, PROC_REF(on_attempted_pass))
+ RegisterSignal(parent, COMSIG_MOVABLE_SPACEMOVE, PROC_REF(on_space_move))
+ RegisterSignal(parent, COMSIG_LIVING_TRY_SPEECH, PROC_REF(on_try_speech))
+ RegisterSignal(parent, COMSIG_MOB_CHANGED_TYPE, PROC_REF(on_transformed))
+ living_parent.update_appearance(UPDATE_ICON)
+ GLOB.blob_telepathy_mobs |= parent
+
+/datum/component/blob_minion/UnregisterFromParent()
+ if (!isnull(overmind))
+ overmind.blob_mobs -= parent
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags &= ~PASSBLOB
+ living_parent.faction -= ROLE_BLOB
+ REMOVE_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ add_verb(parent, /mob/living/verb/pulled)
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_BLOB_ACT,
+ COMSIG_ATOM_FIRE_ACT,
+ COMSIG_ATOM_TRIED_PASS,
+ COMSIG_ATOM_UPDATE_ICON,
+ COMSIG_LIVING_TRY_SPEECH,
+ COMSIG_MOB_CHANGED_TYPE,
+ COMSIG_MOB_GET_STATUS_TAB_ITEMS,
+ COMSIG_MOB_MIND_INITIALIZED,
+ COMSIG_MOVABLE_SPACEMOVE,
+ ))
+ GLOB.blob_telepathy_mobs -= parent
+
+/// Become blobpilled when we gain a mind
+/datum/component/blob_minion/proc/on_mind_init(mob/living/minion, datum/mind/new_mind)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ var/datum/antagonist/blob_minion/minion_motive = new(overmind)
+ new_mind.add_antag_datum(minion_motive)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_appearance(mob/living/minion)
+ SIGNAL_HANDLER
+ if(isnull(overmind))
+ minion.remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ return
+ minion.add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_status_tab(mob/living/minion, list/status_items)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ status_items += "Blobs to Win: [length(overmind.blobs_legit)]/[overmind.blobwincount]"
+
+/// If we feel the gentle caress of a blob, we feel better
+/datum/component/blob_minion/proc/on_blob_touched(mob/living/minion)
+ SIGNAL_HANDLER
+ if(minion.stat == DEAD || minion.health >= minion.maxHealth)
+ return COMPONENT_CANCEL_BLOB_ACT // Don't hurt us in order to heal us
+ for(var/i in 1 to 2)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(parent)) // hello yes you are being healed
+ heal_effect.color = isnull(overmind) ? COLOR_BLACK : overmind.blobstrain.complementary_color
+ minion.heal_overall_damage(minion.maxHealth * BLOBMOB_HEALING_MULTIPLIER)
+ return COMPONENT_CANCEL_BLOB_ACT
+
+/// If we feel the fearsome bite of open flame, we feel worse
+/datum/component/blob_minion/proc/on_burned(mob/living/minion, exposed_temperature, exposed_volume)
+ SIGNAL_HANDLER
+ if(isnull(exposed_temperature))
+ minion.adjustFireLoss(5)
+ return
+ minion.adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
+
+/// Someone is attempting to move through us, allow it if it is a blob tile
+/datum/component/blob_minion/proc/on_attempted_pass(mob/living/minion, atom/movable/incoming)
+ SIGNAL_HANDLER
+ if(istype(incoming, /obj/structure/blob))
+ return COMSIG_COMPONENT_PERMIT_PASSAGE
+
+/// If we're near a blob, stop drifting
+/datum/component/blob_minion/proc/on_space_move(mob/living/minion)
+ SIGNAL_HANDLER
+ var/obj/structure/blob/blob_handhold = locate() in range(1, parent)
+ if (!isnull(blob_handhold))
+ return COMSIG_MOVABLE_STOP_SPACEMOVE
+
+/// We only speak telepathically to blobs
+/datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced)
+ SIGNAL_HANDLER
+ var/spanned_message = minion.say_quote(message)
+ var/rendered = span_blob("\[Blob Telepathy\] [minion.real_name] [spanned_message]")
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, minion)
+ return COMPONENT_CANNOT_SPEAK
+
+/// Called when a blob minion is transformed into something else, hopefully a spore into a zombie
+/datum/component/blob_minion/proc/on_transformed(mob/living/minion, mob/living/replacement)
+ SIGNAL_HANDLER
+ overmind?.assume_direct_control(replacement)
+
+/datum/component/blob_minion/PostTransfer()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
diff --git a/code/datums/components/breeding.dm b/code/datums/components/breeding.dm
new file mode 100644
index 000000000000..9f5a9e278f1f
--- /dev/null
+++ b/code/datums/components/breeding.dm
@@ -0,0 +1,76 @@
+/*
+ * A component to allow us to breed
+ */
+/datum/component/breed
+ /// additional mobs we can breed with
+ var/list/can_breed_with
+ ///path of the baby
+ var/baby_path
+ ///time to wait after breeding
+ var/breed_timer
+ ///AI key we set when we're ready to breed
+ var/breed_key = BB_BREED_READY
+ ///are we ready to breed?
+ var/ready_to_breed = TRUE
+ ///callback after we give birth to the child
+ var/datum/callback/post_birth
+
+/datum/component/breed/Initialize(list/can_breed_with = list(), breed_timer = 40 SECONDS, baby_path, post_birth)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(ishuman(parent)) //sin detected
+ return COMPONENT_INCOMPATIBLE
+
+ if(!ispath(baby_path))
+ stack_trace("attempted to add a breeding component with invalid baby path!")
+ return
+
+ src.can_breed_with = can_breed_with
+ src.breed_timer = breed_timer
+ src.baby_path = baby_path
+ src.post_birth = post_birth
+
+ ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
+/datum/component/breed/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(breed_with_partner))
+ ADD_TRAIT(parent, TRAIT_MOB_BREEDER, REF(src))
+ var/mob/living/parent_mob = parent
+ parent_mob.ai_controller?.set_blackboard_key(breed_key, TRUE)
+
+/datum/component/breed/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
+ REMOVE_TRAIT(parent, TRAIT_MOB_BREEDER, REF(src))
+ post_birth = null
+
+
+/datum/component/breed/proc/breed_with_partner(mob/living/source, mob/living/target)
+ SIGNAL_HANDLER
+
+ if(source.istate & ISTATE_HARM)
+ return
+
+ if(!is_type_in_typecache(target, can_breed_with))
+ return
+
+ if(!HAS_TRAIT(target, TRAIT_MOB_BREEDER) || target.gender == source.gender)
+ return
+
+ if(!ready_to_breed)
+ source.balloon_alert(source, "not ready!")
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+ var/turf/delivery_destination = get_turf(source)
+ var/mob/living/baby = new baby_path(delivery_destination)
+ new /obj/effect/temp_visual/heart(delivery_destination)
+ toggle_status(source)
+
+ addtimer(CALLBACK(src, PROC_REF(toggle_status), source), breed_timer)
+ post_birth?.Invoke(baby, target)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/datum/component/breed/proc/toggle_status(mob/living/source)
+ ready_to_breed = !ready_to_breed
+ source.ai_controller?.set_blackboard_key(BB_BREED_READY, ready_to_breed)
+
diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm
index 2f0175b1eee3..4ff742004592 100644
--- a/code/datums/components/butchering.dm
+++ b/code/datums/components/butchering.dm
@@ -87,10 +87,8 @@
log_combat(user, H, "wounded via throat slitting", source)
H.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD, wound_bonus=CANT_WOUND) // easy tiger, we'll get to that in a sec
var/obj/item/bodypart/slit_throat = H.get_bodypart(BODY_ZONE_HEAD)
- if(slit_throat)
- var/datum/wound/slash/critical/screaming_through_a_slit_throat = new
- screaming_through_a_slit_throat.apply_wound(slit_throat)
- H.apply_status_effect(/datum/status_effect/neck_slice)
+ if (H.cause_wound_of_type_and_severity(WOUND_SLASH, slit_throat, WOUND_SEVERITY_CRITICAL))
+ H.apply_status_effect(/datum/status_effect/neck_slice)
/**
* Handles a user butchering a target
diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm
index dbcacdfeaaf4..afbe6de22228 100644
--- a/code/datums/components/chasm.dm
+++ b/code/datums/components/chasm.dm
@@ -137,7 +137,7 @@
dropped_thing.visible_message(span_boldwarning("[dropped_thing] falls into [parent]!"), span_userdanger("[oblivion_message]"))
if (isliving(dropped_thing))
var/mob/living/falling_mob = dropped_thing
- falling_mob.notransform = TRUE
+ ADD_TRAIT(falling_mob, TRAIT_NO_TRANSFORM, REF(src))
falling_mob.Paralyze(20 SECONDS)
var/oldtransform = dropped_thing.transform
@@ -174,7 +174,7 @@
else if(isliving(dropped_thing))
var/mob/living/fallen_mob = dropped_thing
- fallen_mob.notransform = FALSE
+ REMOVE_TRAIT(fallen_mob, TRAIT_NO_TRANSFORM, REF(src))
if (fallen_mob.stat != DEAD)
fallen_mob.investigate_log("has died from falling into a chasm.", INVESTIGATE_DEATHS)
fallen_mob.death(TRUE)
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index b76c1fd7acb8..5922e05586fa 100644
--- a/code/datums/components/cult_ritual_item.dm
+++ b/code/datums/components/cult_ritual_item.dm
@@ -304,7 +304,7 @@
)
if(cultist.blood_volume)
- cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days?
+ cultist.apply_damage(initial(rune_to_scribe.scribe_damage), BRUTE, pick(GLOB.arm_zones), wound_bonus = CANT_WOUND) // *cuts arm* *bone explodes* ever have one of those days?
var/scribe_mod = initial(rune_to_scribe.scribe_delay)
if(!initial(rune_to_scribe.no_scribe_boost) && (our_turf.type in turfs_that_boost_us))
diff --git a/code/datums/components/curse_of_polymorph.dm b/code/datums/components/curse_of_polymorph.dm
new file mode 100644
index 000000000000..bbfb6c842e30
--- /dev/null
+++ b/code/datums/components/curse_of_polymorph.dm
@@ -0,0 +1,36 @@
+/**
+ * curse of polymorph component;
+ *
+ * Used as a rpgloot suffix and wizard spell!
+ */
+/datum/component/curse_of_polymorph
+ var/polymorph_type
+
+/datum/component/curse_of_polymorph/Initialize(polymorph_type)
+ . = ..()
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.polymorph_type = polymorph_type
+
+/datum/component/curse_of_polymorph/RegisterWithParent()
+ . = ..()
+ var/obj/item/cursed_item = parent
+ RegisterSignal(cursed_item, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip))
+
+/datum/component/curse_of_polymorph/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(
+ COMSIG_ITEM_EQUIPPED,
+ ))
+
+///signal called from equipping parent
+/datum/component/curse_of_polymorph/proc/on_equip(datum/source, mob/living/equipper, slot)
+ SIGNAL_HANDLER
+ var/obj/item/polymorpher_item = parent
+ // Items with no slot flags curse on pickup (because hand slot)
+ if(polymorpher_item.slot_flags && !(polymorpher_item.slot_flags & slot))
+ return
+ ASYNC
+ equipper.dropItemToGround(polymorpher_item, TRUE)
+ equipper.wabbajack(polymorph_type)
+
diff --git a/code/datums/components/damage_chain.dm b/code/datums/components/damage_chain.dm
new file mode 100644
index 000000000000..e472083c4a11
--- /dev/null
+++ b/code/datums/components/damage_chain.dm
@@ -0,0 +1,112 @@
+/**
+ * Draws a line between you and another atom, hurt anyone stood in the line
+ */
+/datum/component/damage_chain
+ dupe_mode = COMPONENT_DUPE_ALLOWED
+ /// How often do we attempt to deal damage?
+ var/tick_interval
+ /// Tracks when we can next deal damage
+ COOLDOWN_DECLARE(tick_cooldown)
+ /// Damage inflicted per tick
+ var/damage_per_tick
+ /// Type of damage to inflict
+ var/damage_type
+ /// Optional callback which checks if we can damage the target
+ var/datum/callback/validate_target
+ /// Optional callback for additional visuals or text display when dealing damage
+ var/datum/callback/chain_damage_feedback
+ /// We will fire the damage feedback callback on every x successful attacks
+ var/feedback_interval
+ /// How many successful attacks have we made?
+ var/successful_attacks = 0
+ /// Time between making any attacks at which we just reset the successful attack counter
+ var/reset_feedback_timer = 0
+ /// Our chain
+ var/datum/beam/chain
+
+/datum/component/damage_chain/Initialize(
+ atom/linked_to,
+ max_distance = 7,
+ beam_icon = 'icons/effects/beam.dmi',
+ beam_state = "medbeam",
+ beam_type = /obj/effect/ebeam,
+ tick_interval = 0.3 SECONDS,
+ damage_per_tick = 1.2,
+ damage_type = BURN,
+ datum/callback/validate_target = null,
+ datum/callback/chain_damage_feedback = null,
+ feedback_interval = 5,
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (!isatom(linked_to))
+ CRASH("Attempted to create [type] linking [parent.type] with non-atom [linked_to]!")
+
+ src.tick_interval = tick_interval
+ src.damage_per_tick = damage_per_tick
+ src.damage_type = damage_type
+ src.validate_target = validate_target
+ src.chain_damage_feedback = chain_damage_feedback
+ src.feedback_interval = feedback_interval
+
+ var/atom/atom_parent = parent
+ chain = atom_parent.Beam(linked_to, icon = beam_icon, icon_state = beam_state, beam_type = beam_type, maxdistance = max_distance)
+ RegisterSignal(chain, COMSIG_PARENT_QDELETING, PROC_REF(end_beam))
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/damage_chain/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(end_beam)) // We actually don't really use many signals it's all processing
+
+/datum/component/damage_chain/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+
+/datum/component/damage_chain/Destroy(force, silent)
+ if (!QDELETED(chain))
+ UnregisterSignal(chain, COMSIG_PARENT_QDELETING)
+ QDEL_NULL(chain)
+ chain = null
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/// Destroy ourself
+/datum/component/damage_chain/proc/end_beam()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/damage_chain/process(seconds_per_tick)
+ var/successful_hit = FALSE
+ var/list/target_turfs = list()
+ for(var/obj/effect/ebeam/chainpart in chain.elements)
+ if (isnull(chainpart) || !chainpart.x || !chainpart.y || !chainpart.z)
+ continue
+ var/turf/overlaps = get_turf_pixel(chainpart)
+ target_turfs |= overlaps
+ if(overlaps == get_turf(chain.origin) || overlaps == get_turf(chain.target))
+ continue
+ for(var/turf/nearby_turf in circle_range(overlaps, 1))
+ target_turfs |= nearby_turf
+
+ for(var/turf/hit_turf as anything in target_turfs)
+ for(var/mob/living/victim in hit_turf)
+ if (victim == parent || victim.stat == DEAD)
+ continue
+ if (!isnull(validate_target) && !validate_target.Invoke(victim))
+ continue
+ if (successful_attacks == 0)
+ chain_damage_feedback?.Invoke(victim)
+ victim.apply_damage(damage_per_tick, damage_type, wound_bonus = CANT_WOUND)
+ successful_hit = TRUE
+
+ if (isnull(chain_damage_feedback))
+ return
+ if (successful_hit)
+ successful_attacks++
+ reset_feedback_timer = addtimer(CALLBACK(src, PROC_REF(reset_feedback)), 10 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+ if (successful_attacks > feedback_interval)
+ reset_feedback()
+
+/// Make it so that the next time we hit something we'll invoke the feedback callback
+/datum/component/damage_chain/proc/reset_feedback()
+ successful_attacks = 0
+ deltimer(reset_feedback_timer)
diff --git a/code/datums/components/direct_explosive_trap.dm b/code/datums/components/direct_explosive_trap.dm
new file mode 100644
index 000000000000..136c29afc11d
--- /dev/null
+++ b/code/datums/components/direct_explosive_trap.dm
@@ -0,0 +1,85 @@
+/**
+ * Responds to certain signals and 'explodes' on the person using the item.
+ * Differs from `interaction_booby_trap` in that this doesn't actually explode, it just directly calls ex_act on one person.
+ */
+/datum/component/direct_explosive_trap
+ /// An optional mob to inform about explosions
+ var/mob/living/saboteur
+ /// Amount of force to apply
+ var/explosive_force
+ /// Colour for examine notification
+ var/glow_colour
+ /// Optional additional target checks before we go off
+ var/datum/callback/explosive_checks
+ /// Signals which set off the bomb, must pass a mob as the first non-source argument
+ var/list/triggering_signals
+
+/datum/component/direct_explosive_trap/Initialize(
+ mob/living/saboteur,
+ explosive_force = EXPLODE_HEAVY,
+ expire_time = 1 MINUTES,
+ glow_colour = COLOR_RED,
+ datum/callback/explosive_checks,
+ list/triggering_signals = list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_BUMPED)
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.saboteur = saboteur
+ src.explosive_force = explosive_force
+ src.glow_colour = glow_colour
+ src.explosive_checks = explosive_checks
+ src.triggering_signals = triggering_signals
+
+ if (expire_time > 0)
+ addtimer(CALLBACK(src, PROC_REF(bomb_expired)), expire_time, TIMER_DELETE_ME)
+
+/datum/component/direct_explosive_trap/RegisterWithParent()
+ if (!(COMSIG_PARENT_EXAMINE in triggering_signals)) // Maybe you're being extra mean with this one
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+ RegisterSignals(parent, triggering_signals, PROC_REF(explode))
+ if (!isnull(saboteur))
+ RegisterSignal(saboteur, COMSIG_PARENT_QDELETING, PROC_REF(on_bomber_deleted))
+
+/datum/component/direct_explosive_trap/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_PARENT_EXAMINE) + triggering_signals)
+ if (!isnull(saboteur))
+ UnregisterSignal(saboteur, COMSIG_PARENT_QDELETING)
+
+/datum/component/direct_explosive_trap/Destroy(force, silent)
+ if (isnull(saboteur))
+ return ..()
+ UnregisterSignal(saboteur, COMSIG_PARENT_QDELETING)
+ saboteur = null
+ return ..()
+
+/// Called if we sit too long without going off
+/datum/component/direct_explosive_trap/proc/bomb_expired()
+ if (!isnull(saboteur))
+ to_chat(saboteur, span_bolddanger("Failure! Your trap didn't catch anyone this time..."))
+ qdel(src)
+
+/// Let people know something is up
+/datum/component/direct_explosive_trap/proc/on_examined(datum/source, mob/user, text)
+ SIGNAL_HANDLER
+ text += span_holoparasite("It glows with a strange light...")
+
+/// Blow up
+/datum/component/direct_explosive_trap/proc/explode(atom/source, mob/living/victim)
+ SIGNAL_HANDLER
+ if (!isliving(victim))
+ return
+ if (!isnull(explosive_checks) && !explosive_checks.Invoke(victim))
+ return
+ to_chat(victim, span_bolddanger("[source] was boobytrapped!"))
+ if (!isnull(saboteur))
+ to_chat(saboteur, span_bolddanger("Success! Your trap on [source] caught [victim.name]!"))
+ playsound(source, 'sound/effects/explosion2.ogg', 200, TRUE)
+ new /obj/effect/temp_visual/explosion(get_turf(source))
+ EX_ACT(victim, explosive_force)
+ qdel(src)
+
+/// Don't hang a reference to the person who placed the bomb
+/datum/component/direct_explosive_trap/proc/on_bomber_deleted()
+ SIGNAL_HANDLER
+ saboteur = null
diff --git a/code/datums/components/embedded.dm b/code/datums/components/embedded.dm
index 8c69fd8100f5..f0c82e61ddf4 100644
--- a/code/datums/components/embedded.dm
+++ b/code/datums/components/embedded.dm
@@ -93,7 +93,8 @@
if(harmful)
victim.throw_alert(ALERT_EMBEDDED_OBJECT, /atom/movable/screen/alert/embeddedobject)
playsound(victim,'sound/weapons/bladeslice.ogg', 40)
- weapon.add_mob_blood(victim)//it embedded itself in you, of course it's bloody!
+ if (limb.can_bleed())
+ weapon.add_mob_blood(victim)//it embedded itself in you, of course it's bloody!
damage += weapon.w_class * impact_pain_mult
victim.add_mood_event("embedded", /datum/mood_event/embedded)
@@ -303,7 +304,7 @@
return
var/damage = weapon.w_class * remove_pain_mult
limb.receive_damage(brute=(1-pain_stam_pct) * damage * 1.5, sharpness=SHARP_EDGED) // Performs exit wounds and flings the user to the caster if nearby
- limb.force_wound_upwards(/datum/wound/pierce/moderate)
+ victim.cause_wound_of_type_and_severity(WOUND_PIERCE, limb, WOUND_SEVERITY_MODERATE)
victim.stamina.adjust(-pain_stam_pct * damage)
playsound(get_turf(victim), 'sound/effects/wounds/blood2.ogg', 50, TRUE)
diff --git a/code/datums/components/explodable.dm b/code/datums/components/explodable.dm
index bf59f7aa3bb5..0cb2a5458024 100644
--- a/code/datums/components/explodable.dm
+++ b/code/datums/components/explodable.dm
@@ -91,13 +91,16 @@
detonate()
///Called when you attack a specific body part of the thing this is equipped on. Useful for exploding pants.
-/datum/component/explodable/proc/explodable_attack_zone(datum/source, damage, damagetype, def_zone)
+/datum/component/explodable/proc/explodable_attack_zone(datum/source, damage, damagetype, def_zone, ...)
SIGNAL_HANDLER
if(!def_zone)
return
if(damagetype != BURN) //Don't bother if it's not fire.
return
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/hitting = def_zone
+ def_zone = hitting.body_zone
if(!is_hitting_zone(def_zone)) //You didn't hit us! ha!
return
detonate()
diff --git a/code/datums/components/fantasy/_fantasy.dm b/code/datums/components/fantasy/_fantasy.dm
index 164d97aefd46..31f969e61932 100644
--- a/code/datums/components/fantasy/_fantasy.dm
+++ b/code/datums/components/fantasy/_fantasy.dm
@@ -17,16 +17,18 @@
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
- src.quality = quality || randomQuality()
+ src.quality = quality
+ if(isnull(src.quality))
+ src.quality = random_quality()
src.canFail = canFail
src.announce = announce
src.affixes = affixes
appliedComponents = list()
if(affixes && affixes.len)
- setAffixes()
+ set_affixes()
else
- randomAffixes()
+ random_affixes()
/datum/component/fantasy/Destroy()
unmodify()
@@ -37,6 +39,11 @@
var/obj/item/master = parent
originalName = master.name
modify()
+ RegisterSignal(parent, COMSIG_STACK_CAN_MERGE, PROC_REF(try_merge_stack))
+
+/datum/component/fantasy/proc/try_merge_stack(obj/item/stack/to_merge, in_hand)
+ SIGNAL_HANDLER
+ return CANCEL_STACK_MERGE
/datum/component/fantasy/UnregisterFromParent()
unmodify()
@@ -53,14 +60,20 @@
src.announce = announce || src.announce
modify()
-/datum/component/fantasy/proc/randomQuality()
+/datum/component/fantasy/proc/random_quality()
var/quality = pick(1;15, 2;14, 2;13, 2;12, 3;11, 3;10, 3;9, 4;8, 4;7, 4;6, 5;5, 5;4, 5;3, 6;2, 6;1, 6;0)
if(prob(50))
quality = -quality
return quality
///proc on creation for random affixes
-/datum/component/fantasy/proc/randomAffixes(force)
+/datum/component/fantasy/proc/random_affixes(force)
+ var/alignment
+ if(quality >= 0)
+ alignment |= AFFIX_GOOD
+ if(quality <= 0)
+ alignment |= AFFIX_EVIL
+
if(!affixListing)
affixListing = list()
for(var/T in subtypesof(/datum/fantasy_affix))
@@ -72,12 +85,6 @@
return
affixes = list()
- var/alignment
- if(quality >= 0)
- alignment |= AFFIX_GOOD
- if(quality <= 0)
- alignment |= AFFIX_EVIL
-
var/usedSlots = NONE
for(var/i in 1 to max(1, abs(quality))) // We want at least 1 affix applied
var/datum/fantasy_affix/affix = pick_weight(affixListing)
@@ -91,7 +98,7 @@
usedSlots |= affix.placement
///proc on creation for specific affixes given to the fantasy component
-/datum/component/fantasy/proc/setAffixes(force)
+/datum/component/fantasy/proc/set_affixes(force)
var/usedSlots = NONE
for(var/datum/fantasy_affix/affix in affixes) // We want at least 1 affix applied
if((affix.placement & usedSlots) || (!affix.validate(parent)))
@@ -101,12 +108,7 @@
/datum/component/fantasy/proc/modify()
var/obj/item/master = parent
-
- master.force = max(0, master.force + quality)
- master.throwforce = max(0, master.throwforce + quality)
- master.set_armor(master.get_armor().generate_new_with_modifiers(list(ARMOR_ALL = quality)))
- master.wound_bonus += quality
- master.bare_wound_bonus += quality
+ master.apply_fantasy_bonuses(quality)
var/newName = originalName
for(var/i in affixes)
@@ -121,10 +123,10 @@
place.visible_message(span_danger("[parent] [span_blue("violently glows blue")] for a while, then evaporates."))
master.burn()
return
- else if(announce)
- announce()
master.name = newName
+ if(announce)
+ announce()
/datum/component/fantasy/proc/unmodify()
var/obj/item/master = parent
@@ -133,12 +135,7 @@
var/datum/fantasy_affix/affix = i
affix.remove(src)
QDEL_LIST(appliedComponents)
-
- master.force = max(0, master.force - quality)
- master.throwforce = max(0, master.throwforce - quality)
- master.set_armor(master.get_armor().generate_new_with_modifiers(list(ARMOR_ALL = -quality)))
- master.wound_bonus -= quality
- master.bare_wound_bonus -= quality
+ master.remove_fantasy_bonuses(quality)
master.name = originalName
@@ -153,4 +150,4 @@
span = ""
effect_description = span_bold("mottled black glow")
- location.visible_message("[span][originalName] is covered by a [effect_description] and then transforms into [parent]!")
+ location.visible_message("[span]The [originalName] is covered by a [effect_description] and then transforms into [parent]!")
diff --git a/code/datums/components/fantasy/prefixes.dm b/code/datums/components/fantasy/prefixes.dm
index 8ac9aa0cef80..cd80a1143130 100644
--- a/code/datums/components/fantasy/prefixes.dm
+++ b/code/datums/components/fantasy/prefixes.dm
@@ -145,7 +145,6 @@
var/obj/item/master = comp.parent
master.RemoveElement(/datum/element/venomous)
-
/datum/fantasy_affix/soul_stealer
name = "soul-stealing"
placement = AFFIX_PREFIX
diff --git a/code/datums/components/fantasy/suffixes.dm b/code/datums/components/fantasy/suffixes.dm
index 4e38d61b0d18..c8809efae491 100644
--- a/code/datums/components/fantasy/suffixes.dm
+++ b/code/datums/components/fantasy/suffixes.dm
@@ -42,9 +42,16 @@
/datum/fantasy_affix/cosmetic_suffixes/apply(datum/component/fantasy/comp, newName)
if(comp.quality > 0 || (comp.quality == 0 && prob(50)))
- return "[newName] of [pick(goodSuffixes)]"
+ . = "[newName] of [pick(goodSuffixes)]"
+ if(comp.quality >= 10)
+ START_PROCESSING(SSprocessing, src)
+ if(comp.quality >= 15)
+ comp.parent.AddComponent(/datum/component/unusual_effect, color = "#FFEA0030", include_particles = TRUE)
+ else
+ comp.parent.AddComponent(/datum/component/unusual_effect, color = "#FFBF0030")
else
- return "[newName] of [pick(badSuffixes)]"
+ . = "[newName] of [pick(badSuffixes)]"
+ return .
//////////// Good suffixes
/datum/fantasy_affix/bane
@@ -189,11 +196,13 @@
name = "curse of hunger"
placement = AFFIX_SUFFIX
alignment = AFFIX_EVIL
+ weight = 5
/datum/fantasy_affix/curse_of_hunger/validate(obj/item/attached)
- //curse of hunger that attaches onto food has the ability to eat itself. it's hilarious.
- if(!IS_EDIBLE(attached))
- return TRUE
+ // Curse of hunger can be really unbearable to deal with,
+ // so it should not start on someone or in a bag.
+ if(!isturf(attached.loc))
+ return FALSE
return TRUE
/datum/fantasy_affix/curse_of_hunger/apply(datum/component/fantasy/comp, newName)
@@ -201,10 +210,64 @@
var/obj/item/master = comp.parent
var/filter_color = "#8a0c0ca1" //clarified args
var/new_name = pick(", eternally hungry", " of the glutton", " cursed with hunger", ", consumer of all", " of the feast")
- master.AddElement(/datum/element/curse_announcement, "[master] is cursed with the curse of hunger!", filter_color, new_name, comp)
+ master.AddElement(/datum/element/curse_announcement, "[master] is cursed with the curse of hunger!", filter_color, "", comp)
comp.appliedComponents += master.AddComponent(/datum/component/curse_of_hunger)
- return newName //no spoilers!
+ return "[newName][new_name]"
/datum/fantasy_affix/curse_of_hunger/remove(datum/component/fantasy/comp)
var/obj/item/master = comp.parent
master.RemoveElement(/datum/element/curse_announcement) //just in case
+
+/datum/fantasy_affix/curse_of_polymorph
+ name = "curse of polymorph"
+ placement = AFFIX_SUFFIX
+ alignment = AFFIX_EVIL
+
+/datum/fantasy_affix/curse_of_polymorph/validate(obj/item/attached)
+ // Don't start on someone so that it doesn't immediately polymorph them.
+ if(ismob(attached.loc))
+ return FALSE
+ if(!isclothing(attached))
+ return FALSE
+ return TRUE
+
+/datum/fantasy_affix/curse_of_polymorph/apply(datum/component/fantasy/comp, newName)
+ . = ..()
+ var/obj/item/master = comp.parent
+ var/filter_color = "#800080a1" //clarified args
+ var/new_name = pick(", transforming", " of the polymorph", " cursed with polymorphing", ", changer of all", " of changing")
+ var/static/list/possible_results = list(
+ WABBAJACK_MONKEY,
+ WABBAJACK_ROBOT,
+ WABBAJACK_SLIME,
+ WABBAJACK_XENO,
+ WABBAJACK_HUMAN,
+ WABBAJACK_ANIMAL,
+ )
+ master.AddElement(/datum/element/curse_announcement, "[master] is cursed with the curse of polymorph!", filter_color, "", comp)
+ comp.appliedComponents += master.AddComponent(/datum/component/curse_of_polymorph, pick(possible_results))
+ return "[newName][new_name]"
+
+/datum/fantasy_affix/curse_of_polymorph/remove(datum/component/fantasy/comp)
+ var/obj/item/master = comp.parent
+ master.RemoveElement(/datum/element/curse_announcement) //just in case
+
+/datum/fantasy_affix/speed
+ name = "of speed"
+ placement = AFFIX_SUFFIX
+ alignment = AFFIX_GOOD
+
+/datum/fantasy_affix/speed/validate(obj/item/attached)
+ if(!istype(attached, /obj/item/clothing/shoes))
+ return FALSE
+ return TRUE
+
+/datum/fantasy_affix/speed/apply(datum/component/fantasy/comp, newName)
+ . = ..()
+ var/obj/item/master = comp.parent
+ master.slowdown = min(-comp.quality / 5, master.slowdown)
+ return "[newName] of speed"
+
+/datum/fantasy_affix/speed/remove(datum/component/fantasy/comp)
+ var/obj/item/master = comp.parent
+ master.slowdown = initial(master.slowdown)
diff --git a/code/datums/components/focused_attacker.dm b/code/datums/components/focused_attacker.dm
new file mode 100644
index 000000000000..2cc0890016ff
--- /dev/null
+++ b/code/datums/components/focused_attacker.dm
@@ -0,0 +1,71 @@
+/**
+ * Increases our attack damage every time we attack the same target
+ * Not compatible with any other component or status effect which modifies attack damage
+ */
+/datum/component/focused_attacker
+ /// Amount of damage we gain per attack
+ var/gain_per_attack
+ /// Maximum amount by which we can increase our attack power
+ var/maximum_gain
+ /// The last thing we attacked
+ var/atom/last_target
+
+/datum/component/focused_attacker/Initialize(gain_per_attack = 5, maximum_gain = 25)
+ . = ..()
+ if (!isliving(parent) && !isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.maximum_gain = maximum_gain
+ src.gain_per_attack = gain_per_attack
+
+/datum/component/focused_attacker/Destroy(force, silent)
+ if (!isnull(last_target))
+ UnregisterSignal(last_target, COMSIG_PARENT_QDELETING)
+ return ..()
+
+/datum/component/focused_attacker/RegisterWithParent()
+ if (isliving(parent))
+ RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK), PROC_REF(pre_mob_attack))
+ else
+ RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(pre_item_attack))
+
+/datum/component/focused_attacker/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_ITEM_PRE_ATTACK))
+
+/// Before a mob attacks, try increasing its attack power
+/datum/component/focused_attacker/proc/pre_mob_attack(mob/living/attacker, atom/target)
+ SIGNAL_HANDLER
+ if (isnull(target) || isturf(target))
+ return
+ if (target == last_target)
+ if (attacker.melee_damage_lower - initial(attacker.melee_damage_lower) >= maximum_gain)
+ return
+ attacker.melee_damage_lower += gain_per_attack
+ attacker.melee_damage_upper += gain_per_attack
+ return
+
+ attacker.melee_damage_lower = initial(attacker.melee_damage_lower)
+ attacker.melee_damage_upper = initial(attacker.melee_damage_upper)
+ register_new_target(target)
+
+/// Before an item attacks, try increasing its attack power
+/datum/component/focused_attacker/proc/pre_item_attack(obj/item/weapon, atom/target, mob/user, params)
+ SIGNAL_HANDLER
+ if (target == last_target)
+ if (weapon.force - initial(weapon.force) < maximum_gain)
+ weapon.force += gain_per_attack
+ return
+
+ weapon.force = initial(weapon.force)
+ register_new_target(target)
+
+/// Register a new target
+/datum/component/focused_attacker/proc/register_new_target(atom/target)
+ if (!isnull(last_target))
+ UnregisterSignal(last_target, COMSIG_PARENT_QDELETING)
+ last_target = target
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_deleted))
+
+/// Drop our target ref on deletion
+/datum/component/focused_attacker/proc/on_target_deleted(target)
+ SIGNAL_HANDLER
+ last_target = null
diff --git a/code/datums/components/food/ice_cream_holder.dm b/code/datums/components/food/ice_cream_holder.dm
index 60da9b207d6e..af2143e2457f 100644
--- a/code/datums/components/food/ice_cream_holder.dm
+++ b/code/datums/components/food/ice_cream_holder.dm
@@ -33,14 +33,16 @@
var/datum/reagent/sweetener
-/datum/component/ice_cream_holder/Initialize(max_scoops = DEFAULT_MAX_ICE_CREAM_SCOOPS,
- change_name = TRUE,
- filled_name,
- change_desc = FALSE,
- x_offset = 0,
- y_offset = 0,
- datum/reagent/sweetener = /datum/reagent/consumable/sugar,
- list/prefill_flavours)
+/datum/component/ice_cream_holder/Initialize(
+ max_scoops = DEFAULT_MAX_ICE_CREAM_SCOOPS,
+ change_name = TRUE,
+ filled_name,
+ change_desc = FALSE,
+ x_offset = 0,
+ y_offset = 0,
+ datum/reagent/sweetener = /datum/reagent/consumable/sugar,
+ list/prefill_flavours,
+)
if(!IS_EDIBLE(parent)) /// There is no easy way to add servings to those non-item edibles, but I won't stop you.
return COMPONENT_INCOMPATIBLE
@@ -169,7 +171,7 @@
if(compare_list(our_scoops, icecream_order.wanted_flavors))
return COMPONENT_CORRECT_ORDER
-/datum/component/ice_cream_holder/proc/sell_ice_cream(obj/item/source, mob/living/simple_animal/robot_customer/sold_to)
+/datum/component/ice_cream_holder/proc/sell_ice_cream(obj/item/source, mob/living/basic/robot_customer/sold_to)
SIGNAL_HANDLER
//the price of ice cream scales with the number of scoops. Yummy.
diff --git a/code/datums/components/gunpoint.dm b/code/datums/components/gunpoint.dm
index 969c3e3a233a..90ba318bb582 100644
--- a/code/datums/components/gunpoint.dm
+++ b/code/datums/components/gunpoint.dm
@@ -161,25 +161,27 @@
qdel(src)
///If the shooter is hit by an attack, they have a 50% chance to flinch and fire. If it hit the arm holding the trigger, it's an 80% chance to fire instead
-/datum/component/gunpoint/proc/flinch(attacker, damage, damagetype, def_zone)
+/datum/component/gunpoint/proc/flinch(mob/living/source, damage_amount, damagetype, def_zone, blocked, wound_bonus, bare_wound_bonus, sharpness, attack_direction, attacking_item)
SIGNAL_HANDLER
- var/mob/living/shooter = parent
- if(attacker == shooter)
- return // somehow this wasn't checked for months but no one tried punching themselves to initiate the shot, amazing
+ if(!attack_direction) // No fliching from yourself
+ return
var/flinch_chance = 50
- var/gun_hand = LEFT_HANDS
+ var/gun_hand = (source.get_held_index_of_item(weapon) % 2) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
- if(shooter.held_items[RIGHT_HANDS] == weapon)
- gun_hand = RIGHT_HANDS
+ if(isbodypart(def_zone))
+ var/obj/item/bodypart/hitting = def_zone
+ def_zone = hitting.body_zone
- if((def_zone == BODY_ZONE_L_ARM && gun_hand == LEFT_HANDS) || (def_zone == BODY_ZONE_R_ARM && gun_hand == RIGHT_HANDS))
+ if(def_zone == gun_hand)
flinch_chance = 80
if(prob(flinch_chance))
- shooter.visible_message(span_danger("[shooter] flinches!"), \
- span_danger("You flinch!"))
+ source.visible_message(
+ span_danger("[source] flinches!"),
+ span_danger("You flinch!"),
+ )
INVOKE_ASYNC(src, PROC_REF(trigger_reaction))
#undef GUNPOINT_DELAY_STAGE_2
diff --git a/code/datums/components/healing_touch.dm b/code/datums/components/healing_touch.dm
index 144cc7833992..965a52de9468 100644
--- a/code/datums/components/healing_touch.dm
+++ b/code/datums/components/healing_touch.dm
@@ -8,10 +8,15 @@
* This intercepts the attack and starts a do_after if the target is in its allowed type list.
*/
/datum/component/healing_touch
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// How much brute damage to heal
var/heal_brute
/// How much burn damage to heal
var/heal_burn
+ /// How much toxin damage to heal
+ var/heal_tox
+ /// How much oxygen damage to heal
+ var/heal_oxy
/// How much stamina damage to heal
var/heal_stamina
/// Interaction will use this key, and be blocked while this key is in use
@@ -26,16 +31,26 @@
var/valid_biotypes
/// Which kinds of carbon limbs can we heal, has no effect on non-carbon mobs. Set to null if you don't care about excluding prosthetics.
var/required_bodytype
- /// How targetting yourself works, expects one of HEALING_TOUCH_ANYONE, HEALING_TOUCH_NOT_SELF, or HEALING_TOUCH_SELF_ONLY
- var/self_targetting
+ /// How targeting yourself works, expects one of HEALING_TOUCH_ANYONE, HEALING_TOUCH_NOT_SELF, or HEALING_TOUCH_SELF_ONLY
+ var/self_targeting
/// Text to print when action starts, replaces %SOURCE% with healer and %TARGET% with healed mob
var/action_text
/// Text to print when action completes, replaces %SOURCE% with healer and %TARGET% with healed mob
var/complete_text
+ /// Whether to print the target's remaining health after healing (for non-carbon targets only)
+ var/show_health
+ /// Color for the healing effect
+ var/heal_color
+ /// Optional click modifier required
+ var/required_modifier
+ /// Callback to run after healing a mob
+ var/datum/callback/after_healed
/datum/component/healing_touch/Initialize(
heal_brute = 20,
heal_burn = 20,
+ heal_tox = 0,
+ heal_oxy = 0,
heal_stamina = 0,
heal_time = 2 SECONDS,
interaction_key = DOAFTER_SOURCE_HEAL_TOUCH,
@@ -43,15 +58,21 @@
list/valid_targets_typecache = list(),
valid_biotypes = MOB_ORGANIC | MOB_MINERAL,
required_bodytype = BODYTYPE_ORGANIC,
- self_targetting = HEALING_TOUCH_NOT_SELF,
+ self_targeting = HEALING_TOUCH_NOT_SELF,
action_text = "%SOURCE% begins healing %TARGET%",
complete_text = "%SOURCE% finishes healing %TARGET%",
+ show_health = FALSE,
+ heal_color = COLOR_HEALING_CYAN,
+ required_modifier = null,
+ datum/callback/after_healed = null,
)
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.heal_brute = heal_brute
src.heal_burn = heal_burn
+ src.heal_tox = heal_tox
+ src.heal_oxy = heal_oxy
src.heal_stamina = heal_stamina
src.heal_time = heal_time
src.interaction_key = interaction_key
@@ -59,13 +80,25 @@
src.valid_targets_typecache = valid_targets_typecache.Copy()
src.valid_biotypes = valid_biotypes
src.required_bodytype = required_bodytype
- src.self_targetting = self_targetting
+ src.self_targeting = self_targeting
src.action_text = action_text
src.complete_text = complete_text
+ src.show_health = show_health
+ src.heal_color = heal_color
+ src.required_modifier = required_modifier
+ src.after_healed = after_healed
RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(try_healing)) // Players
RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(try_healing)) // NPCs
+// Let's populate this list as we actually use it, this thing has too many args
+/datum/component/healing_touch/InheritComponent(
+ datum/component/new_component,
+ i_am_original,
+ heal_color,
+)
+ src.heal_color = heal_color
+
/datum/component/healing_touch/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
return ..()
@@ -75,12 +108,15 @@
return ..()
/// Validate our target, and interrupt the attack chain to start healing it if it is allowed
-/datum/component/healing_touch/proc/try_healing(mob/living/healer, atom/target)
+/datum/component/healing_touch/proc/try_healing(mob/living/healer, atom/target, proximity, modifiers)
SIGNAL_HANDLER
if (!isliving(target))
return
- if (!is_type_in_typecache(target, valid_targets_typecache))
+ if (!isnull(required_modifier) && !LAZYACCESS(modifiers, required_modifier))
+ return
+
+ if (length(valid_targets_typecache) && !is_type_in_typecache(target, valid_targets_typecache))
return // Fall back to attacking it
if (extra_checks && !extra_checks.Invoke(healer, target))
@@ -90,7 +126,7 @@
healer.balloon_alert(healer, "busy!")
return COMPONENT_CANCEL_ATTACK_CHAIN
- switch (self_targetting)
+ switch (self_targeting)
if (HEALING_TOUCH_NOT_SELF)
if (target == healer)
healer.balloon_alert(healer, "can't heal yourself!")
@@ -123,6 +159,14 @@
return FALSE
if (target.stamina.loss > 0 && heal_stamina)
return TRUE
+ if (target.getOxyLoss() > 0 && heal_oxy)
+ return TRUE
+ if (target.getToxLoss() > 0 && heal_tox)
+ return TRUE
+ if (target.getOxyLoss() > 0 && heal_oxy)
+ return TRUE
+ if (target.getToxLoss() > 0 && heal_tox)
+ return TRUE
if (!iscarbon(target))
return (target.getBruteLoss() > 0 && heal_brute) || (target.getFireLoss() > 0 && heal_burn)
var/mob/living/carbon/carbon_target = target
@@ -146,8 +190,26 @@
if (complete_text)
healer.visible_message(span_notice("[format_string(complete_text, healer, target)]"))
- target.heal_overall_damage(brute = heal_brute, burn = heal_burn, stamina = heal_stamina, required_bodytype = required_bodytype)
- new /obj/effect/temp_visual/heal(get_turf(target), COLOR_HEALING_CYAN)
+ var/healed = target.heal_overall_damage(
+ brute = heal_brute,
+ burn = heal_burn,
+ stamina = heal_stamina,
+ required_bodytype = required_bodytype,
+ updating_health = FALSE,
+ )
+ healed += target.adjustOxyLoss(-heal_oxy, updating_health = FALSE, required_biotype = valid_biotypes)
+ healed += target.adjustToxLoss(-heal_tox, updating_health = FALSE, required_biotype = valid_biotypes)
+ if (healed <= 0)
+ return
+
+ target.updatehealth()
+ new /obj/effect/temp_visual/heal(get_turf(target), heal_color)
+ after_healed?.Invoke(target)
+
+ if(!show_health)
+ return
+ var/formatted_string = format_string("%TARGET% now has [health_percentage(target)] health.", healer, target)
+ to_chat(healer, span_danger(formatted_string))
/// Reformats the passed string with the replacetext keys
/datum/component/healing_touch/proc/format_string(string, atom/source, atom/target)
diff --git a/code/datums/components/holderloving.dm b/code/datums/components/holderloving.dm
index e8f797511c2a..bcd073b2600f 100644
--- a/code/datums/components/holderloving.dm
+++ b/code/datums/components/holderloving.dm
@@ -37,6 +37,7 @@
COMSIG_ITEM_EQUIPPED,
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXITED,
+ COMSIG_ITEM_STORED,
), PROC_REF(check_my_loc))
/datum/component/holderloving/UnregisterFromParent()
@@ -46,6 +47,7 @@
COMSIG_ITEM_EQUIPPED,
COMSIG_ATOM_ENTERED,
COMSIG_ATOM_EXITED,
+ COMSIG_ITEM_STORED,
))
/datum/component/holderloving/PostTransfer()
diff --git a/code/datums/components/joint_damage.dm b/code/datums/components/joint_damage.dm
new file mode 100644
index 000000000000..5397bd307cab
--- /dev/null
+++ b/code/datums/components/joint_damage.dm
@@ -0,0 +1,35 @@
+/*
+ * A component given to mobs to damage a linked mob
+ */
+/datum/component/joint_damage
+ ///the mob we will damage
+ var/datum/weakref/overlord_mob
+ ///our last health count
+ var/previous_health_count
+
+/datum/component/joint_damage/Initialize(mob/overlord_mob)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/mob/living/parent_mob = parent
+ previous_health_count = parent_mob.health
+ if(overlord_mob)
+ src.overlord_mob = WEAKREF(overlord_mob)
+
+/datum/component/joint_damage/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(damage_overlord))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(damage_overlord))
+
+/datum/component/joint_damage/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_DEATH))
+
+/datum/component/joint_damage/Destroy()
+ overlord_mob = null
+ return ..()
+
+/datum/component/joint_damage/proc/damage_overlord(mob/living/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/overlord_to_damage = overlord_mob?.resolve()
+ if(!isnull(overlord_to_damage))
+ overlord_to_damage.adjustBruteLoss(previous_health_count - source.health) ///damage or heal overlord
+ previous_health_count = source.health
diff --git a/code/datums/components/leash.dm b/code/datums/components/leash.dm
new file mode 100644
index 000000000000..66771682b1e9
--- /dev/null
+++ b/code/datums/components/leash.dm
@@ -0,0 +1,180 @@
+/// Keeps the parent within the distance of its owner as naturally as possible,
+/// but teleporting if necessary.
+/datum/component/leash
+ /// The owner of the leash. If this is qdeleted, the leash is as well.
+ var/atom/movable/owner
+
+ /// The maximum distance you can move from your owner
+ var/distance
+
+ /// The object type to create on the old turf when forcibly teleporting out
+ var/force_teleport_out_effect
+
+ /// The object type to create on the new turf when forcibly teleporting out
+ var/force_teleport_in_effect
+
+ VAR_PRIVATE
+ // Pathfinding can yield, so only move us closer if this is the best one
+ current_path_tick = 0
+ last_completed_path_tick = 0
+
+ performing_path_move = FALSE
+
+/datum/component/leash/Initialize(
+ atom/movable/owner,
+ distance = 3,
+ force_teleport_out_effect,
+ force_teleport_in_effect,
+)
+ . = ..()
+
+ if (!ismovable(parent))
+ stack_trace("Parent must be a movable")
+ return COMPONENT_INCOMPATIBLE
+
+ if (!ismovable(owner))
+ stack_trace("[owner] (owner) is not a movable")
+ return COMPONENT_INCOMPATIBLE
+
+ if (!isnum(distance))
+ stack_trace("[distance] (distance) must be a number")
+ return COMPONENT_INCOMPATIBLE
+
+ if (!isnull(force_teleport_out_effect) && !ispath(force_teleport_out_effect))
+ stack_trace("force_teleport_out_effect must be null or a path, not [force_teleport_out_effect]")
+ return COMPONENT_INCOMPATIBLE
+
+ if (!isnull(force_teleport_in_effect) && !ispath(force_teleport_in_effect))
+ stack_trace("force_teleport_in_effect must be null or a path, not [force_teleport_in_effect]")
+ return COMPONENT_INCOMPATIBLE
+
+ src.owner = owner
+ src.distance = distance
+ src.force_teleport_out_effect = force_teleport_out_effect
+ src.force_teleport_in_effect = force_teleport_in_effect
+
+ RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(on_owner_qdel))
+
+ var/static/list/container_connections = list(
+ COMSIG_MOVABLE_MOVED = PROC_REF(on_owner_moved),
+ )
+
+ AddComponent(/datum/component/connect_containers, owner, container_connections)
+ RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved))
+ RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move))
+
+ check_distance()
+
+/datum/component/leash/Destroy()
+ owner = null
+ return ..()
+
+/datum/component/leash/proc/set_distance(distance)
+ ASSERT(isnum(distance))
+ src.distance = distance
+ check_distance()
+
+/datum/component/leash/proc/on_owner_qdel()
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ qdel(src)
+
+/datum/component/leash/proc/on_owner_moved(atom/movable/source)
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ check_distance()
+
+/datum/component/leash/proc/on_parent_pre_move(atom/movable/source, atom/new_location)
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ if (performing_path_move)
+ return NONE
+
+ var/turf/new_location_turf = get_turf(new_location)
+ if (get_dist(new_location_turf, owner) <= distance)
+ return NONE
+
+ if (ismob(source))
+ source.balloon_alert(source, "too far!")
+
+ return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
+
+/datum/component/leash/proc/check_distance()
+ set waitfor = FALSE
+ PRIVATE_PROC(TRUE)
+
+ if (get_dist(parent, owner) <= distance)
+ return
+
+ SEND_SIGNAL(parent, COMSIG_LEASH_PATH_STARTED)
+
+ current_path_tick += 1
+ var/our_path_tick = current_path_tick
+
+ var/list/path = get_path_to(parent, owner, mintargetdist = distance)
+
+ if (last_completed_path_tick > our_path_tick)
+ return
+
+ last_completed_path_tick = our_path_tick
+
+ commit_path(path)
+
+/datum/component/leash/proc/commit_path(list/turf/path)
+ SHOULD_NOT_SLEEP(TRUE)
+ PRIVATE_PROC(TRUE)
+
+ performing_path_move = TRUE
+
+ var/atom/movable/movable_parent = parent
+
+ for (var/turf/to_move as anything in path)
+ // Could be an older path, don't make us teleport back
+ if (!to_move.Adjacent(parent))
+ continue
+
+ if (!movable_parent.Move(to_move))
+ force_teleport_back("bad path step")
+ return
+
+ if (get_dist(parent, owner) > distance)
+ force_teleport_back("incomplete path")
+
+ performing_path_move = FALSE
+ SEND_SIGNAL(parent, COMSIG_LEASH_PATH_COMPLETE)
+
+/datum/component/leash/proc/force_teleport_back(reason)
+ PRIVATE_PROC(TRUE)
+
+ var/atom/movable/movable_parent = parent
+
+ SSblackbox.record_feedback("tally", "leash_force_teleport_back", 1, reason)
+
+ if (force_teleport_out_effect)
+ new force_teleport_out_effect(movable_parent.loc)
+
+ movable_parent.forceMove(get_turf(owner))
+
+ if (force_teleport_in_effect)
+ new force_teleport_in_effect(movable_parent.loc)
+
+ if (ismob(movable_parent))
+ movable_parent.balloon_alert(movable_parent, "moved out of range!")
+
+ SEND_SIGNAL(parent, COMSIG_LEASH_FORCE_TELEPORT)
+
+/// A debug spawner that will create a corgi leashed to a bike horn, plus a beam
+/obj/effect/spawner/debug_leash
+
+/obj/effect/spawner/debug_leash/Initialize(mapload)
+ . = ..()
+
+ var/obj/item/bikehorn/bike_horn = new(loc)
+ var/mob/living/basic/pet/dog/corgi/corgi = new(loc)
+
+ corgi.AddComponent(/datum/component/leash, bike_horn)
+
+ corgi.Beam(bike_horn)
diff --git a/code/datums/components/life_link.dm b/code/datums/components/life_link.dm
new file mode 100644
index 000000000000..c65206bc1ff2
--- /dev/null
+++ b/code/datums/components/life_link.dm
@@ -0,0 +1,169 @@
+/**
+ * A mob with this component passes all damage (and healing) it takes to another mob, passed as a parameter
+ * Essentially we use another mob's health bar as our health bar
+ */
+/datum/component/life_link
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Mob we pass all of our damage to
+ var/mob/living/host
+ /// Optional callback invoked when damage gets transferred
+ var/datum/callback/on_passed_damage
+ /// Optional callback invoked when the linked mob dies
+ var/datum/callback/on_linked_death
+
+/datum/component/life_link/Initialize(mob/living/host, datum/callback/on_passed_damage, datum/callback/on_linked_death)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (!istype(host))
+ CRASH("Life link created on [parent.type] and attempted to link to invalid type [host?.type].")
+ register_host(host)
+ src.on_passed_damage = on_passed_damage
+ src.on_linked_death = on_linked_death
+
+/datum/component/life_link/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
+ RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_damage_adjusted))
+ RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_updated))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_status_tab_updated))
+ if (!isnull(host))
+ var/mob/living/living_parent = parent
+ living_parent.updatehealth()
+
+/datum/component/life_link/UnregisterFromParent()
+ unregister_host()
+ UnregisterSignal(parent, list(COMSIG_CARBON_LIMB_DAMAGED, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+
+/datum/component/life_link/InheritComponent(datum/component/new_comp, i_am_original, mob/living/host, datum/callback/on_passed_damage, datum/callback/on_linked_death)
+ register_host(host)
+
+/// Set someone up as our new host
+/datum/component/life_link/proc/register_host(mob/living/new_host)
+ unregister_host()
+ if (isnull(new_host))
+ return
+ host = new_host
+ RegisterSignal(host, COMSIG_LIVING_DEATH, PROC_REF(on_host_died))
+ RegisterSignal(host, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_updated))
+ RegisterSignal(host, COMSIG_LIVING_REVIVE, PROC_REF(on_host_revived))
+ RegisterSignal(host, COMSIG_PARENT_QDELETING, PROC_REF(on_host_deleted))
+ var/mob/living/living_parent = parent
+ living_parent.updatehealth()
+
+/// Drop someone from being our host
+/datum/component/life_link/proc/unregister_host()
+ if (isnull(host))
+ return
+ UnregisterSignal(host, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_REVIVE, COMSIG_PARENT_QDELETING))
+ host = null
+
+/// Called when your damage goes up or down
+/datum/component/life_link/proc/on_damage_adjusted(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (forced)
+ return
+ amount *= our_mob.get_damage_mod(type)
+ switch (type)
+ if(BRUTE)
+ host.adjustBruteLoss(amount, forced = TRUE)
+ if(BURN)
+ host.adjustFireLoss(amount, forced = TRUE)
+ if(TOX)
+ host.adjustToxLoss(amount, forced = TRUE)
+ if(OXY)
+ host.adjustOxyLoss(amount, forced = TRUE)
+ if(CLONE)
+ host.adjustCloneLoss(amount, forced = TRUE)
+
+ on_passed_damage?.Invoke(our_mob, host, amount)
+ return COMPONENT_IGNORE_CHANGE
+
+/// Called when someone hurts one of our limbs, bypassing normal damage adjustment
+/datum/component/life_link/proc/on_limb_damage(mob/living/our_mob, limb, brute, burn)
+ SIGNAL_HANDLER
+ if (brute != 0)
+ host.adjustBruteLoss(brute, updating_health = FALSE)
+ if (burn != 0)
+ host.adjustFireLoss(burn, updating_health = FALSE)
+ if (brute != 0 || burn != 0)
+ host.updatehealth()
+ on_passed_damage?.Invoke(our_mob, host, brute + burn)
+ return COMPONENT_PREVENT_LIMB_DAMAGE
+
+/// Called when either the host or parent's health tries to update, update our displayed health
+/datum/component/life_link/proc/on_health_updated()
+ SIGNAL_HANDLER
+ update_health_hud(parent)
+ update_med_hud_health(parent)
+ update_med_hud_status(parent)
+
+/// Update our parent's health display based on how harmed our host is
+/datum/component/life_link/proc/update_health_hud(mob/living/mob_parent)
+ var/severity = 0
+ var/healthpercent = health_percentage(host)
+ switch(healthpercent)
+ if(100 to INFINITY)
+ severity = 0
+ if(85 to 100)
+ severity = 1
+ if(70 to 85)
+ severity = 2
+ if(55 to 70)
+ severity = 3
+ if(40 to 55)
+ severity = 4
+ if(25 to 40)
+ severity = 5
+ else
+ severity = 6
+ if(severity > 0)
+ mob_parent.overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
+ else
+ mob_parent.clear_fullscreen("brute")
+ if(mob_parent.hud_used?.healths)
+ mob_parent.hud_used.healths.maptext = MAPTEXT("
[round(healthpercent, 0.5)]%
")
+
+/// Update our health on the medical hud
+/datum/component/life_link/proc/update_med_hud_health(mob/living/mob_parent)
+ var/image/holder = mob_parent.hud_list?[HEALTH_HUD]
+ if(isnull(holder))
+ return
+ holder.icon_state = "hud[RoundHealth(host)]"
+ var/icon/size_check = icon(mob_parent.icon, mob_parent.icon_state, mob_parent.dir)
+ holder.pixel_y = size_check.Height() - world.icon_size
+
+/// Update our vital status on the medical hud
+/datum/component/life_link/proc/update_med_hud_status(mob/living/mob_parent)
+ var/image/holder = mob_parent.hud_list?[STATUS_HUD]
+ if(isnull(holder))
+ return
+ var/icon/size_check = icon(mob_parent.icon, mob_parent.icon_state, mob_parent.dir)
+ holder.pixel_y = size_check.Height() - world.icon_size
+ if(host.stat == DEAD || HAS_TRAIT(host, TRAIT_FAKEDEATH))
+ holder.icon_state = "huddead"
+ else
+ holder.icon_state = "hudhealthy"
+
+/// When our status tab updates, draw how much HP our host has in there
+/datum/component/life_link/proc/on_status_tab_updated(mob/living/source, list/items)
+ SIGNAL_HANDLER
+ var/healthpercent = health_percentage(host)
+ items += "Host Health: [round(healthpercent, 0.5)]%"
+
+/// Called when our host dies, we should die too
+/datum/component/life_link/proc/on_host_died(mob/living/source, gibbed)
+ SIGNAL_HANDLER
+ on_linked_death?.Invoke(parent, host, gibbed)
+ var/mob/living/living_parent = parent
+ living_parent.death(gibbed)
+
+/// Called when our host undies, we should undie too
+/datum/component/life_link/proc/on_host_revived(mob/living/source, full_heal_flags)
+ SIGNAL_HANDLER
+ var/mob/living/living_parent = parent
+ living_parent.revive(full_heal_flags)
+
+/// Called when
+/datum/component/life_link/proc/on_host_deleted()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/components/listen_and_repeat.dm b/code/datums/components/listen_and_repeat.dm
new file mode 100644
index 000000000000..44a914a1b90a
--- /dev/null
+++ b/code/datums/components/listen_and_repeat.dm
@@ -0,0 +1,83 @@
+/// The maximum number of phrases we can store in our speech buffer
+#define MAX_SPEECH_BUFFER_SIZE 500
+/// Tendency we have to ignore radio chatter
+#define RADIO_IGNORE_CHANCE 10
+
+/// Simple element that will deterministically set a value based on stuff that the source has heard and will then compel the source to repeat it.
+/// Requires a valid AI Blackboard.
+/datum/component/listen_and_repeat
+ /// List of things that we start out having in our speech buffer
+ var/list/desired_phrases = null
+ /// The AI Blackboard Key we assign the value to.
+ var/blackboard_key = null
+ /// Probability we speak
+ var/speech_probability = null
+ /// Probabiliy we switch our phrase
+ var/switch_phrase_probability = null
+ /// List of things that we've heard and will repeat.
+ var/list/speech_buffer = null
+
+/datum/component/listen_and_repeat/Initialize(list/desired_phrases, blackboard_key)
+ . = ..()
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(!isnull(desired_phrases))
+ LAZYADD(speech_buffer, desired_phrases)
+
+ src.blackboard_key = blackboard_key
+
+ RegisterSignal(parent, COMSIG_MOVABLE_PRE_HEAR, PROC_REF(on_hear))
+ RegisterSignal(parent, COMSIG_NEEDS_NEW_PHRASE, PROC_REF(set_new_blackboard_phrase))
+ RegisterSignal(parent, COMSIG_LIVING_WRITE_MEMORY, PROC_REF(on_write_memory))
+
+ ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
+/datum/component/listen_and_repeat/Destroy(force, silent)
+ REMOVE_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+ return ..()
+
+/// Called when we hear something.
+/datum/component/listen_and_repeat/proc/on_hear(datum/source, list/passed_arguments)
+ SIGNAL_HANDLER
+
+ var/message = passed_arguments[HEARING_RAW_MESSAGE]
+ var/speaker = passed_arguments[HEARING_SPEAKER]
+ var/over_radio = !!passed_arguments[HEARING_RADIO_FREQ]
+ if(speaker == source) // don't parrot ourselves
+ return
+
+ if(over_radio && prob(RADIO_IGNORE_CHANCE))
+ return
+
+ var/number_of_excess_strings = LAZYLEN(speech_buffer) - MAX_SPEECH_BUFFER_SIZE
+ if(number_of_excess_strings > 0) // only remove if we're overfull
+ for(var/i in 1 to number_of_excess_strings)
+ LAZYREMOVE(speech_buffer, pick(speech_buffer))
+
+ LAZYOR(speech_buffer, html_decode(message))
+
+/// Called to set a new value for the blackboard key.
+/datum/component/listen_and_repeat/proc/set_new_blackboard_phrase(datum/source)
+ SIGNAL_HANDLER
+ var/atom/movable/atom_source = source
+ var/datum/ai_controller/controller = atom_source.ai_controller
+ if(!LAZYLEN(speech_buffer))
+ controller.clear_blackboard_key(blackboard_key)
+ return NO_NEW_PHRASE_AVAILABLE
+
+ var/selected_phrase = pick(speech_buffer)
+ controller.set_blackboard_key(blackboard_key, selected_phrase)
+
+/// Exports all the speech buffer data to a dedicated blackboard key on the source.
+/datum/component/listen_and_repeat/proc/on_write_memory(datum/source, dead, gibbed)
+ SIGNAL_HANDLER
+ var/atom/movable/atom_source = source
+ var/datum/ai_controller/controller = atom_source.ai_controller
+ if(LAZYLEN(speech_buffer)) // what? well whatever let's just move on
+ return
+
+ controller.set_blackboard_key(BB_EXPORTABLE_STRING_BUFFER_LIST, speech_buffer.Copy())
+
+#undef MAX_SPEECH_BUFFER_SIZE
+#undef RADIO_IGNORE_CHANCE
diff --git a/code/datums/components/lock_on_cursor.dm b/code/datums/components/lock_on_cursor.dm
index 1811a13aa9b0..71c729b03f35 100644
--- a/code/datums/components/lock_on_cursor.dm
+++ b/code/datums/components/lock_on_cursor.dm
@@ -8,13 +8,13 @@
*/
/datum/component/lock_on_cursor
dupe_mode = COMPONENT_DUPE_ALLOWED
- /// Appearance to overlay onto whatever we are targetting
+ /// Appearance to overlay onto whatever we are targeting
var/mutable_appearance/lock_appearance
/// Current images we are displaying to the client
var/list/image/lock_images
/// Typecache of things we are allowed to target
var/list/target_typecache
- /// Cache of weakrefs to ignore targetting formatted as `list(weakref = TRUE)`
+ /// Cache of weakrefs to ignore targeting formatted as `list(weakref = TRUE)`
var/list/immune_weakrefs
/// Number of things we can target at once
var/lock_amount
diff --git a/code/datums/components/mob_chain.dm b/code/datums/components/mob_chain.dm
new file mode 100644
index 000000000000..f3cda5cf22d1
--- /dev/null
+++ b/code/datums/components/mob_chain.dm
@@ -0,0 +1,227 @@
+/**
+ * Component allowing you to create a linked list of mobs.
+ * These mobs will follow each other and attack as one, as well as sharing damage taken.
+ */
+/datum/component/mob_chain
+
+ /// If true then damage we take is passed backwards along the line
+ var/pass_damage_back
+ /// If true then we will set our icon state based on line position
+ var/vary_icon_state
+
+ /// Mob in front of us in the chain
+ var/mob/living/front
+ /// Mob behind us in the chain
+ var/mob/living/back
+
+/datum/component/mob_chain/Initialize(mob/living/front, pass_damage_back = TRUE, vary_icon_state = FALSE)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.front = front
+ src.pass_damage_back = pass_damage_back
+ src.vary_icon_state = vary_icon_state
+ if (!isnull(front))
+ SEND_SIGNAL(front, COMSIG_MOB_GAINED_CHAIN_TAIL, parent)
+ parent.AddComponent(/datum/component/leash, owner = front, distance = 1) // Handles catching up gracefully
+ var/mob/living/living_parent = parent
+ living_parent.set_glide_size(front.glide_size)
+
+/datum/component/mob_chain/Destroy(force, silent)
+ if (!isnull(front))
+ SEND_SIGNAL(front, COMSIG_MOB_LOST_CHAIN_TAIL, parent)
+ front = null
+ back = null
+ return ..()
+
+/datum/component/mob_chain/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_GAINED_CHAIN_TAIL, PROC_REF(on_gained_tail))
+ RegisterSignal(parent, COMSIG_MOB_LOST_CHAIN_TAIL, PROC_REF(on_lost_tail))
+ RegisterSignal(parent, COMSIG_MOB_CHAIN_CONTRACT, PROC_REF(on_contracted))
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_deletion))
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ RegisterSignal(parent, COMSIG_ATOM_CAN_BE_PULLED, PROC_REF(on_pulled))
+ RegisterSignals(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, COMSIG_MOB_ATTACK_RANGED), PROC_REF(on_attack))
+ RegisterSignal(parent, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(on_glide_size_changed))
+ if (vary_icon_state)
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_update_icon_state))
+ update_mob_appearance()
+ if (pass_damage_back)
+ RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_adjust_damage))
+ RegisterSignal(parent, COMSIG_LIVING_ADJUST_STAMINA_DAMAGE, PROC_REF(on_adjust_stamina))
+ RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
+
+ var/datum/action/cooldown/worm_contract/shrink = new(parent)
+ shrink.Grant(parent)
+
+/datum/component/mob_chain/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_CAN_BE_PULLED,
+ COMSIG_ATOM_UPDATE_ICON_STATE,
+ COMSIG_CARBON_LIMB_DAMAGED,
+ COMSIG_HUMAN_EARLY_UNARMED_ATTACK,
+ COMSIG_LIVING_ADJUST_BRUTE_DAMAGE,
+ COMSIG_LIVING_ADJUST_BURN_DAMAGE,
+ COMSIG_LIVING_ADJUST_CLONE_DAMAGE,
+ COMSIG_LIVING_DEATH,
+ COMSIG_LIVING_ADJUST_OXY_DAMAGE,
+ COMSIG_LIVING_ADJUST_STAMINA_DAMAGE,
+ COMSIG_LIVING_ADJUST_TOX_DAMAGE,
+ COMSIG_LIVING_UNARMED_ATTACK,
+ COMSIG_MOB_ATTACK_RANGED,
+ COMSIG_MOB_CHAIN_CONTRACT,
+ COMSIG_MOB_GAINED_CHAIN_TAIL,
+ COMSIG_MOB_LOST_CHAIN_TAIL,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_MOVABLE_UPDATE_GLIDE_SIZE,
+ COMSIG_PARENT_QDELETING,
+ ))
+ qdel(parent.GetComponent(/datum/component/leash))
+ var/mob/living/living_parent = parent
+ var/datum/action/cooldown/worm_contract/shrink = locate() in living_parent.actions
+ qdel(shrink)
+
+/// Update how we look
+/datum/component/mob_chain/proc/update_mob_appearance()
+ if (!vary_icon_state)
+ return
+ var/mob/living/body = parent
+ body.update_appearance(UPDATE_ICON_STATE)
+
+/// Called when something sets us as IT'S front
+/datum/component/mob_chain/proc/on_gained_tail(mob/living/body, mob/living/tail)
+ SIGNAL_HANDLER
+ back = tail
+ update_mob_appearance()
+
+/// Called when our tail loses its chain component
+/datum/component/mob_chain/proc/on_lost_tail()
+ SIGNAL_HANDLER
+ back = null
+ update_mob_appearance()
+
+/// Called when our tail gets pulled up to our body
+/datum/component/mob_chain/proc/on_contracted(mob/living/shrinking)
+ SIGNAL_HANDLER
+ if (isnull(back))
+ return
+ back.forceMove(shrinking.loc)
+ var/datum/action/cooldown/worm_contract/shrink = locate() in back.actions
+ if (isnull(shrink))
+ return
+ INVOKE_ASYNC(shrink, TYPE_PROC_REF(/datum/action, Trigger))
+
+/// If we die so does the guy behind us, then stop following the leader
+/datum/component/mob_chain/proc/on_death()
+ SIGNAL_HANDLER
+ back?.death()
+ qdel(src)
+
+/// If we get deleted so does the guy behind us
+/datum/component/mob_chain/proc/on_deletion()
+ SIGNAL_HANDLER
+ QDEL_NULL(back)
+ front?.update_appearance(UPDATE_ICON)
+
+/// Pull our tail behind us when we move
+/datum/component/mob_chain/proc/on_moved(mob/living/mover, turf/old_loc)
+ SIGNAL_HANDLER
+ if(isnull(back) || back.loc == old_loc)
+ return
+ back.Move(old_loc)
+
+/// Update our visuals based on if we have someone in front and behind
+/datum/component/mob_chain/proc/on_update_icon_state(mob/living/our_mob)
+ SIGNAL_HANDLER
+ var/current_icon_state = our_mob.base_icon_state
+ if(isnull(front))
+ current_icon_state = "[current_icon_state]_start"
+ else if(isnull(back))
+ current_icon_state = "[current_icon_state]_end"
+ else
+ current_icon_state = "[current_icon_state]_mid"
+
+ our_mob.icon_state = current_icon_state
+ if (isanimal_or_basicmob(our_mob))
+ var/mob/living/basic/basic_parent = our_mob
+ basic_parent.icon_living = current_icon_state
+
+/// Do not allow someone to be pulled out of the chain
+/datum/component/mob_chain/proc/on_pulled(mob/living/our_mob)
+ SIGNAL_HANDLER
+ if (!isnull(front))
+ return COMSIG_ATOM_CANT_PULL
+
+/// Tell our tail to attack too
+/datum/component/mob_chain/proc/on_attack(mob/living/our_mob, atom/target)
+ SIGNAL_HANDLER
+ if (target == back || target == front)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ if (isnull(back) || QDELETED(target))
+ return
+ INVOKE_ASYNC(back, TYPE_PROC_REF(/mob, ClickOn), target)
+
+/// Maintain glide size backwards
+/datum/component/mob_chain/proc/on_glide_size_changed(mob/living/our_mob, new_size)
+ SIGNAL_HANDLER
+ back?.set_glide_size(new_size)
+
+/// On gain or lose stamina, adjust our tail too
+/datum/component/mob_chain/proc/on_adjust_stamina(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (forced)
+ return
+ back?.stamina.adjust(-amount, forced)
+
+/// On damage or heal, affect our furthest segment
+/datum/component/mob_chain/proc/on_adjust_damage(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (isnull(back) || forced)
+ return
+ if (type == STAMINA)
+ back.stamina.adjust(-amount, forced)
+ return // Pass stamina changes all the way along so we maintain consistent speed
+ switch (type)
+ if(BRUTE)
+ back.adjustBruteLoss(amount, forced = forced)
+ if(BURN)
+ back.adjustFireLoss(amount, forced = forced)
+ if(TOX)
+ back.adjustToxLoss(amount, forced = forced)
+ if(OXY) // If all segments are suffocating we pile damage backwards until our ass starts dying forwards
+ back.adjustOxyLoss(amount, forced = forced)
+ if(CLONE)
+ back.adjustCloneLoss(amount, forced = forced)
+ return COMPONENT_IGNORE_CHANGE
+
+/// Special handling for if damage is delegated to a mob's limbs instead of its overall damage
+/datum/component/mob_chain/proc/on_limb_damage(mob/living/our_mob, limb, brute, burn)
+ SIGNAL_HANDLER
+ if (isnull(back))
+ return
+ if (brute != 0)
+ back.adjustBruteLoss(brute, updating_health = FALSE)
+ if (burn != 0)
+ back.adjustFireLoss(burn, updating_health = FALSE)
+ if (brute != 0 || burn != 0)
+ back.updatehealth()
+ return COMPONENT_PREVENT_LIMB_DAMAGE
+
+/**
+ * Shrink the chain of mobs into one tile.
+ */
+/datum/action/cooldown/worm_contract
+ name = "Force Contract"
+ desc = "Forces your body to contract onto a single tile."
+ background_icon_state = "bg_heretic"
+ overlay_icon_state = "bg_heretic_border"
+ button_icon = 'icons/mob/actions/actions_ecult.dmi'
+ button_icon_state = "worm_contract"
+ cooldown_time = 30 SECONDS
+ melee_cooldown_time = 0 SECONDS
+
+/datum/action/cooldown/worm_contract/Activate(atom/target)
+ SEND_SIGNAL(owner, COMSIG_MOB_CHAIN_CONTRACT)
+ StartCooldown()
diff --git a/code/datums/components/pellet_cloud.dm b/code/datums/components/pellet_cloud.dm
index 620f28f1c134..c54ac6b430b0 100644
--- a/code/datums/components/pellet_cloud.dm
+++ b/code/datums/components/pellet_cloud.dm
@@ -307,7 +307,7 @@
var/damage_dealt = wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE]
var/w_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS]
var/bw_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS]
- var/wound_type = (initial(P.damage_type) == BRUTE) ? WOUND_BLUNT : WOUND_BURN // sharpness is handled in the wound rolling
+ var/wounding_type = (initial(P.damage_type) == BRUTE) ? WOUND_BLUNT : WOUND_BURN // sharpness is handled in the wound rolling
wound_info_by_part -= hit_part
// technically this only checks armor worn the moment that all the pellets resolve rather than as each one hits you,
@@ -320,7 +320,7 @@
armor_factor *= ARMOR_WEAKENED_MULTIPLIER
damage_dealt *= max(0, 1 - armor_factor*0.01)
- hit_part.painless_wound_roll(wound_type, damage_dealt, w_bonus, bw_bonus, initial(P.sharpness))
+ hit_part.painless_wound_roll(wounding_type, damage_dealt, w_bonus, bw_bonus, initial(P.sharpness))
var/limb_hit_text = ""
if(hit_part)
diff --git a/code/datums/components/pet_commands/fetch.dm b/code/datums/components/pet_commands/fetch.dm
index a8723f8bd4bd..fa0b3193a447 100644
--- a/code/datums/components/pet_commands/fetch.dm
+++ b/code/datums/components/pet_commands/fetch.dm
@@ -3,7 +3,7 @@
* Watch for someone throwing or pointing at something and then go get it and bring it back.
* If it's food we might eat it instead.
*/
-/datum/pet_command/point_targetting/fetch
+/datum/pet_command/point_targeting/fetch
command_name = "Fetch"
command_desc = "Command your pet to retrieve something you throw or point at."
radial_icon = 'icons/mob/actions/actions_spells.dmi'
@@ -16,20 +16,22 @@
/// If true, this is a poorly trained pet who will eat food you throw instead of bringing it back
var/will_eat_targets = TRUE
-/datum/pet_command/point_targetting/fetch/New(mob/living/parent)
+/datum/pet_command/point_targeting/fetch/New(mob/living/parent)
. = ..()
+ if(isnull(parent))
+ return
parent.AddElement(/datum/element/ai_held_item) // We don't remove this on destroy because they might still be holding something
-/datum/pet_command/point_targetting/fetch/add_new_friend(mob/living/tamer)
+/datum/pet_command/point_targeting/fetch/add_new_friend(mob/living/tamer)
. = ..()
RegisterSignal(tamer, COMSIG_MOB_THROW, PROC_REF(listened_throw))
-/datum/pet_command/point_targetting/fetch/remove_friend(mob/living/unfriended)
+/datum/pet_command/point_targeting/fetch/remove_friend(mob/living/unfriended)
. = ..()
UnregisterSignal(unfriended, COMSIG_MOB_THROW)
/// A friend has thrown something, if we're listening or at least not busy then go get it
-/datum/pet_command/point_targetting/fetch/proc/listened_throw(mob/living/carbon/thrower)
+/datum/pet_command/point_targeting/fetch/proc/listened_throw(mob/living/carbon/thrower)
SIGNAL_HANDLER
var/mob/living/parent = weak_parent.resolve()
@@ -55,7 +57,7 @@
RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(listen_throw_land))
/// A throw we were listening to has finished, see if it's in range for us to try grabbing it
-/datum/pet_command/point_targetting/fetch/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum)
+/datum/pet_command/point_targeting/fetch/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum)
SIGNAL_HANDLER
UnregisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED)
@@ -72,7 +74,7 @@
parent.ai_controller.set_blackboard_key(BB_FETCH_DELIVER_TO, throwing_datum.thrower)
// Don't try and fetch turfs or anchored objects if someone points at them
-/datum/pet_command/point_targetting/fetch/look_for_target(mob/living/pointing_friend, obj/item/pointed_atom)
+/datum/pet_command/point_targeting/fetch/look_for_target(mob/living/pointing_friend, obj/item/pointed_atom)
if (!istype(pointed_atom))
return FALSE
if (pointed_atom.anchored)
@@ -85,7 +87,7 @@
parent.ai_controller.set_blackboard_key(BB_FETCH_DELIVER_TO, pointing_friend)
// Finally, plan our actions
-/datum/pet_command/point_targetting/fetch/execute_action(datum/ai_controller/controller)
+/datum/pet_command/point_targeting/fetch/execute_action(datum/ai_controller/controller)
controller.queue_behavior(/datum/ai_behavior/forget_failed_fetches)
var/atom/target = controller.blackboard[BB_CURRENT_PET_TARGET]
diff --git a/code/datums/components/pet_commands/pet_command.dm b/code/datums/components/pet_commands/pet_command.dm
index 7899ffce40ae..7762b9b2aa81 100644
--- a/code/datums/components/pet_commands/pet_command.dm
+++ b/code/datums/components/pet_commands/pet_command.dm
@@ -109,25 +109,25 @@
CRASH("Pet command execute action not implemented.")
/**
- * # Point Targetting Pet Command
+ * # Point Targeting Pet Command
* As above but also listens for you pointing at something and marks it as a target
*/
-/datum/pet_command/point_targetting
+/datum/pet_command/point_targeting
/// Text describing an action we perform upon receiving a new target
var/pointed_reaction
- /// Blackboard key for targetting datum, this is likely going to need it
- var/targetting_datum_key = BB_PET_TARGETTING_DATUM
+ /// Blackboard key for targeting strategy, this is likely going to need it
+ var/targeting_strategy_key = BB_PET_TARGETING_STRATEGY
-/datum/pet_command/point_targetting/add_new_friend(mob/living/tamer)
+/datum/pet_command/point_targeting/add_new_friend(mob/living/tamer)
. = ..()
RegisterSignal(tamer, COMSIG_MOB_POINTED, PROC_REF(look_for_target))
-/datum/pet_command/point_targetting/remove_friend(mob/living/unfriended)
+/datum/pet_command/point_targeting/remove_friend(mob/living/unfriended)
. = ..()
UnregisterSignal(unfriended, COMSIG_MOB_POINTED)
/// Target the pointed atom for actions
-/datum/pet_command/point_targetting/proc/look_for_target(mob/living/friend, atom/pointed_atom)
+/datum/pet_command/point_targeting/proc/look_for_target(mob/living/friend, atom/pointed_atom)
SIGNAL_HANDLER
var/mob/living/parent = weak_parent.resolve()
diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm
index a562c1168432..211b995b22e2 100644
--- a/code/datums/components/pet_commands/pet_commands_basic.dm
+++ b/code/datums/components/pet_commands/pet_commands_basic.dm
@@ -100,7 +100,7 @@
* # Pet Command: Attack
* Tells a pet to chase and bite the next thing you point at
*/
-/datum/pet_command/point_targetting/attack
+/datum/pet_command/point_targeting/attack
command_name = "Attack"
command_desc = "Command your pet to attack things that you point out to it."
radial_icon = 'icons/effects/effects.dmi'
@@ -111,17 +111,17 @@
pointed_reaction = "and growls"
/// Balloon alert to display if providing an invalid target
var/refuse_reaction = "shakes head"
- /// Attack behaviour to use, generally you will want to override this to add some kind of cooldown
+ /// Attack behaviour to use
var/attack_behaviour = /datum/ai_behavior/basic_melee_attack
// Refuse to target things we can't target, chiefly other friends
-/datum/pet_command/point_targetting/attack/set_command_target(mob/living/parent, atom/target)
+/datum/pet_command/point_targeting/attack/set_command_target(mob/living/parent, atom/target)
if (!target)
return
var/mob/living/living_parent = parent
if (!living_parent.ai_controller)
return
- var/datum/targetting_datum/targeter = living_parent.ai_controller.blackboard[targetting_datum_key]
+ var/datum/targeting_strategy/targeter = GET_TARGETING_STRATEGY(living_parent.ai_controller.blackboard[targeting_strategy_key])
if (!targeter)
return
if (!targeter.can_attack(living_parent, target))
@@ -129,21 +129,51 @@
return
return ..()
-/// Display feedback about not targetting something
-/datum/pet_command/point_targetting/attack/proc/refuse_target(mob/living/parent, atom/target)
+/// Display feedback about not targeting something
+/datum/pet_command/point_targeting/attack/proc/refuse_target(mob/living/parent, atom/target)
var/mob/living/living_parent = parent
living_parent.balloon_alert_to_viewers("[refuse_reaction]")
living_parent.visible_message(span_notice("[living_parent] refuses to attack [target]."))
-/datum/pet_command/point_targetting/attack/execute_action(datum/ai_controller/controller)
- controller.queue_behavior(attack_behaviour, BB_CURRENT_PET_TARGET, targetting_datum_key)
+/datum/pet_command/point_targeting/attack/execute_action(datum/ai_controller/controller)
+ controller.queue_behavior(attack_behaviour, BB_CURRENT_PET_TARGET, targeting_strategy_key)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/**
+ * # Breed command. breed with a partner!
+ */
+/datum/pet_command/point_targeting/breed
+ command_name = "Breed"
+ command_desc = "Command your pet to attempt to breed with a partner."
+ radial_icon = 'icons/mob/simple/animal.dmi'
+ radial_icon_state = "heart"
+ speech_commands = list("breed", "consummate")
+
+/datum/pet_command/point_targeting/breed/set_command_target(mob/living/parent, atom/target)
+ if(isnull(target) || !isliving(target))
+ return
+ if(!HAS_TRAIT(parent, TRAIT_MOB_BREEDER) || !HAS_TRAIT(target, TRAIT_MOB_BREEDER))
+ return
+ if(isnull(parent.ai_controller))
+ return
+ if(!parent.ai_controller.blackboard[BB_BREED_READY] || isnull(parent.ai_controller.blackboard[BB_BABIES_PARTNER_TYPES]))
+ return
+ var/mob/living/living_target = target
+ if(!living_target.ai_controller?.blackboard[BB_BREED_READY])
+ return
+ return ..()
+
+/datum/pet_command/point_targeting/breed/execute_action(datum/ai_controller/controller)
+ if(is_type_in_list(controller.blackboard[BB_CURRENT_PET_TARGET], controller.blackboard[BB_BABIES_PARTNER_TYPES]))
+ controller.queue_behavior(/datum/ai_behavior/make_babies, BB_CURRENT_PET_TARGET)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return SUBTREE_RETURN_FINISH_PLANNING
/**
* # Pet Command: Targetted Ability
* Tells a pet to use some kind of ability on the next thing you point at
*/
-/datum/pet_command/point_targetting/use_ability
+/datum/pet_command/point_targeting/use_ability
command_name = "Use ability"
command_desc = "Command your pet to use one of its special skills on something that you point out to it."
radial_icon = 'icons/mob/actions/actions_spells.dmi'
@@ -154,7 +184,7 @@
/// Blackboard key where a reference to some kind of mob ability is stored
var/pet_ability_key
-/datum/pet_command/point_targetting/use_ability/execute_action(datum/ai_controller/controller)
+/datum/pet_command/point_targeting/use_ability/execute_action(datum/ai_controller/controller)
if (!pet_ability_key)
return
var/datum/action/cooldown/using_action = controller.blackboard[pet_ability_key]
@@ -164,3 +194,45 @@
// We also don't check if the cooldown is over because there's no way a pet owner can know that, the behaviour will handle it
controller.queue_behavior(/datum/ai_behavior/pet_use_ability, pet_ability_key, BB_CURRENT_PET_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/pet_command/protect_owner
+ command_name = "Protect owner"
+ command_desc = "Your pet will run to your aid."
+ hidden = TRUE
+ ///the range our owner needs to be in for us to protect him
+ var/protect_range = 9
+ ///the behavior we will use when he is attacked
+ var/protect_behavior = /datum/ai_behavior/basic_melee_attack
+
+/datum/pet_command/protect_owner/add_new_friend(mob/living/tamer)
+ RegisterSignal(tamer, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(set_attacking_target))
+ if(!HAS_TRAIT(tamer, TRAIT_RELAYING_ATTACKER))
+ tamer.AddElement(/datum/element/relay_attackers)
+
+/datum/pet_command/protect_owner/remove_friend(mob/living/unfriended)
+ UnregisterSignal(unfriended, COMSIG_ATOM_WAS_ATTACKED)
+
+/datum/pet_command/protect_owner/execute_action(datum/ai_controller/controller)
+ var/mob/living/victim = controller.blackboard[BB_CURRENT_PET_TARGET]
+ if(QDELETED(victim))
+ return
+ // cancel the action if they're below our given crit stat, OR if we're trying to attack ourselves (this can happen on tamed mobs w/ protect subtree rarely)
+ if(victim.stat > controller.blackboard[BB_TARGET_MINIMUM_STAT] || victim == controller.pawn)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return
+ controller.queue_behavior(protect_behavior, BB_CURRENT_PET_TARGET, BB_PET_TARGETING_STRATEGY)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/pet_command/protect_owner/set_command_active(mob/living/parent, mob/living/victim)
+ . = ..()
+ set_command_target(parent, victim)
+
+/datum/pet_command/protect_owner/proc/set_attacking_target(atom/source, mob/living/attacker)
+ var/mob/living/basic/owner = weak_parent.resolve()
+ if(isnull(owner))
+ return
+ var/mob/living/current_target = owner.ai_controller?.blackboard[BB_CURRENT_PET_TARGET]
+ if(attacker == current_target) //we are already dealing with this target
+ return
+ if(isliving(attacker) && can_see(owner, attacker, protect_range))
+ set_command_active(owner, attacker)
diff --git a/code/datums/components/pinata.dm b/code/datums/components/pinata.dm
index 8f6866c5d949..1056200e3e27 100644
--- a/code/datums/components/pinata.dm
+++ b/code/datums/components/pinata.dm
@@ -33,7 +33,7 @@
return
return COMPONENT_INCOMPATIBLE
-/datum/component/pinata/proc/damage_inflicted(obj/target, damage, damage_type)
+/datum/component/pinata/proc/damage_inflicted(obj/target, damage, damage_type, ...)
SIGNAL_HANDLER
if(damage < minimum_damage || damage_type == STAMINA || damage_type == OXY)
return
diff --git a/code/datums/components/pry_open_door.dm b/code/datums/components/pry_open_door.dm
deleted file mode 100644
index 17e445d25ca8..000000000000
--- a/code/datums/components/pry_open_door.dm
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Attached to a basic mob.
- * Causes attacks on doors to attempt to open them.
- */
-/datum/component/pry_open_door
- /// Odds the attack opens the door
- var/open_chance
- /// Time it takes to open a door with force
- var/force_wait
-
-/datum/component/pry_open_door/Initialize(open_chance = 100, force_wait = 10 SECONDS)
- . = ..()
-
- if(!isbasicmob(parent))
- return COMPONENT_INCOMPATIBLE
- src.open_chance = open_chance
- src.force_wait = force_wait
-
-/datum/component/pry_open_door/RegisterWithParent()
- RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(hostile_attackingtarget))
-
-/datum/component/pry_open_door/UnregisterFromParent()
- UnregisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET)
-
-/datum/component/pry_open_door/proc/hostile_attackingtarget(mob/living/basic/attacker, atom/target, success)
- SIGNAL_HANDLER
-
- if(!success)
- return
- if(istype(target, /obj/machinery/door/airlock) && prob(open_chance))
- var/obj/machinery/door/airlock/airlock_target = target
- INVOKE_ASYNC(src, PROC_REF(open_door), attacker, airlock_target)
-
-/datum/component/pry_open_door/proc/open_door(mob/living/basic/attacker, obj/machinery/door/airlock/airlock_target)
- if(airlock_target.locked)
- to_chat(attacker, span_warning("The airlock's bolts prevent it from being forced!"))
- return
- else if(!airlock_target.allowed(attacker) && airlock_target.hasPower())
- attacker.visible_message(span_warning("We start forcing the [airlock_target] open."), \
- span_hear("You hear a metal screeching sound."))
- playsound(airlock_target, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
- if(!do_after(attacker, force_wait, airlock_target))
- return
- if(airlock_target.locked)
- return
- attacker.visible_message(span_warning("We force the [airlock_target] to open."))
- airlock_target.open(BYPASS_DOOR_CHECKS)
- else if(!airlock_target.hasPower())
- attacker.visible_message(span_warning("We force the [airlock_target] to open."))
- airlock_target.open(FORCING_DOOR_CHECKS)
- else
- airlock_target.open(DEFAULT_DOOR_CHECKS)
diff --git a/code/datums/components/ranged_attacks.dm b/code/datums/components/ranged_attacks.dm
index 1a3bcacd5d05..aecaa2c18086 100644
--- a/code/datums/components/ranged_attacks.dm
+++ b/code/datums/components/ranged_attacks.dm
@@ -8,6 +8,10 @@
var/projectile_type
/// Sound to play when we fire our projectile
var/projectile_sound
+ /// how many shots we will fire
+ var/burst_shots
+ /// intervals between shots
+ var/burst_intervals
/// Time to wait between shots
var/cooldown_time
/// Tracks time between shots
@@ -17,6 +21,8 @@
casing_type,
projectile_type,
projectile_sound = 'sound/weapons/gun/pistol/shot.ogg',
+ burst_shots,
+ burst_intervals = 0.2 SECONDS,
cooldown_time = 3 SECONDS,
)
. = ..()
@@ -32,6 +38,10 @@
CRASH("Set both casing type and projectile type in [parent]'s ranged attacks component! uhoh! stinky!")
if (!casing_type && !projectile_type)
CRASH("Set neither casing type nor projectile type in [parent]'s ranged attacks component! What are they supposed to be attacking with, air?")
+ if(burst_shots <= 1)
+ return
+ src.burst_shots = burst_shots
+ src.burst_intervals = burst_intervals
/datum/component/ranged_attacks/RegisterWithParent()
. = ..()
@@ -49,12 +59,16 @@
return
COOLDOWN_START(src, fire_cooldown, cooldown_time)
INVOKE_ASYNC(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers)
- SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers)
+ if(isnull(burst_shots))
+ return
+ for(var/i in 1 to (burst_shots - 1))
+ addtimer(CALLBACK(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers), i * burst_intervals)
/// Actually fire the damn thing
/datum/component/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers)
if(projectile_type)
firer.fire_projectile(projectile_type, target, projectile_sound)
+ SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers)
return
playsound(firer, projectile_sound, 100, TRUE)
var/turf/startloc = get_turf(firer)
@@ -66,6 +80,8 @@
else
target_zone = ran_zone()
casing.fire_casing(target, firer, null, null, null, target_zone, 0, firer)
+ casing.update_appearance()
casing.AddElement(/datum/element/temporary_atom, 30 SECONDS)
+ SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers)
return
diff --git a/code/datums/components/ranged_mob_full_auto.dm b/code/datums/components/ranged_mob_full_auto.dm
new file mode 100644
index 000000000000..13cf3b1a4952
--- /dev/null
+++ b/code/datums/components/ranged_mob_full_auto.dm
@@ -0,0 +1,203 @@
+#define AUTOFIRE_MOUSEUP 1
+#define AUTOFIRE_MOUSEDOWN 0
+
+/// Allows a mob to autofire by holding down the cursor
+/datum/component/ranged_mob_full_auto
+ /// Delay before attempting to fire again, note that this is just when we make attempts and is separate from mob's actual firing cooldown
+ var/autofire_shot_delay
+ /// Our client for click tracking
+ var/client/clicker
+ /// Are we currently firing?
+ var/is_firing = FALSE
+ /// This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.
+ var/awaiting_status = AUTOFIRE_MOUSEDOWN
+ /// What are we currently shooting at?
+ var/atom/target
+ /// Where are we currently shooting at?
+ var/turf/target_loc
+ /// When will we next try to shoot?
+ COOLDOWN_DECLARE(next_shot_cooldown)
+
+/datum/component/ranged_mob_full_auto/Initialize(autofire_shot_delay = 0.5 SECONDS)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.autofire_shot_delay = autofire_shot_delay
+
+ var/mob/living/living_parent = parent
+ if (isnull(living_parent.client))
+ return
+ on_gained_client(parent)
+
+/datum/component/ranged_mob_full_auto/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_gained_client))
+ RegisterSignal(parent, COMSIG_MOB_LOGOUT, PROC_REF(on_lost_client))
+
+/datum/component/ranged_mob_full_auto/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT))
+
+/datum/component/ranged_mob_full_auto/process(seconds_per_tick)
+ if (!try_shooting())
+ return PROCESS_KILL
+
+/// Try and take a shot, returns false if we are unable to do so and should stop trying
+/datum/component/ranged_mob_full_auto/proc/try_shooting()
+ if (!is_firing)
+ return FALSE
+ if (!COOLDOWN_FINISHED(src, next_shot_cooldown))
+ return TRUE // Don't fire but also keep processing
+
+ var/mob/living/living_parent = parent
+
+ if (isnull(target) || get_turf(target) != target_loc) // Target moved or got destroyed since we last aimed.
+ set_target(target_loc)
+ target = target_loc // So we keep firing on the emptied tile until we move our mouse and find a new target.
+ if (get_dist(living_parent, target) <= 0)
+ set_target(get_step(living_parent, living_parent.dir)) // Shoot in the direction faced if the mouse is on the same tile as we are.
+ target_loc = target
+ else if (!in_view_range(living_parent, target))
+ stop_firing()
+ return FALSE // Can't see shit
+
+ living_parent.face_atom(target)
+ COOLDOWN_START(src, next_shot_cooldown, autofire_shot_delay)
+ living_parent.RangedAttack(target)
+ return TRUE
+
+/// Setter for reference handling
+/datum/component/ranged_mob_full_auto/proc/set_target(atom/new_target)
+ if (!isnull(target))
+ UnregisterSignal(target, COMSIG_PARENT_QDELETING)
+ target = new_target
+ if (!isnull(target))
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_deleted))
+
+/// Don't hang references
+/datum/component/ranged_mob_full_auto/proc/on_target_deleted()
+ SIGNAL_HANDLER
+ set_target(null)
+
+/// When we gain a client, start tracking clicks
+/datum/component/ranged_mob_full_auto/proc/on_gained_client(mob/living/source)
+ SIGNAL_HANDLER
+ clicker = source.client
+ RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(on_mouse_down))
+
+/// When we lose our client, stop functioning
+/datum/component/ranged_mob_full_auto/proc/on_lost_client(mob/living/source)
+ SIGNAL_HANDLER
+ if (!isnull(clicker))
+ UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDOWN, COMSIG_CLIENT_MOUSEDRAG, COMSIG_CLIENT_MOUSEUP))
+ stop_firing()
+ clicker = null
+
+/// On mouse down start shooting!
+/datum/component/ranged_mob_full_auto/proc/on_mouse_down(client/source, atom/target, turf/location, control, params)
+ SIGNAL_HANDLER
+ if (awaiting_status != AUTOFIRE_MOUSEDOWN)
+ return // Avoid a double mousedown with no mouseup
+ var/list/modifiers = params2list(params)
+
+ if (LAZYACCESS(modifiers, SHIFT_CLICK))
+ return
+ if (LAZYACCESS(modifiers, CTRL_CLICK))
+ return
+ if (LAZYACCESS(modifiers, MIDDLE_CLICK))
+ return
+ if (LAZYACCESS(modifiers, RIGHT_CLICK))
+ return
+ if (LAZYACCESS(modifiers, ALT_CLICK))
+ return
+ var/mob/living/living_parent = parent
+ if (!isturf(living_parent.loc) || living_parent.Adjacent(target))
+ return
+
+ if (isnull(location) || istype(target, /atom/movable/screen)) // Clicking on a screen object.
+ if (target.plane != CLICKCATCHER_PLANE) // The clickcatcher is a special case. We want the click to trigger then, under it.
+ return // If we click and drag on our worn backpack, for example, we want it to open instead.
+ set_target(parse_caught_click_modifiers(modifiers, get_turf(source.eye), source))
+ params = list2params(modifiers)
+ if (isnull(target))
+ CRASH("Failed to get the turf under clickcatcher")
+
+ awaiting_status = AUTOFIRE_MOUSEUP
+ source.click_intercept_time = world.time // From this point onwards Click() will no longer be triggered.
+ if (is_firing)
+ stop_firing()
+
+ set_target(target)
+ target_loc = get_turf(target)
+ INVOKE_ASYNC(src, PROC_REF(start_firing))
+
+/// Start tracking mouse movement and processing our shots
+/datum/component/ranged_mob_full_auto/proc/start_firing()
+ if (is_firing)
+ return
+
+ is_firing = TRUE
+ if (!try_shooting()) // First one is immediate
+ stop_firing()
+ return
+
+ clicker.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
+ clicker.mouse_pointer_icon = clicker.mouse_override_icon
+
+ START_PROCESSING(SSprojectiles, src)
+ RegisterSignal(clicker, COMSIG_CLIENT_MOUSEUP, PROC_REF(on_mouse_up))
+ RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, PROC_REF(on_mouse_drag))
+
+/// When the mouse moved let's try and shift our aim
+/datum/component/ranged_mob_full_auto/proc/on_mouse_drag(client/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
+ SIGNAL_HANDLER
+ if (!isnull(over_location))
+ set_target(over_object)
+ target_loc = get_turf(over_object)
+ return
+
+ //This happens when the mouse is over an inventory or screen object, or on entering deep darkness, for example.
+ var/list/modifiers = params2list(params)
+ var/new_target = parse_caught_click_modifiers(modifiers, get_turf(source.eye), source)
+ params = list2params(modifiers)
+
+ if (!isnull(new_target))
+ set_target(new_target)
+ target_loc = new_target
+ return
+
+ if (QDELETED(target)) //No new target acquired, and old one was deleted, get us out of here.
+ stop_firing()
+ CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]. Old target was incidentally QDELETED.")
+
+
+ set_target(get_turf(target)) //If previous target wasn't a turf, let's turn it into one to avoid locking onto a potentially moving target.
+ target_loc = target
+ CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]")
+
+/// When the mouse is released we should stop
+/datum/component/ranged_mob_full_auto/proc/on_mouse_up()
+ SIGNAL_HANDLER
+ if (awaiting_status != AUTOFIRE_MOUSEUP)
+ return
+ stop_firing()
+ return COMPONENT_CLIENT_MOUSEUP_INTERCEPT
+
+/// Stop watching our mouse and processing shots
+/datum/component/ranged_mob_full_auto/proc/stop_firing()
+ if (!is_firing)
+ return
+
+ is_firing = FALSE
+ set_target(null)
+ target_loc = null
+ STOP_PROCESSING(SSprojectiles, src)
+ awaiting_status = AUTOFIRE_MOUSEDOWN
+
+ if (isnull(clicker))
+ return
+ UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDRAG, COMSIG_CLIENT_MOUSEUP))
+ clicker.mouse_override_icon = null
+ clicker.mouse_pointer_icon = null
+
+#undef AUTOFIRE_MOUSEUP
+#undef AUTOFIRE_MOUSEDOWN
diff --git a/code/datums/components/recharging_attacks.dm b/code/datums/components/recharging_attacks.dm
new file mode 100644
index 000000000000..ad4670b2dbfc
--- /dev/null
+++ b/code/datums/components/recharging_attacks.dm
@@ -0,0 +1,66 @@
+/// Reduces the cooldown of a given action upon landing attacks, critting, or killing mobs.
+/datum/component/recharging_attacks
+ /// The target of the most recent attack
+ var/last_target
+ /// The stat of the most recently attacked mob
+ var/last_stat
+ /// The action to recharge when attacking
+ var/datum/action/cooldown/recharged_action
+ /// The amount of cooldown to refund on a successful attack
+ var/attack_refund
+ /// The amount of cooldown to refund when putting a target into critical
+ var/crit_refund
+
+/datum/component/recharging_attacks/Initialize(
+ datum/action/cooldown/recharged_action,
+ attack_refund = 1 SECONDS,
+ crit_refund = 5 SECONDS,
+)
+ . = ..()
+ if (!isbasicmob(parent) || !istype(recharged_action))
+ return COMPONENT_INCOMPATIBLE
+ src.recharged_action = recharged_action
+ src.attack_refund = attack_refund
+ src.crit_refund = crit_refund
+
+/datum/component/recharging_attacks/Destroy()
+ UnregisterSignal(recharged_action, COMSIG_PARENT_QDELETING)
+ recharged_action = null
+ return ..()
+
+/datum/component/recharging_attacks/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(set_old_stat))
+ RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(check_stat))
+ RegisterSignal(recharged_action, COMSIG_PARENT_QDELETING, PROC_REF(on_action_qdel))
+
+/datum/component/recharging_attacks/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_HOSTILE_POST_ATTACKINGTARGET))
+ if(recharged_action)
+ UnregisterSignal(recharged_action, COMSIG_PARENT_QDELETING)
+
+/datum/component/recharging_attacks/proc/set_old_stat(mob/attacker, mob/attacked)
+ SIGNAL_HANDLER
+ if(!isliving(attacked))
+ return
+ last_target = attacked
+ last_stat = attacked.stat
+
+/datum/component/recharging_attacks/proc/check_stat(mob/living/attacker, mob/living/attacked, success)
+ SIGNAL_HANDLER
+ if(!isliving(attacked) || attacked != last_target || attacker.faction_check_atom(attacked))
+ return
+
+ var/final_refund = attack_refund
+ if(QDELETED(attacked) || (attacked.stat == DEAD && last_stat != DEAD)) //The target is dead and we killed them - full refund
+ final_refund = recharged_action.cooldown_time
+ else if(attacked.stat > CONSCIOUS && last_stat == CONSCIOUS) //We knocked the target unconscious - partial refund
+ final_refund = crit_refund
+
+ recharged_action.next_use_time -= final_refund
+ recharged_action.build_all_button_icons()
+
+/datum/component/recharging_attacks/proc/on_action_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/components/regenerator.dm b/code/datums/components/regenerator.dm
index a6182935a3f0..b33e68abcbc5 100644
--- a/code/datums/components/regenerator.dm
+++ b/code/datums/components/regenerator.dm
@@ -45,7 +45,7 @@
deltimer(regeneration_start_timer)
/// When you take damage, reset the cooldown and start processing
-/datum/component/regenerator/proc/on_take_damage(datum/source, damage, damagetype)
+/datum/component/regenerator/proc/on_take_damage(datum/source, damage, damagetype, ...)
SIGNAL_HANDLER
if (damage <= 0)
diff --git a/code/datums/components/revenge_ability.dm b/code/datums/components/revenge_ability.dm
index a4ba7b4fb515..b79f435662bc 100644
--- a/code/datums/components/revenge_ability.dm
+++ b/code/datums/components/revenge_ability.dm
@@ -8,7 +8,7 @@
/// The ability to use when we are attacked
var/datum/action/cooldown/ability
/// Optional datum for validating targets
- var/datum/targetting_datum/targetting
+ var/datum/targeting_strategy/targeting
/// Trigger only if target is at least this far away
var/min_range
/// Trigger only if target is at least this close
@@ -16,12 +16,12 @@
/// Target the ability at ourself instead of at the offender
var/target_self
-/datum/component/revenge_ability/Initialize(datum/action/cooldown/ability, datum/targetting_datum/targetting, min_range = 0, max_range = INFINITY, target_self = FALSE)
+/datum/component/revenge_ability/Initialize(datum/action/cooldown/ability, datum/targeting_strategy/targeting, min_range = 0, max_range = INFINITY, target_self = FALSE)
. = ..()
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.ability = ability
- src.targetting = targetting
+ src.targeting = targeting
src.min_range = min_range
src.max_range = max_range
src.target_self = target_self
@@ -45,7 +45,7 @@
var/distance = get_dist(ability_user, attacker)
if (distance < min_range || distance > max_range)
return
- if (targetting && !targetting.can_attack(victim, attacker))
+ if (targeting && !targeting.can_attack(victim, attacker))
return
INVOKE_ASYNC(ability, TYPE_PROC_REF(/datum/action/cooldown, InterceptClickOn), ability_user, null, (target_self) ? ability_user : attacker)
diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm
index 14358a5c5a38..5a043ef6f069 100644
--- a/code/datums/components/riding/riding.dm
+++ b/code/datums/components/riding/riding.dm
@@ -64,6 +64,8 @@
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(vehicle_moved))
RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(vehicle_bump))
RegisterSignal(parent, COMSIG_BUCKLED_CAN_Z_MOVE, PROC_REF(riding_can_z_move))
+ RegisterSignals(parent, GLOB.movement_type_addtrait_signals, PROC_REF(on_movement_type_trait_gain))
+ RegisterSignals(parent, GLOB.movement_type_removetrait_signals, PROC_REF(on_movement_type_trait_loss))
/**
* This proc handles all of the proc calls to things like set_vehicle_dir_layer() that a type of riding datum needs to call on creation
@@ -84,6 +86,10 @@
unequip_buckle_inhands(rider)
rider.updating_glide_size = TRUE
UnregisterSignal(rider, COMSIG_LIVING_TRY_PULL)
+ for (var/trait in GLOB.movement_type_trait_to_flag)
+ if (HAS_TRAIT(parent, trait))
+ REMOVE_TRAIT(rider, trait, REF(src))
+ REMOVE_TRAIT(rider, TRAIT_NO_FLOATING_ANIM, REF(src))
if(!movable_parent.has_buckled_mobs())
qdel(src)
@@ -99,6 +105,11 @@
rider.stop_pulling()
RegisterSignal(rider, COMSIG_LIVING_TRY_PULL, PROC_REF(on_rider_try_pull))
+ for (var/trait in GLOB.movement_type_trait_to_flag)
+ if (HAS_TRAIT(parent, trait))
+ ADD_TRAIT(rider, trait, REF(src))
+ ADD_TRAIT(rider, TRAIT_NO_FLOATING_ANIM, REF(src))
+
/// This proc is called when the rider attempts to grab the thing they're riding, preventing them from doing so.
/datum/component/riding/proc/on_rider_try_pull(mob/living/rider_pulling, atom/movable/target, force)
SIGNAL_HANDLER
@@ -273,3 +284,24 @@
/datum/component/riding/proc/riding_can_z_move(atom/movable/movable_parent, direction, turf/start, turf/destination, z_move_flags, mob/living/rider)
SIGNAL_HANDLER
return COMPONENT_RIDDEN_ALLOW_Z_MOVE
+
+/// Called when our vehicle gains a movement trait, so we can apply it to the riders
+/datum/component/riding/proc/on_movement_type_trait_gain(atom/movable/source, trait)
+ SIGNAL_HANDLER
+ var/atom/movable/movable_parent = parent
+ for (var/mob/rider in movable_parent.buckled_mobs)
+ ADD_TRAIT(rider, trait, REF(src))
+
+/// Called when our vehicle loses a movement trait, so we can remove it from the riders
+/datum/component/riding/proc/on_movement_type_trait_loss(atom/movable/source, trait)
+ SIGNAL_HANDLER
+ var/atom/movable/movable_parent = parent
+ for (var/mob/rider in movable_parent.buckled_mobs)
+ REMOVE_TRAIT(rider, trait, REF(src))
+
+/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter)
+ SIGNAL_HANDLER
+
+ if((living_hitter in source.buckled_mobs))
+ return
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index 5a829df75f81..d72b9e3c8d7d 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -203,7 +203,7 @@
human_parent.buckle_lying = 0
// the riding mob is made nondense so they don't bump into any dense atoms the carrier is pulling,
// since pulled movables are moved before buckled movables
- riding_mob.set_density(FALSE)
+ ADD_TRAIT(riding_mob, TRAIT_UNDENSE, VEHICLE_TRAIT)
else if(ride_check_flags & CARRIER_NEEDS_ARM) // fireman
human_parent.buckle_lying = 90
@@ -227,7 +227,7 @@
unequip_buckle_inhands(parent)
var/mob/living/carbon/human/H = parent
H.remove_movespeed_modifier(/datum/movespeed_modifier/human_carry)
- former_rider.set_density(!former_rider.body_position)
+ REMOVE_TRAIT(H, TRAIT_UNDENSE, VEHICLE_TRAIT)
return ..()
/// If the carrier shoves the person they're carrying, force the carried mob off
@@ -440,7 +440,7 @@
set_vehicle_dir_layer(WEST, ABOVE_MOB_LAYER)
/datum/component/riding/creature/guardian/ride_check(mob/living/user, consequences = TRUE)
- var/mob/living/simple_animal/hostile/guardian/charger = parent
+ var/mob/living/basic/guardian/charger = parent
if(!istype(charger))
return ..()
return charger.summoner == user
diff --git a/code/datums/components/rot.dm b/code/datums/components/rot.dm
index beea0d209b27..3dafdf420d4a 100644
--- a/code/datums/components/rot.dm
+++ b/code/datums/components/rot.dm
@@ -114,7 +114,7 @@
/datum/component/rot/proc/rot_react_touch(datum/source, mob/living/react_to)
SIGNAL_HANDLER
- rot_react(source, react_to, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ rot_react(source, react_to, pick(GLOB.arm_zones))
/// Triggered when something enters the component's parent.
/datum/component/rot/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
diff --git a/code/datums/components/seethrough_mob.dm b/code/datums/components/seethrough_mob.dm
new file mode 100644
index 000000000000..4359c454f1a1
--- /dev/null
+++ b/code/datums/components/seethrough_mob.dm
@@ -0,0 +1,135 @@
+///A component that lets you turn your character transparent in order to see and click through yourself.
+/datum/component/seethrough_mob
+ ///The atom that enables our dark magic
+ var/atom/movable/render_source_atom
+ ///The fake version of ourselves
+ var/image/trickery_image
+ ///Which alpha do we animate towards?
+ var/target_alpha
+ ///How long our faze in/out takes
+ var/animation_time
+ ///Does this object let clicks from players its transparent to pass through it
+ var/clickthrough
+ ///Is the seethrough effect currently active
+ var/is_active
+ ///The mob's original render_target value
+ var/initial_render_target_value
+ ///This component's personal uid
+ var/personal_uid
+
+/datum/component/seethrough_mob/Initialize(target_alpha = 100, animation_time = 0.5 SECONDS, clickthrough = TRUE)
+ . = ..()
+
+ if(!ismob(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.target_alpha = target_alpha
+ src.animation_time = animation_time
+ src.clickthrough = clickthrough
+ src.is_active = FALSE
+ src.render_source_atom = new()
+
+ var/static/uid = 0
+ uid++
+ src.personal_uid = uid
+
+ render_source_atom.appearance_flags |= ( RESET_COLOR | RESET_TRANSFORM)
+
+ render_source_atom.vis_flags |= (VIS_INHERIT_ID | VIS_INHERIT_PLANE | VIS_INHERIT_LAYER)
+
+ render_source_atom.render_source = "*transparent_bigmob[personal_uid]"
+
+ var/datum/action/cooldown/toggle_seethrough/action = new(src)
+ action.Grant(parent)
+
+/datum/component/seethrough_mob/Destroy(force, silent)
+ QDEL_NULL(render_source_atom)
+ return ..()
+
+///Set up everything we need to trick the client and keep it looking normal for everyone else
+/datum/component/seethrough_mob/proc/trick_mob()
+ SIGNAL_HANDLER
+
+ var/mob/fool = parent
+ var/datum/hud/our_hud = fool.hud_used
+ for(var/atom/movable/screen/plane_master/seethrough as anything in our_hud.get_true_plane_masters(SEETHROUGH_PLANE))
+ seethrough.unhide_plane(fool)
+
+ var/icon/current_mob_icon = icon(fool.icon, fool.icon_state)
+ render_source_atom.pixel_x = -fool.pixel_x
+ render_source_atom.pixel_y = ((current_mob_icon.Height() - 32) * 0.5)
+
+ initial_render_target_value = fool.render_target
+ fool.render_target = "*transparent_bigmob[personal_uid]"
+ fool.vis_contents.Add(render_source_atom)
+
+ trickery_image = new(render_source_atom)
+ trickery_image.loc = render_source_atom
+ trickery_image.override = TRUE
+
+ trickery_image.pixel_x = 0
+ trickery_image.pixel_y = 0
+
+ if(clickthrough)
+ //Special plane so we can click through the overlay
+ SET_PLANE_EXPLICIT(trickery_image, SEETHROUGH_PLANE, fool)
+
+ fool.client.images += trickery_image
+
+ animate(trickery_image, alpha = target_alpha, time = animation_time)
+
+ RegisterSignal(fool, COMSIG_MOB_LOGOUT, PROC_REF(on_client_disconnect))
+
+///Remove the screen object and make us appear solid to ourselves again
+/datum/component/seethrough_mob/proc/untrick_mob()
+ var/mob/fool = parent
+ animate(trickery_image, alpha = 255, time = animation_time)
+ UnregisterSignal(fool, COMSIG_MOB_LOGOUT)
+
+ //after playing the fade-in animation, remove the image and the trick atom
+ addtimer(CALLBACK(src, PROC_REF(clear_image), trickery_image, fool.client), animation_time)
+
+///Remove the image and the trick atom
+/datum/component/seethrough_mob/proc/clear_image(image/removee, client/remove_from)
+ var/atom/movable/atom_parent = parent
+ atom_parent.vis_contents -= render_source_atom
+ atom_parent.render_target = initial_render_target_value
+ remove_from?.images -= removee
+
+///Effect is disabled when they log out because client gets deleted
+/datum/component/seethrough_mob/proc/on_client_disconnect()
+ SIGNAL_HANDLER
+
+ var/mob/fool = parent
+ UnregisterSignal(fool, COMSIG_MOB_LOGOUT)
+ var/datum/hud/our_hud = fool.hud_used
+ for(var/atom/movable/screen/plane_master/seethrough as anything in our_hud.get_true_plane_masters(SEETHROUGH_PLANE))
+ seethrough.hide_plane(fool)
+ clear_image(trickery_image, fool.client)
+
+/datum/component/seethrough_mob/proc/toggle_active()
+ is_active = !is_active
+ if(is_active)
+ trick_mob()
+ else
+ untrick_mob()
+
+/datum/action/cooldown/toggle_seethrough
+ name = "Toggle Seethrough"
+ desc = "Allows you to see behind your massive body and click through it."
+ button_icon = 'icons/mob/actions/actions_xeno.dmi'
+ button_icon_state = "alien_sneak"
+ background_icon_state = "bg_alien"
+ cooldown_time = 1 SECONDS
+ melee_cooldown_time = 0
+
+/datum/action/cooldown/toggle_seethrough/Remove(mob/remove_from)
+ var/datum/component/seethrough_mob/transparency = target
+ if(transparency.is_active)
+ transparency.untrick_mob()
+ return ..()
+
+/datum/action/cooldown/toggle_seethrough/Activate(atom/t)
+ StartCooldown()
+ var/datum/component/seethrough_mob/transparency = target
+ transparency.toggle_active()
diff --git a/code/datums/components/shrink.dm b/code/datums/components/shrink.dm
index 9c1c5f76dcde..67cd3d39e23c 100644
--- a/code/datums/components/shrink.dm
+++ b/code/datums/components/shrink.dm
@@ -10,10 +10,11 @@
parent_atom.transform = parent_atom.transform.Scale(0.5,0.5)
olddens = parent_atom.density
oldopac = parent_atom.opacity
- parent_atom.set_density(FALSE)
+
parent_atom.set_opacity(FALSE)
if(isliving(parent_atom))
var/mob/living/L = parent_atom
+ ADD_TRAIT(L, TRAIT_UNDENSE, SHRUNKEN_TRAIT)
L.add_movespeed_modifier(/datum/movespeed_modifier/shrink_ray)
if(iscarbon(L))
var/mob/living/carbon/C = L
@@ -23,6 +24,8 @@
if(ishuman(C))
var/mob/living/carbon/human/H = C
H.physiology.damage_resistance -= 100//carbons take double damage while shrunk
+ else
+ parent_atom.set_density(FALSE) // this is handled by the UNDENSE trait on mobs
parent_atom.visible_message(span_warning("[parent_atom] shrinks down to a tiny size!"),
span_userdanger("Everything grows bigger!"))
QDEL_IN(src, shrink_time)
@@ -30,12 +33,14 @@
/datum/component/shrink/Destroy()
var/atom/parent_atom = parent
parent_atom.transform = parent_atom.transform.Scale(2,2)
- parent_atom.set_density(olddens)
parent_atom.set_opacity(oldopac)
if(isliving(parent_atom))
var/mob/living/L = parent_atom
L.remove_movespeed_modifier(/datum/movespeed_modifier/shrink_ray)
+ REMOVE_TRAIT(L, TRAIT_UNDENSE, SHRUNKEN_TRAIT)
if(ishuman(L))
var/mob/living/carbon/human/H = L
H.physiology.damage_resistance += 100
+ else
+ parent_atom.set_density(olddens) // this is handled by the UNDENSE trait on mobs
return ..()
diff --git a/code/datums/components/singularity.dm b/code/datums/components/singularity.dm
index 9637a2a746c1..5fdc230c3e14 100644
--- a/code/datums/components/singularity.dm
+++ b/code/datums/components/singularity.dm
@@ -101,7 +101,7 @@
)
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
- RegisterSignal(parent, COMSIG_ATOM_BULLET_ACT, PROC_REF(consume_bullets))
+ RegisterSignal(parent, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(consume_bullets))
if (notify_admins)
admin_investigate_setup()
@@ -127,7 +127,7 @@
COMSIG_ATOM_ATTACK_PAW,
COMSIG_ATOM_BLOB_ACT,
COMSIG_ATOM_BSA_BEAM,
- COMSIG_ATOM_BULLET_ACT,
+ COMSIG_ATOM_PRE_BULLET_ACT,
COMSIG_ATOM_BUMPED,
COMSIG_MOVABLE_PRE_MOVE,
COMSIG_PARENT_ATTACKBY,
@@ -180,6 +180,7 @@
SIGNAL_HANDLER
qdel(projectile)
+ return COMPONENT_BULLET_BLOCKED
/// Calls singularity_act on the thing passed, usually destroying the object
/datum/component/singularity/proc/default_singularity_act(atom/thing)
diff --git a/code/datums/components/sitcomlaughter.dm b/code/datums/components/sitcomlaughter.dm
index 8dfef21b749d..04a190d15f01 100644
--- a/code/datums/components/sitcomlaughter.dm
+++ b/code/datums/components/sitcomlaughter.dm
@@ -1,6 +1,6 @@
/datum/component/wearertargeting/sitcomlaughter
valid_slots = list(ITEM_SLOT_HANDS, ITEM_SLOT_BELT, ITEM_SLOT_ID, ITEM_SLOT_LPOCKET, ITEM_SLOT_RPOCKET, ITEM_SLOT_SUITSTORE, ITEM_SLOT_DEX_STORAGE)
- signals = list(COMSIG_MOB_CREAMED, COMSIG_ON_CARBON_SLIP, COMSIG_ON_VENDOR_CRUSH, COMSIG_MOB_CLUMSY_SHOOT_FOOT)
+ signals = list(COMSIG_MOB_CREAMED, COMSIG_ON_CARBON_SLIP, COMSIG_POST_TILT_AND_CRUSH, COMSIG_MOB_CLUMSY_SHOOT_FOOT)
proctype = PROC_REF(EngageInComedy)
mobtype = /mob/living
///Sounds used for when user has a sitcom action occur
diff --git a/code/datums/components/slippery.dm b/code/datums/components/slippery.dm
index ce9c80c5d348..3e0bbde193cb 100644
--- a/code/datums/components/slippery.dm
+++ b/code/datums/components/slippery.dm
@@ -42,9 +42,29 @@
if(isitem(parent))
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip))
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop))
+ RegisterSignal(parent, COMSIG_ITEM_APPLY_FANTASY_BONUSES, PROC_REF(apply_fantasy_bonuses))
+ RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses))
else
RegisterSignal(parent, COMSIG_ATOM_ENTERED, PROC_REF(Slip))
+/datum/component/slippery/proc/apply_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ knockdown_time = source.modify_fantasy_variable("knockdown_time", knockdown_time, bonus)
+ if(bonus >= 5)
+ paralyze_time = source.modify_fantasy_variable("paralyze_time", paralyze_time, bonus)
+ LAZYSET(source.fantasy_modifications, "lube_flags", lube_flags)
+ lube_flags |= SLIDE
+ if(bonus >= 10)
+ lube_flags |= GALOSHES_DONT_HELP|SLIP_WHEN_CRAWLING
+
+/datum/component/slippery/proc/remove_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ knockdown_time = source.reset_fantasy_variable("knockdown_time", knockdown_time)
+ paralyze_time = source.reset_fantasy_variable("paralyze_time", paralyze_time)
+ var/previous_lube_flags = LAZYACCESS(source.fantasy_modifications, "lube_flags")
+ if(!isnull(previous_lube_flags))
+ lube_flags = previous_lube_flags
+
/datum/component/slippery/proc/add_connect_loc_behalf_to_parent()
if(ismovable(parent))
AddComponent(/datum/component/connect_loc_behalf, parent, default_connections)
diff --git a/code/datums/components/soul_stealer.dm b/code/datums/components/soul_stealer.dm
index 88ce917e7e11..9c0619cfc8c2 100644
--- a/code/datums/components/soul_stealer.dm
+++ b/code/datums/components/soul_stealer.dm
@@ -5,13 +5,16 @@
* Used in the cult bastard sword!
*/
/datum/component/soul_stealer
+ var/obj/item/soulstone/soulstone_type
/// List of soulstones captured by this item.
var/list/obj/item/soulstone/soulstones = list()
-/datum/component/soul_stealer/Initialize()
+/datum/component/soul_stealer/Initialize(soulstone_type = /obj/item/soulstone/anybody/purified)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
+ src.soulstone_type = soulstone_type
+
/datum/component/soul_stealer/Destroy()
QDEL_LIST(soulstones) // We own these, so we'll also just get rid of them. Any souls inside will die, this is fine.
return ..()
@@ -60,7 +63,7 @@
/datum/component/soul_stealer/proc/try_capture(mob/living/carbon/human/victim, mob/living/captor)
if(victim.stat == CONSCIOUS)
return
- var/obj/item/soulstone/soulstone = new(parent)
+ var/obj/item/soulstone/soulstone = new soulstone_type(parent)
soulstone.attack(victim, captor)
if(!length(soulstone.contents)) // failed
qdel(soulstone)
diff --git a/code/datums/components/spirit_holding.dm b/code/datums/components/spirit_holding.dm
index dc11d5fd7292..daae8f8b43f5 100644
--- a/code/datums/components/spirit_holding.dm
+++ b/code/datums/components/spirit_holding.dm
@@ -7,7 +7,7 @@
///bool on if this component is currently polling for observers to inhabit the item
var/attempting_awakening = FALSE
///mob contained in the item.
- var/mob/living/simple_animal/shade/bound_spirit
+ var/mob/living/basic/shade/bound_spirit
/datum/component/spirit_holding/Initialize()
if(!ismovable(parent)) //you may apply this to mobs, i take no responsibility for how that works out
diff --git a/code/datums/components/style/style.dm b/code/datums/components/style/style.dm
index a0d6958582ce..c527653d8a6c 100644
--- a/code/datums/components/style/style.dm
+++ b/code/datums/components/style/style.dm
@@ -452,7 +452,7 @@
// Negative effects
-/datum/component/style/proc/on_take_damage()
+/datum/component/style/proc/on_take_damage(...)
SIGNAL_HANDLER
point_multiplier = round(max(point_multiplier - 0.3, 1), 0.1)
diff --git a/code/datums/components/surgery_initiator.dm b/code/datums/components/surgery_initiator.dm
index af725c1b6982..2f7fb3c3f7c9 100644
--- a/code/datums/components/surgery_initiator.dm
+++ b/code/datums/components/surgery_initiator.dm
@@ -61,7 +61,7 @@
var/list/available_surgeries = get_available_surgeries(user, target)
if(!length(available_surgeries))
- if (target.body_position == LYING_DOWN)
+ if (target.body_position == LYING_DOWN || !(target.mobility_flags & MOBILITY_LIEDOWN))
target.balloon_alert(user, "no surgeries available!")
else
target.balloon_alert(user, "make them lie down!")
@@ -99,7 +99,7 @@
continue
else if(carbon_target && (surgery.surgery_flags & SURGERY_REQUIRE_LIMB)) //mob with no limb in surgery zone when we need a limb
continue
- if((surgery.surgery_flags & SURGERY_REQUIRE_RESTING) && target.body_position != LYING_DOWN)
+ if(IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
continue
if(!surgery.can_start(user, target))
continue
@@ -307,7 +307,7 @@
target.balloon_alert(user, "not the right type of limb!")
return
- if ((surgery.surgery_flags & SURGERY_REQUIRE_RESTING) && target.body_position != LYING_DOWN)
+ if (IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
target.balloon_alert(user, "patient is not lying down!")
return
diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm
index daa66ea9fb5b..bfdd2ce481fb 100644
--- a/code/datums/components/tackle.dm
+++ b/code/datums/components/tackle.dm
@@ -365,7 +365,7 @@
/datum/component/tackler/proc/splat(mob/living/carbon/user, atom/hit)
if(istype(hit, /obj/machinery/vending)) // before we do anything else-
var/obj/machinery/vending/darth_vendor = hit
- darth_vendor.tilt(user, TRUE)
+ darth_vendor.tilt(user, 100)
return
else if(istype(hit, /obj/structure/window))
var/obj/structure/window/W = hit
diff --git a/code/datums/components/torn_wall.dm b/code/datums/components/torn_wall.dm
new file mode 100644
index 000000000000..cadd7895349f
--- /dev/null
+++ b/code/datums/components/torn_wall.dm
@@ -0,0 +1,105 @@
+
+#define TORN_WALL_RUINED 2
+#define TORN_WALL_DAMAGED 1
+#define TORN_WALL_INITIAL 0
+
+/**
+ * Component applied to a wall to progressively destroy it.
+ * If component is applied to something which already has it, stage increases.
+ * Wall is destroyed on third application.
+ * Can be fixed using a welder
+ */
+/datum/component/torn_wall
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ var/current_stage = TORN_WALL_INITIAL
+
+/datum/component/torn_wall/Initialize()
+ . = ..()
+ if (!isclosedturf(parent) || isindestructiblewall(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/torn_wall/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_WELDER), PROC_REF(on_welded))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
+ RegisterSignal(parent, COMSIG_TURF_CHANGE, PROC_REF(on_turf_changed))
+ apply_visuals()
+
+/datum/component/torn_wall/UnregisterFromParent()
+ var/atom/atom_parent = parent
+ UnregisterSignal(parent, list(
+ COMSIG_PARENT_EXAMINE,
+ COMSIG_ATOM_TOOL_ACT(TOOL_WELDER),
+ COMSIG_ATOM_UPDATE_OVERLAYS,
+ COMSIG_TURF_CHANGE,
+ ))
+ atom_parent.update_appearance(UPDATE_ICON)
+
+/datum/component/torn_wall/InheritComponent(datum/component/C, i_am_original)
+ increase_stage()
+
+/// Play a fun animation and make our wall look damaged
+/datum/component/torn_wall/proc/apply_visuals()
+ var/atom/atom_parent = parent
+ playsound(atom_parent, 'sound/effects/bang.ogg', 50, vary = TRUE)
+ atom_parent.update_appearance(UPDATE_ICON)
+ atom_parent.Shake(shake_interval = 0.1 SECONDS, duration = 0.5 SECONDS)
+
+/// Make the effect more dramatic
+/datum/component/torn_wall/proc/increase_stage()
+ current_stage++
+ if (current_stage != TORN_WALL_RUINED)
+ apply_visuals()
+ return
+ var/turf/closed/wall/attached_wall = parent
+ playsound(attached_wall, 'sound/effects/meteorimpact.ogg', 100, vary = TRUE)
+
+ if(ismineralturf(attached_wall))
+ var/turf/closed/mineral/mineral_turf = attached_wall
+ mineral_turf.gets_drilled()
+ return
+
+ attached_wall.dismantle_wall(devastated = TRUE)
+
+/// Fix it up on weld
+/datum/component/torn_wall/proc/on_welded(atom/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+ INVOKE_ASYNC(src, PROC_REF(try_repair), source, user, tool)
+ return COMPONENT_BLOCK_TOOL_ATTACK
+
+/// Fix us up
+/datum/component/torn_wall/proc/try_repair(atom/source, mob/user, obj/item/tool)
+ source.balloon_alert(user, "repairing...")
+ if(!tool.use_tool(source, user, 5 SECONDS, amount = 2, volume = 50))
+ source.balloon_alert(user, "interrupted!")
+ return
+ current_stage--
+ if (current_stage < TORN_WALL_INITIAL)
+ qdel(src)
+ return
+ source.update_appearance(UPDATE_ICON)
+ source.tool_act(user, tool, TOOL_WELDER, is_right_clicking = FALSE) // Keep going
+
+/// Give them a hint
+/datum/component/torn_wall/proc/on_examined(atom/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ var/intensity = (current_stage == TORN_WALL_INITIAL) ? "slightly" : "badly"
+ examine_list += span_notice("It looks [intensity] damaged.")
+ examine_list += span_info("You may be able to repair it using a welding tool.")
+
+/// Show a little crack on here
+/datum/component/torn_wall/proc/on_update_overlays(turf/source, list/overlays)
+ SIGNAL_HANDLER
+ var/mutable_appearance/crack = mutable_appearance('icons/turf/overlays.dmi', "explodable", source.layer + 0.1)
+ if (current_stage == TORN_WALL_INITIAL)
+ crack.alpha *= 0.5
+ overlays += crack
+
+/// If the wall becomes any other turf, delete us. Transforming into a different works fine as a fix.
+/datum/component/torn_wall/proc/on_turf_changed()
+ SIGNAL_HANDLER
+ qdel(src)
+
+#undef TORN_WALL_RUINED
+#undef TORN_WALL_DAMAGED
+#undef TORN_WALL_INITIAL
diff --git a/code/datums/components/trader/trader.dm b/code/datums/components/trader/trader.dm
new file mode 100644
index 000000000000..21fd143528ab
--- /dev/null
+++ b/code/datums/components/trader/trader.dm
@@ -0,0 +1,457 @@
+#define TRADER_RADIAL_BUY "TRADER_RADIAL_BUY"
+#define TRADER_RADIAL_SELL "TRADER_RADIAL_SELL"
+#define TRADER_RADIAL_TALK "TRADER_RADIAL_TALK"
+#define TRADER_RADIAL_LORE "TRADER_RADIAL_LORE"
+#define TRADER_RADIAL_NO "TRADER_RADIAL_NO"
+#define TRADER_RADIAL_YES "TRADER_RADIAL_YES"
+#define TRADER_RADIAL_OUT_OF_STOCK "TRADER_RADIAL_OUT_OF_STOCK"
+#define TRADER_RADIAL_DISCUSS_BUY "TRADER_RADIAL_DISCUSS_BUY"
+#define TRADER_RADIAL_DISCUSS_SELL "TRADER_RADIAL_DISCUSS_SELL"
+
+#define TRADER_OPTION_BUY "Buy"
+#define TRADER_OPTION_SELL "Sell"
+#define TRADER_OPTION_TALK "Talk"
+#define TRADER_OPTION_LORE "Lore"
+#define TRADER_OPTION_NO "No"
+#define TRADER_OPTION_YES "Yes"
+#define TRADER_OPTION_BUYING "Buying?"
+#define TRADER_OPTION_SELLING "Selling?"
+
+//The defines below show the index the info is located in the product_info entry list
+
+#define TRADER_PRODUCT_INFO_PRICE 1
+#define TRADER_PRODUCT_INFO_QUANTITY 2
+//Only valid for wanted_items
+#define TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION 3
+
+/**
+ * # Trader NPC Component
+ * Manages the barks and the stocks of the traders
+ * Also manages the interactive radial menu
+ */
+/datum/component/trader
+
+ /**
+ * Format; list(TYPEPATH = list(PRICE, QUANTITY))
+ * Associated list of items the NPC sells with how much they cost and the quantity available before a restock
+ * This list is filled by Initialize(), if you want to change the starting products, modify initial_products()
+ * *
+ */
+ var/list/obj/item/products = list()
+ /**
+ * A list of wanted items that the trader would wish to buy, each typepath has a assigned value, quantity and additional flavor text
+ *
+ * CHILDREN OF TYPEPATHS INCLUDED IN WANTED_ITEMS WILL BE TREATED AS THE PARENT IF NO ENTRY EXISTS FOR THE CHILDREN
+ *
+ * As an additional note; if you include multiple children of a typepath; the typepath with the most children should be placed after all other typepaths
+ * Bad; list(/obj/item/milk = list(100, 1, ""), /obj/item/milk/small = list(50, 2, ""))
+ * Good; list(/obj/item/milk/small = list(50, 2, ""), /obj/item/milk = list(100, 1, ""))
+ * This is mainly because sell_item() uses a istype(item_being_sold, item_in_entry) to determine what parent should the child be automatically considered as
+ * If /obj/item/milk/small/spooky was being sold; /obj/item/milk/small would be the first to check against rather than /obj/item/milk
+ *
+ * Format; list(TYPEPATH = list(PRICE, QUANTITY, ADDITIONAL_DESCRIPTION))
+ * Associated list of items able to be sold to the NPC with the money given for them.
+ * The price given should be the "base" price; any price manipulation based on variables should be done with apply_sell_price_mods()
+ * ADDITIONAL_DESCRIPTION is any additional text added to explain how the variables of the item effect the price; if it's stack based, it's final price depends how much is in the stack
+ * EX; /obj/item/stack/sheet/mineral/diamond = list(500, INFINITY, ", per 100 cm3 sheet of diamond")
+ * This list is filled by Initialize(), if you want to change the starting wanted items, modify initial_wanteds()
+ */
+ var/list/wanted_items = list()
+
+ ///Contains images of all radial icons
+ var/static/list/radial_icons_cache = list()
+
+ ///Contains information of a specific trader
+ var/datum/trader_data/trader_data
+
+/*
+Can accept both a type path, and an instance of a datum. Type path has priority.
+*/
+/datum/component/trader/Initialize(trader_data_path = null, trader_data = null)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(ispath(trader_data_path, /datum/trader_data))
+ trader_data = new trader_data_path
+ if(isnull(trader_data))
+ CRASH("Initialised trader component with no trader data.")
+
+ src.trader_data = trader_data
+
+ radial_icons_cache = list(
+ TRADER_RADIAL_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buy"),
+ TRADER_RADIAL_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_sell"),
+ TRADER_RADIAL_TALK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_talk"),
+ TRADER_RADIAL_LORE = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_lore"),
+ TRADER_RADIAL_DISCUSS_BUY = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_buying"),
+ TRADER_RADIAL_DISCUSS_SELL = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_selling"),
+ TRADER_RADIAL_YES = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_yes"),
+ TRADER_RADIAL_NO = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_no"),
+ TRADER_RADIAL_OUT_OF_STOCK = image(icon = 'icons/hud/radial.dmi', icon_state = "radial_center"),
+ )
+
+ restock_products()
+ renew_item_demands()
+
+/datum/component/trader/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand))
+
+/datum/component/trader/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND)
+
+///If our trader is alive, and the customer left clicks them with an empty hand without combat mode
+/datum/component/trader/proc/on_attack_hand(atom/source, mob/living/carbon/customer)
+ SIGNAL_HANDLER
+ if(!can_trade(customer) || (customer.istate & ISTATE_HARM))
+ return
+ var/list/npc_options = list()
+ if(length(products))
+ npc_options[TRADER_OPTION_BUY] = radial_icons_cache[TRADER_RADIAL_BUY]
+ if(length(wanted_items))
+ npc_options[TRADER_OPTION_SELL] = radial_icons_cache[TRADER_RADIAL_SELL]
+ if(length(trader_data.say_phrases))
+ npc_options[TRADER_OPTION_TALK] = radial_icons_cache[TRADER_RADIAL_TALK]
+ if(!length(npc_options))
+ return
+
+ var/mob/living/trader = parent
+ trader.face_atom(customer)
+
+ INVOKE_ASYNC(src, PROC_REF(open_npc_options), customer, npc_options)
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/**
+ * Generates a radial of the initial radials of the NPC
+ * Called via asynch, due to the sleep caused by show_radial_menu
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to buy something
+ */
+/datum/component/trader/proc/open_npc_options(mob/living/carbon/customer, list/npc_options)
+ if(!can_trade(customer))
+ return
+ var/npc_result = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ switch(npc_result)
+ if(TRADER_OPTION_BUY)
+ buy_item(customer)
+ if(TRADER_OPTION_SELL)
+ try_sell(customer)
+ if(TRADER_OPTION_TALK)
+ discuss(customer)
+
+/**
+ * Checks if the customer is ok to use the radial
+ *
+ * Checks if the customer is not a mob or is incapacitated or not adjacent to the source of the radial, in those cases returns FALSE, otherwise returns TRUE
+ * Arguments:
+ * * customer - (Mob REF) The mob checking the menu
+ */
+/datum/component/trader/proc/check_menu(mob/customer)
+ if(!istype(customer))
+ return FALSE
+ if(IS_DEAD_OR_INCAP(customer) || !customer.Adjacent(parent))
+ return FALSE
+ return TRUE
+
+/**
+ * Generates a radial of the items the NPC sells and lets the user try to buy one
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to buy something
+ */
+/datum/component/trader/proc/buy_item(mob/customer)
+ if(!can_trade(customer))
+ return
+
+ if(!LAZYLEN(products))
+ return
+
+ var/list/display_names = list()
+ var/list/items = list()
+ var/list/product_info
+
+ for(var/obj/item/thing as anything in products)
+ display_names["[initial(thing.name)]"] = thing
+
+ if(!radial_icons_cache[thing])
+ radial_icons_cache[thing] = image(icon = initial(thing.icon), icon_state = initial(thing.icon_state_preview) ? initial(thing.icon_state_preview) : initial(thing.icon_state))
+
+ var/image/item_image = radial_icons_cache[thing]
+ product_info = products[thing]
+
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //out of stock
+ item_image.overlays += radial_icons_cache[TRADER_RADIAL_OUT_OF_STOCK]
+
+ items += list("[initial(thing.name)]" = item_image)
+
+ var/pick = show_radial_menu(customer, parent, items, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!pick || !can_trade(customer))
+ return
+
+ var/obj/item/item_to_buy = display_names[pick]
+ var/mob/living/trader = parent
+ trader.face_atom(customer)
+ product_info = products[item_to_buy]
+
+ if(!product_info[TRADER_PRODUCT_INFO_QUANTITY])
+ trader.say("[initial(item_to_buy.name)] appears to be out of stock.")
+ return
+
+ trader.say("It will cost you [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name] to buy \the [initial(item_to_buy.name)]. Are you sure you want to buy it?")
+ var/list/npc_options = list(
+ TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES],
+ TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO],
+ )
+
+ var/buyer_will_buy = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(buyer_will_buy != TRADER_OPTION_YES || !can_trade(customer))
+ return
+
+ trader.face_atom(customer)
+
+ if(!spend_buyer_offhand_money(customer, product_info[TRADER_PRODUCT_INFO_PRICE]))
+ trader.say(trader_data.return_trader_phrase(NO_CASH_PHRASE))
+ return
+
+ item_to_buy = new item_to_buy(get_turf(customer))
+ customer.put_in_hands(item_to_buy)
+ playsound(trader, trader_data.sell_sound, 50, TRUE)
+ log_econ("[item_to_buy] has been sold to [customer] (typepath used for product info; [item_to_buy.type]) by [trader] for [product_info[TRADER_PRODUCT_INFO_PRICE]] cash.")
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
+ trader.say(trader_data.return_trader_phrase(BUY_PHRASE))
+
+///Calculates the value of money in the hand of the buyer and spends it if it's sufficient
+/datum/component/trader/proc/spend_buyer_offhand_money(mob/customer, the_cost)
+ var/value = 0
+ var/obj/item/holochip/cash = customer.is_holding_item_of_type(/obj/item/holochip)
+ if(cash)
+ value += cash.credits
+ if((value >= the_cost) && cash)
+ return cash.spend(the_cost)
+ return FALSE //Purchase unsuccessful
+
+/**
+ * Tries to call sell_item on one of the customer's held items, if fail gives a chat message
+ *
+ * Gets both items in the customer's hands, and then tries to call sell_item on them, if both fail, he gives a chat message
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to sell something
+ */
+/datum/component/trader/proc/try_sell(mob/customer)
+ if(!can_trade(customer))
+ return
+ var/sold_item = FALSE
+ for(var/obj/item/an_item in customer.held_items)
+ if(sell_item(customer, an_item))
+ sold_item = TRUE
+ break
+ if(!sold_item && can_trade(customer)) //only talk if you are not dead or in combat
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(ITEM_REJECTED_PHRASE))
+
+
+/**
+ * Checks if an item is in the list of wanted items and if it is after a Yes/No radial returns generate_cash with the value of the item for the NPC
+ * Arguments:
+ * * customer - (Mob REF) The mob trying to sell something
+ * * selling - (Item REF) The item being sold
+ */
+/datum/component/trader/proc/sell_item(mob/customer, obj/item/selling)
+ if(isnull(selling))
+ return FALSE
+ var/list/product_info
+ //Keep track of the typepath; rather mundane but it's required for correctly modifying the wanted_items
+ //should a product be sellable because even if it doesn't have a entry because it's a child of a parent that is present on the list
+ var/typepath_for_product_info
+
+ if(selling.type in wanted_items)
+ product_info = wanted_items[selling.type]
+ typepath_for_product_info = selling.type
+ else //Assume wanted_items is setup in the correct way; read wanted_items documentation for more info
+ for(var/typepath in wanted_items)
+ if(!istype(selling, typepath))
+ continue
+
+ product_info = wanted_items[typepath]
+ typepath_for_product_info = typepath
+ break
+
+ if(!product_info) //Nothing interesting to sell
+ return FALSE
+
+ var/mob/living/trader = parent
+
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0)
+ trader.say(trader_data.return_trader_phrase(TRADER_HAS_ENOUGH_ITEM_PHRASE))
+ return FALSE
+
+ var/cost = apply_sell_price_mods(selling, product_info[TRADER_PRODUCT_INFO_PRICE])
+ if(cost <= 0)
+ trader.say(trader_data.return_trader_phrase(ITEM_IS_WORTHLESS_PHRASE))
+ return FALSE
+
+ trader.say(trader_data.return_trader_phrase(INTERESTED_PHRASE))
+ trader.say("You will receive [cost] [trader_data.currency_name] for the [selling].")
+ var/list/npc_options = list(
+ TRADER_OPTION_YES = radial_icons_cache[TRADER_RADIAL_YES],
+ TRADER_OPTION_NO = radial_icons_cache[TRADER_RADIAL_NO],
+ )
+
+ trader.face_atom(customer)
+
+ var/npc_result = show_radial_menu(customer, trader, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!can_trade(customer))
+ return
+ if(npc_result != TRADER_OPTION_YES)
+ trader.say(trader_data.return_trader_phrase(ITEM_SELLING_CANCELED_PHRASE))
+ return TRUE
+
+ trader.say(trader_data.return_trader_phrase(ITEM_SELLING_ACCEPTED_PHRASE))
+ playsound(trader, trader_data.sell_sound, 50, TRUE)
+ log_econ("[selling] has been sold to [trader] (typepath used for product info; [typepath_for_product_info]) by [customer] for [cost] cash.")
+ exchange_sold_items(selling, cost, typepath_for_product_info)
+ generate_cash(cost, customer)
+ return TRUE
+
+/**
+ * Modifies the 'base' price of a item based on certain variables
+ *
+ * Arguments:
+ * * Reference to the item; this is the item being sold
+ * * Original cost; the original cost of the item, to be manipulated depending on the variables of the item, one example is using item.amount if it's a stack
+ */
+/datum/component/trader/proc/apply_sell_price_mods(obj/item/selling, original_cost)
+ if(isstack(selling))
+ var/obj/item/stack/stackoverflow = selling
+ original_cost *= stackoverflow.amount
+ return original_cost
+
+/**
+ * Handles modifying/deleting the items to ensure that a proper amount is converted into cash; put into it's own proc to make the children of this not override a 30+ line sell_item()
+ *
+ * Arguments:
+ * * selling - (Item REF) this is the item being sold
+ * * value_exchanged_for - (Number) the "value", useful for a scenario where you want to remove enough items equal to the value
+ * * original_typepath - (Typepath) For scenarios where a children of a parent is being sold but we want to modify the parent's product information
+ */
+/datum/component/trader/proc/exchange_sold_items(obj/item/selling, value_exchanged_for, original_typepath)
+ var/list/product_info = wanted_items[original_typepath]
+ if(isstack(selling))
+ var/obj/item/stack/the_stack = selling
+ var/actually_sold = min(the_stack.amount, product_info[TRADER_PRODUCT_INFO_QUANTITY])
+ the_stack.use(actually_sold)
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= (actually_sold)
+ else
+ qdel(selling)
+ product_info[TRADER_PRODUCT_INFO_QUANTITY] -= 1
+
+/**
+ * Creates an item equal to the value set by the proc and puts it in the user's hands if possible
+ * Arguments:
+ * * value - A number; The amount of cash that will be on the holochip
+ * * customer - Reference to a mob; The mob we put the holochip in hands of
+ */
+/datum/component/trader/proc/generate_cash(value, mob/customer)
+ var/obj/item/holochip/chip = new /obj/item/holochip(get_turf(customer), value)
+ customer.put_in_hands(chip)
+
+///Talk about what items are being sold/wanted by the trader and in what quantity or lore
+/datum/component/trader/proc/discuss(mob/customer)
+ var/list/npc_options = list(
+ TRADER_OPTION_LORE = radial_icons_cache[TRADER_RADIAL_LORE],
+ TRADER_OPTION_SELLING = radial_icons_cache[TRADER_RADIAL_DISCUSS_SELL],
+ TRADER_OPTION_BUYING = radial_icons_cache[TRADER_RADIAL_DISCUSS_BUY],
+ )
+ var/pick = show_radial_menu(customer, parent, npc_options, custom_check = CALLBACK(src, PROC_REF(check_menu), customer), require_near = TRUE, tooltips = TRUE)
+ if(!can_trade(customer))
+ return
+ switch(pick)
+ if(TRADER_OPTION_LORE)
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(TRADER_LORE_PHRASE))
+ if(TRADER_OPTION_BUYING)
+ trader_buys_what(customer)
+ if(TRADER_OPTION_SELLING)
+ trader_sells_what(customer)
+
+///Displays to the customer what the trader is willing to buy and how much until a restock happens
+/datum/component/trader/proc/trader_buys_what(mob/customer)
+ if(!can_trade(customer))
+ return
+ if(!length(wanted_items))
+ var/mob/living/trader = parent
+ trader.say(trader_data.return_trader_phrase(TRADER_NOT_BUYING_ANYTHING))
+ return
+
+ var/list/buy_info = list(span_green("I'm willing to buy the following:"))
+
+ var/list/product_info
+ for(var/obj/item/thing as anything in wanted_items)
+ product_info = wanted_items[thing]
+ var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "as many as I can." : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Zero demand
+ buy_info += span_notice("• [span_red("(DOESN'T WANT MORE)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_red("[tern_op_result]")] more.")
+ else
+ buy_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name][product_info[TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION]]; willing to buy [span_green("[tern_op_result]")]")
+
+ to_chat(customer, examine_block(buy_info.Join("\n")))
+
+///Displays to the customer what the trader is selling and how much is in stock
+/datum/component/trader/proc/trader_sells_what(mob/customer)
+ if(!can_trade(customer))
+ return
+ var/mob/living/trader = parent
+ if(!length(products))
+ trader.say(trader_data.return_trader_phrase(TRADER_NOT_SELLING_ANYTHING))
+ return
+ var/list/sell_info = list(span_green("I'm currently selling the following:"))
+ var/list/product_info
+ for(var/obj/item/thing as anything in products)
+ product_info = products[thing]
+ var/tern_op_result = (product_info[TRADER_PRODUCT_INFO_QUANTITY] == INFINITY ? "an infinite amount" : "[product_info[TRADER_PRODUCT_INFO_QUANTITY]]") //Coder friendly string concat
+ if(product_info[TRADER_PRODUCT_INFO_QUANTITY] <= 0) //Out of stock
+ sell_info += span_notice("• [span_red("(OUT OF STOCK)")] [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_red("[tern_op_result]")] left in stock")
+ else
+ sell_info += span_notice("• [initial(thing.name)] for [product_info[TRADER_PRODUCT_INFO_PRICE]] [trader_data.currency_name]; [span_green("[tern_op_result]")] left in stock")
+ to_chat(customer, examine_block(sell_info.Join("\n")))
+
+///Sets quantity of all products to initial(quanity); this proc is currently called during initialize
+/datum/component/trader/proc/restock_products()
+ products = trader_data.initial_products.Copy()
+
+///Sets quantity of all wanted_items to initial(quanity); this proc is currently called during initialize
+/datum/component/trader/proc/renew_item_demands()
+ wanted_items = trader_data.initial_wanteds.Copy()
+
+///Returns if the trader is conscious and its combat mode is disabled.
+/datum/component/trader/proc/can_trade(mob/customer)
+ var/mob/living/trader = parent
+ if(trader.istate & ISTATE_HARM)
+ trader.balloon_alert(customer, "in combat!")
+ return FALSE
+ if(IS_DEAD_OR_INCAP(trader))
+ trader.balloon_alert(customer, "indisposed!")
+ return FALSE
+ return TRUE
+
+#undef TRADER_RADIAL_BUY
+#undef TRADER_RADIAL_SELL
+#undef TRADER_RADIAL_TALK
+#undef TRADER_RADIAL_LORE
+#undef TRADER_RADIAL_DISCUSS_BUY
+#undef TRADER_RADIAL_DISCUSS_SELL
+#undef TRADER_RADIAL_NO
+#undef TRADER_RADIAL_YES
+#undef TRADER_RADIAL_OUT_OF_STOCK
+#undef TRADER_PRODUCT_INFO_PRICE
+#undef TRADER_PRODUCT_INFO_QUANTITY
+#undef TRADER_PRODUCT_INFO_PRICE_MOD_DESCRIPTION
+
+#undef TRADER_OPTION_BUY
+#undef TRADER_OPTION_SELL
+#undef TRADER_OPTION_TALK
+#undef TRADER_OPTION_LORE
+#undef TRADER_OPTION_NO
+#undef TRADER_OPTION_YES
+#undef TRADER_OPTION_BUYING
+#undef TRADER_OPTION_SELLING
diff --git a/code/datums/components/transforming.dm b/code/datums/components/transforming.dm
index d704ca2098df..3c3fec196c23 100644
--- a/code/datums/components/transforming.dm
+++ b/code/datums/components/transforming.dm
@@ -89,6 +89,23 @@
RegisterSignal(parent, COMSIG_ITEM_SHARPEN_ACT, PROC_REF(on_sharpen))
RegisterSignal(parent, COMSIG_DETECTIVE_SCANNED, PROC_REF(on_scan))
+ RegisterSignal(parent, COMSIG_ITEM_APPLY_FANTASY_BONUSES, PROC_REF(apply_fantasy_bonuses))
+ RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses))
+
+/datum/component/transforming/proc/apply_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ active = FALSE
+ set_inactive(source)
+ force_on = source.modify_fantasy_variable("force_on", force_on, bonus)
+ throwforce_on = source.modify_fantasy_variable("throwforce_on", throwforce_on, bonus)
+
+/datum/component/transforming/proc/remove_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ active = FALSE
+ set_inactive(source)
+ force_on = source.reset_fantasy_variable("force_on", force_on)
+ throwforce_on = source.reset_fantasy_variable("throwforce_on", throwforce_on)
+
/datum/component/transforming/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_SHARPEN_ACT, COMSIG_DETECTIVE_SCANNED))
diff --git a/code/datums/components/tree_climber.dm b/code/datums/components/tree_climber.dm
index 2a0534a38b85..eaa2136687eb 100644
--- a/code/datums/components/tree_climber.dm
+++ b/code/datums/components/tree_climber.dm
@@ -12,6 +12,8 @@
return COMPONENT_INCOMPATIBLE
src.climbing_distance = climbing_distance
+ ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
/datum/component/tree_climber/RegisterWithParent()
RegisterSignals(parent, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_LIVING_CLIMB_TREE), PROC_REF(climb_tree))
RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
diff --git a/code/datums/components/twohanded.dm b/code/datums/components/twohanded.dm
index 756f30bd644d..8ac0f0896da6 100644
--- a/code/datums/components/twohanded.dm
+++ b/code/datums/components/twohanded.dm
@@ -92,6 +92,25 @@
RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_update_icon))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_ITEM_SHARPEN_ACT, PROC_REF(on_sharpen))
+ RegisterSignal(parent, COMSIG_ITEM_APPLY_FANTASY_BONUSES, PROC_REF(apply_fantasy_bonuses))
+ RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses))
+
+/datum/component/two_handed/proc/apply_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ force_wielded = source.modify_fantasy_variable("force_wielded", force_wielded, bonus)
+ force_unwielded = source.modify_fantasy_variable("force_unwielded", force_unwielded, bonus)
+ if(wielded && ismob(source.loc))
+ unwield(source.loc)
+ if(force_multiplier)
+ force_multiplier = source.modify_fantasy_variable("force_multiplier", force_multiplier, bonus/10, minimum = 1)
+
+/datum/component/two_handed/proc/remove_fantasy_bonuses(obj/item/source, bonus)
+ SIGNAL_HANDLER
+ force_wielded = source.reset_fantasy_variable("force_wielded", force_wielded)
+ force_unwielded = source.reset_fantasy_variable("force_unwielded", force_unwielded)
+ if(wielded && ismob(source.loc))
+ unwield(source.loc)
+ force_multiplier = source.reset_fantasy_variable("force_multiplier", force_multiplier)
// Remove all siginals registered to the parent item
/datum/component/two_handed/UnregisterFromParent()
diff --git a/code/datums/components/udder.dm b/code/datums/components/udder.dm
index 149cb6ee6f00..44367327d3fb 100644
--- a/code/datums/components/udder.dm
+++ b/code/datums/components/udder.dm
@@ -13,7 +13,8 @@
/datum/component/udder/Initialize(udder_type = /obj/item/udder, datum/callback/on_milk_callback, datum/callback/on_generate_callback, reagent_produced_typepath = /datum/reagent/consumable/milk)
if(!isliving(parent)) //technically is possible to drop this on carbons... but you wouldn't do that to me, would you?
return COMPONENT_INCOMPATIBLE
- udder = new udder_type(null, parent, on_generate_callback, reagent_produced_typepath)
+ udder = new udder_type(null)
+ udder.add_features(parent, on_generate_callback, reagent_produced_typepath)
src.on_milk_callback = on_milk_callback
/datum/component/udder/RegisterWithParent()
@@ -69,14 +70,68 @@
var/mob/living/udder_mob
///optional proc to callback to when the udder generates milk
var/datum/callback/on_generate_callback
-
-/obj/item/udder/Initialize(mapload, udder_mob, on_generate_callback, reagent_produced_typepath = /datum/reagent/consumable/milk)
- src.udder_mob = udder_mob
- src.on_generate_callback = on_generate_callback
+ ///do we require some food to generate milk?
+ var/require_consume_type
+ ///how long does each food consumption allow us to make milk
+ var/require_consume_timer = 2 MINUTES
+ ///hunger key we set to look for food
+ var/hunger_key = BB_CHECK_HUNGRY
+
+/obj/item/udder/proc/add_features(parent, callback, reagent = /datum/reagent/consumable/milk)
+ udder_mob = parent
+ on_generate_callback = callback
create_reagents(size, REAGENT_HOLDER_ALIVE)
- src.reagent_produced_typepath = reagent_produced_typepath
+ reagent_produced_typepath = reagent
initial_conditions()
+ if(isnull(require_consume_type))
+ return
+ RegisterSignal(udder_mob, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_mob_consume))
+ RegisterSignal(udder_mob, COMSIG_PARENT_ATTACKBY, PROC_REF(on_mob_feed))
+ udder_mob.ai_controller?.set_blackboard_key(BB_CHECK_HUNGRY, TRUE)
+
+/obj/item/udder/proc/on_mob_consume(datum/source, atom/feed)
+ SIGNAL_HANDLER
+
+ if(!istype(feed, require_consume_type))
+ return
+ INVOKE_ASYNC(src, PROC_REF(handle_consumption), feed)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/obj/item/udder/proc/on_mob_feed(datum/source, atom/used_item, mob/living/user)
+ SIGNAL_HANDLER
+
+ if(!istype(used_item, require_consume_type))
+ return
+ INVOKE_ASYNC(src, PROC_REF(handle_consumption), used_item, user)
+ return COMPONENT_NO_AFTERATTACK
+
+/obj/item/udder/proc/handle_consumption(atom/movable/food, mob/user)
+ if(locate(food.type) in src)
+ if(user)
+ user.balloon_alert(user, "already full!")
+ return
+ playsound(udder_mob.loc,'sound/items/eatfood.ogg', 50, TRUE)
+ udder_mob.visible_message(span_notice("[udder_mob] gobbles up [food]!"), span_notice("You gobble up [food]!"))
+ var/atom/movable/final_food = food
+ if(isstack(food)) //if stack, only consume 1
+ var/obj/item/stack/food_stack = food
+ final_food = food_stack.split_stack(udder_mob, 1)
+ final_food.forceMove(src)
+
+/obj/item/udder/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ if(!istype(arrived, require_consume_type))
+ return ..()
+
+ udder_mob.ai_controller?.set_blackboard_key(hunger_key, FALSE)
+ QDEL_IN(arrived, require_consume_timer)
+ return ..()
+
+/obj/item/udder/Exited(atom/movable/gone, direction)
. = ..()
+ if(!istype(gone, require_consume_type))
+ return
+ udder_mob.ai_controller?.set_blackboard_key(hunger_key, TRUE)
+
/obj/item/udder/Destroy()
. = ..()
@@ -99,10 +154,13 @@
* Proc called every 2 seconds from SSMobs to add whatever reagent the udder is generating.
*/
/obj/item/udder/proc/generate()
- if(prob(5))
- reagents.add_reagent(reagent_produced_typepath, rand(5, 10))
- if(on_generate_callback)
- on_generate_callback.Invoke(reagents.total_volume, reagents.maximum_volume)
+ if(!isnull(require_consume_type) && !(locate(require_consume_type) in src))
+ return
+ if(prob(95))
+ return
+ reagents.add_reagent(reagent_produced_typepath, rand(5, 10), added_purity = 1)
+ if(on_generate_callback)
+ on_generate_callback.Invoke(reagents.total_volume, reagents.maximum_volume)
/**
* Proc called from attacking the component parent with the correct item, moves reagents into the glass basically.
@@ -123,48 +181,20 @@
/**
* # gutlunch udder subtype
- *
- * Used by gutlunches, and generates healing reagents instead of milk on eating gibs instead of a process. Starts empty!
- * Female gutlunches (ahem, guthens if you will) make babies when their udder is full under processing, instead of milk generation
*/
+
/obj/item/udder/gutlunch
name = "nutrient sac"
-
-/obj/item/udder/gutlunch/initial_conditions()
- if(!udder_mob)
- return
- if(udder_mob.gender == FEMALE)
- START_PROCESSING(SSobj, src)
- RegisterSignal(udder_mob, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(on_mob_attacking))
-
-/obj/item/udder/gutlunch/process(seconds_per_tick)
- var/mob/living/simple_animal/hostile/asteroid/gutlunch/gutlunch = udder_mob
- if(reagents.total_volume != reagents.maximum_volume)
- return
- if(gutlunch.make_babies())
- reagents.clear_reagents()
- //usually this would be a callback but this is a specifically gutlunch feature so fuck it, gutlunch specific proccall
- gutlunch.regenerate_icons(reagents.total_volume, reagents.maximum_volume)
-
-/**
- * signal called on parent attacking an atom
-*/
-/obj/item/udder/gutlunch/proc/on_mob_attacking(mob/living/simple_animal/hostile/gutlunch, atom/target)
- SIGNAL_HANDLER
-
- if(is_type_in_typecache(target, gutlunch.wanted_objects)) //we eats
- generate()
- gutlunch.visible_message(span_notice("[udder_mob] slurps up [target]."))
- qdel(target)
- return COMPONENT_HOSTILE_NO_ATTACK //there is no longer a target to attack
+ require_consume_type = /obj/item/stack/ore
+ reagent_produced_typepath = /datum/reagent/medicine/mine_salve
/obj/item/udder/gutlunch/generate()
- var/made_something = FALSE
- if(prob(60))
- reagents.add_reagent(/datum/reagent/consumable/cream, rand(2, 5))
- made_something = TRUE
- if(prob(45))
+ . = ..()
+ if(!.)
+ return
+ if(locate(/obj/item/stack/ore/gold) in src)
+ reagents.add_reagent(/datum/reagent/consumable/cream, rand(2, 5), added_purity = 1)
+ if(locate(/obj/item/stack/ore/bluespace_crystal) in src)
reagents.add_reagent(/datum/reagent/medicine/salglu_solution, rand(2,5))
- made_something = TRUE
- if(made_something && on_generate_callback)
+ if(on_generate_callback)
on_generate_callback.Invoke(reagents.total_volume, reagents.maximum_volume)
diff --git a/code/datums/components/unusual_effect.dm b/code/datums/components/unusual_effect.dm
new file mode 100644
index 000000000000..eb67b9537997
--- /dev/null
+++ b/code/datums/components/unusual_effect.dm
@@ -0,0 +1,60 @@
+
+/particles/unusual_effect
+ icon = 'icons/effects/particles/pollen.dmi'
+ icon_state = "pollen"
+ width = 100
+ height = 100
+ count = 1000
+ spawning = 4
+ lifespan = 0.7 SECONDS
+ fade = 1 SECONDS
+ grow = -0.01
+ velocity = list(0, 0)
+ position = generator(GEN_CIRCLE, 0, 16, NORMAL_RAND)
+ drift = generator(GEN_VECTOR, list(0, -0.2), list(0, 0.2))
+ gravity = list(0, 0.95)
+ scale = generator(GEN_VECTOR, list(0.3, 0.3), list(1,1), NORMAL_RAND)
+ rotation = 30
+ spin = generator(GEN_NUM, -20, 20)
+
+/// Creates a cool looking effect on the movable.
+/// In the future, this could be expanded to have more interesting particles and effects.
+/datum/component/unusual_effect
+ dupe_mode = COMPONENT_DUPE_HIGHLANDER
+
+ var/obj/effect/abstract/particle_holder/special_effects
+
+ var/color
+
+ COOLDOWN_DECLARE(glow_cooldown)
+
+/datum/component/unusual_effect/Initialize(color, include_particles = FALSE)
+ var/atom/movable/parent_movable = parent
+ if (!istype(parent_movable))
+ return COMPONENT_INCOMPATIBLE
+
+ src.color = color
+ parent_movable.add_filter("unusual_effect", 2, list("type" = "outline", "color" = color, "size" = 2))
+ if(include_particles)
+ special_effects = new(parent_movable, /particles/unusual_effect)
+ START_PROCESSING(SSobj, src)
+
+/datum/component/unusual_effect/Destroy(force, silent)
+ var/atom/movable/parent_movable = parent
+ if (istype(parent_movable))
+ parent_movable.remove_filter("unusual_effect")
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/datum/component/unusual_effect/process(seconds_per_tick)
+ var/atom/movable/parent_movable = parent
+ var/filter = parent_movable.get_filter("unusual_effect")
+ if (!filter)
+ parent_movable.add_filter("unusual_effect", 2, list("type" = "outline", "color" = color, "size" = 2))
+ return
+ if(!COOLDOWN_FINISHED(src, glow_cooldown))
+ return
+
+ animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1)
+ animate(alpha = 40, time = 2.5 SECONDS)
+ COOLDOWN_START(src, glow_cooldown, 4 SECONDS)
diff --git a/code/datums/diseases/parrotpossession.dm b/code/datums/diseases/parrotpossession.dm
index 23f68e1a42ff..442a046643b6 100644
--- a/code/datums/diseases/parrotpossession.dm
+++ b/code/datums/diseases/parrotpossession.dm
@@ -13,24 +13,41 @@
severity = DISEASE_SEVERITY_MEDIUM
infectable_biotypes = MOB_ORGANIC|MOB_UNDEAD|MOB_ROBOTIC|MOB_MINERAL
bypasses_immunity = TRUE //2spook
- var/mob/living/simple_animal/parrot/poly/ghost/parrot
+ ///chance we speak
+ var/speak_chance = 5
+ ///controller we speak from
+ var/datum/ai_controller/basic_controller/parrot_controller
/datum/disease/parrot_possession/stage_act(seconds_per_tick, times_fired)
. = ..()
- if(!.)
+
+ if(!. || isnull(parrot_controller))
return
- if(QDELETED(parrot) || parrot.loc != affected_mob)
- cure()
- return FALSE
+ var/potential_phrase = parrot_controller.blackboard[BB_PARROT_REPEAT_STRING]
- if(length(parrot.speech_buffer) && SPT_PROB(parrot.speak_chance, seconds_per_tick)) // I'm not going to dive into polycode trying to adjust that probability. Enjoy doubled ghost parrot speach
- affected_mob.say(pick(parrot.speech_buffer), forced = "parrot possession")
+ if(SPT_PROB(speak_chance, seconds_per_tick) && !isnull(potential_phrase))
+ affected_mob.say(potential_phrase, forced = "parrot possession")
/datum/disease/parrot_possession/cure()
- if(parrot && parrot.loc == affected_mob)
- parrot.forceMove(affected_mob.drop_location())
- affected_mob.visible_message(span_danger("[parrot] is violently driven out of [affected_mob]!"), span_userdanger("[parrot] bursts out of your chest!"))
- ..()
+ var/atom/movable/inside_parrot = locate(/mob/living/basic/parrot/poly/ghost) in affected_mob
+ if(inside_parrot)
+ UnregisterSignal(inside_parrot, list(COMSIG_PARENT_PREQDELETED, COMSIG_MOVABLE_MOVED))
+ inside_parrot.forceMove(affected_mob.drop_location())
+ affected_mob.visible_message(
+ span_danger("[inside_parrot] is violently driven out of [affected_mob]!"),
+ span_userdanger("[inside_parrot] bursts out of your chest!"),
+ )
+ parrot_controller = null
+ return ..()
+
+/datum/disease/parrot_possession/proc/set_parrot(mob/living/parrot)
+ parrot_controller = parrot.ai_controller
+ RegisterSignals(parrot, list(COMSIG_PARENT_PREQDELETED, COMSIG_MOVABLE_MOVED), PROC_REF(on_parrot_exit))
+
+/datum/disease/parrot_possession/proc/on_parrot_exit(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(source, list(COMSIG_PARENT_PREQDELETED, COMSIG_MOVABLE_MOVED))
+ cure()
diff --git a/code/datums/diseases/transformation.dm b/code/datums/diseases/transformation.dm
index 0405b72e854e..43269cb2ee73 100644
--- a/code/datums/diseases/transformation.dm
+++ b/code/datums/diseases/transformation.dm
@@ -58,10 +58,10 @@
to_chat(affected_mob, pick(stage5))
if(QDELETED(affected_mob))
return
- if(affected_mob.notransform)
+ if(HAS_TRAIT_FROM(affected_mob, TRAIT_NO_TRANSFORM, REF(src)))
return
- affected_mob.notransform = 1
- for(var/obj/item/W in affected_mob.get_equipped_items(TRUE))
+ ADD_TRAIT(affected_mob, TRAIT_NO_TRANSFORM, REF(src))
+ for(var/obj/item/W in affected_mob.get_equipped_items(include_pockets = TRUE))
affected_mob.dropItemToGround(W)
for(var/obj/item/I in affected_mob.held_items)
affected_mob.dropItemToGround(I)
diff --git a/code/datums/elements/ai_flee_while_injured.dm b/code/datums/elements/ai_flee_while_injured.dm
index fc1a2e332816..eca709dbee5d 100644
--- a/code/datums/elements/ai_flee_while_injured.dm
+++ b/code/datums/elements/ai_flee_while_injured.dm
@@ -19,6 +19,7 @@
src.stop_fleeing_at = stop_fleeing_at
src.start_fleeing_below = start_fleeing_below
RegisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_changed))
+ on_health_changed(target)
/datum/element/ai_flee_while_injured/Detach(datum/source)
. = ..()
@@ -28,18 +29,18 @@
/datum/element/ai_flee_while_injured/proc/on_health_changed(mob/living/source)
SIGNAL_HANDLER
- if (!source.ai_controller)
+ if (isnull(source.ai_controller))
return
var/current_health_percentage = source.health / source.maxHealth
- if (source.ai_controller.blackboard[BB_BASIC_MOB_FLEEING])
- if (current_health_percentage < stop_fleeing_at)
+ if (source.ai_controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ if (current_health_percentage > start_fleeing_below)
return
- source.ai_controller.CancelActions() // Stop fleeing go back to whatever you were doing
- source.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, FALSE)
+ source.ai_controller.CancelActions()
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE)
return
- if (current_health_percentage > start_fleeing_below)
+ if (current_health_percentage < stop_fleeing_at)
return
- source.ai_controller.CancelActions()
- source.ai_controller.set_blackboard_key(BB_BASIC_MOB_FLEEING, TRUE)
+ source.ai_controller.CancelActions() // Stop fleeing go back to whatever you were doing
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
diff --git a/code/datums/elements/ai_swap_combat_mode.dm b/code/datums/elements/ai_swap_combat_mode.dm
new file mode 100644
index 000000000000..cbd271f314b8
--- /dev/null
+++ b/code/datums/elements/ai_swap_combat_mode.dm
@@ -0,0 +1,64 @@
+/**
+ * Attached to a mob with an AI controller, updates combat mode when the affected mob acquires or loses targets
+ */
+/datum/element/ai_swap_combat_mode
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// The message we yell when we enter combat mode
+ var/list/battle_start_barks
+ /// A one liner said when we exit combat mode
+ var/list/battle_end_barks
+ /// The chance to yell the above lines
+ var/speech_chance
+ /// Target key
+ var/target_key
+
+/datum/element/ai_swap_combat_mode/Attach(datum/target, target_key, list/battle_start_barks = null, list/battle_end_barks = null)
+ . = ..()
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ var/mob/living/living_target = target
+ if(!living_target.ai_controller)
+ return ELEMENT_INCOMPATIBLE
+
+ if(isnull(battle_start_barks))
+ battle_start_barks = list("En Garde!",)
+
+ if(isnull(battle_end_barks))
+ battle_end_barks = list("Never should have come here",)
+
+ src.battle_start_barks = battle_start_barks
+ src.battle_end_barks = battle_end_barks
+ src.target_key = target_key
+ RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_target_gained))
+ RegisterSignal(target, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(on_target_cleared))
+
+/datum/element/ai_swap_combat_mode/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, list(
+ COMSIG_AI_BLACKBOARD_KEY_SET(target_key),
+ COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key),
+ ))
+
+/// When the mob gains a target, and it was not already in combat mode, enter it
+/datum/element/ai_swap_combat_mode/proc/on_target_gained(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(swap_mode(source, TRUE))
+ INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_start_barks)
+
+/// When the mob loses its target, and it was not already out of combat mode, exit it
+/datum/element/ai_swap_combat_mode/proc/on_target_cleared(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(swap_mode(source, FALSE))
+ INVOKE_ASYNC(src, PROC_REF(speak_bark), source, battle_end_barks)
+
+///Says a quip, if the RNG allows it
+/datum/element/ai_swap_combat_mode/proc/speak_bark(mob/living/source, line)
+ source.say(pick(line))
+
+///If the combat mode would be changed into a different state, updates it and returns TRUE, otherwise returns FALSE
+/datum/element/ai_swap_combat_mode/proc/swap_mode(mob/living/source, new_mode)
+ source.set_combat_mode(new_mode)
+ return TRUE
diff --git a/code/datums/elements/amputating_limbs.dm b/code/datums/elements/amputating_limbs.dm
index c2fe7c454a96..16a99e96f6c1 100644
--- a/code/datums/elements/amputating_limbs.dm
+++ b/code/datums/elements/amputating_limbs.dm
@@ -1,4 +1,4 @@
-/// This component will intercept bare-handed attacks by the owner on critically injured carbons and amputate random limbs instead
+/// This component will intercept bare-handed attacks by the owner on sufficiently injured carbons and amputate random limbs instead
/datum/element/amputating_limbs
element_flags = ELEMENT_BESPOKE
argument_hash_start_idx = 2
@@ -6,6 +6,10 @@
var/surgery_time
/// What is the means by which we describe the act of amputation?
var/surgery_verb
+ /// How awake must our target be?
+ var/minimum_stat
+ /// How likely are we to perform this action?
+ var/snip_chance
/// The types of limb we can remove
var/list/target_zones
@@ -13,6 +17,8 @@
datum/target,
surgery_time = 5 SECONDS,
surgery_verb = "prying",
+ minimum_stat = SOFT_CRIT,
+ snip_chance = 100,
list/target_zones = list(BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_ARM, BODY_ZONE_R_LEG),
)
. = ..()
@@ -23,6 +29,8 @@
src.surgery_time = surgery_time
src.surgery_verb = surgery_verb
+ src.minimum_stat = minimum_stat
+ src.snip_chance = snip_chance
src.target_zones = target_zones
RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(try_amputate))
@@ -33,11 +41,11 @@
/// Called when you click on literally anything with your hands, see if it is an injured carbon and then try to cut it up
/datum/element/amputating_limbs/proc/try_amputate(mob/living/surgeon, atom/victim)
SIGNAL_HANDLER
- if (!iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER))
+ if (!iscarbon(victim) || HAS_TRAIT(victim, TRAIT_NODISMEMBER) || !prob(snip_chance))
return
var/mob/living/carbon/limbed_victim = victim
- if (limbed_victim.stat == CONSCIOUS)
+ if (limbed_victim.stat < minimum_stat)
return
if (DOING_INTERACTION_WITH_TARGET(surgeon, victim))
@@ -60,7 +68,7 @@
/// Chop one off
/datum/element/amputating_limbs/proc/amputate(mob/living/surgeon, mob/living/carbon/victim, obj/item/bodypart/to_remove)
- surgeon.visible_message(span_warning("[surgeon] begins [surgery_verb] [to_remove] off of [victim]!"))
- if (!do_after(surgeon, delay = surgery_time, target = victim))
+ surgeon.visible_message(span_warning("[surgeon] [surgery_verb] [to_remove] off of [victim]!"))
+ if (surgery_time > 0 && !do_after(surgeon, delay = surgery_time, target = victim))
return
to_remove.dismember()
diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm
index 86f4be63cac3..3201eeff846f 100644
--- a/code/datums/elements/basic_eating.dm
+++ b/code/datums/elements/basic_eating.dm
@@ -12,10 +12,12 @@
var/damage_amount
/// Type of hurt to apply
var/damage_type
+ /// Whether to flavor it as drinking rather than eating.
+ var/drinking
/// Types the animal can eat.
var/list/food_types
-/datum/element/basic_eating/Attach(datum/target, heal_amt = 0, damage_amount = 0, damage_type = null, food_types = list())
+/datum/element/basic_eating/Attach(datum/target, heal_amt = 0, damage_amount = 0, damage_type = null, drinking = FALSE, food_types = list())
. = ..()
if(!isliving(target))
@@ -24,6 +26,7 @@
src.heal_amt = heal_amt
src.damage_amount = damage_amount
src.damage_type = damage_type
+ src.drinking = drinking
src.food_types = food_types
//this lets players eat
@@ -45,8 +48,12 @@
/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target)
if(!is_type_in_list(target, food_types))
- return
- var/eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp")
+ return FALSE
+ var/eat_verb
+ if(drinking)
+ eat_verb = pick("slurp","sip","guzzle","drink","quaff","suck")
+ else
+ eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp")
if (heal_amt > 0)
var/healed = heal_amt && eater.health < eater.maxHealth
@@ -66,5 +73,8 @@
finish_eating(eater, target)
/datum/element/basic_eating/proc/finish_eating(mob/living/eater, atom/target)
- playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE)
+ if(drinking)
+ playsound(eater.loc,'sound/items/drink.ogg', rand(10,50), TRUE)
+ else
+ playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE)
qdel(target)
diff --git a/code/datums/elements/climbable.dm b/code/datums/elements/climbable.dm
index b7e93c61e246..76062b051159 100644
--- a/code/datums/elements/climbable.dm
+++ b/code/datums/elements/climbable.dm
@@ -131,15 +131,13 @@
///Handles climbing onto the atom when you click-drag
/datum/element/climbable/proc/mousedrop_receive(atom/climbed_thing, atom/movable/dropped_atom, mob/user, params)
SIGNAL_HANDLER
- if(user == dropped_atom && isliving(dropped_atom))
- var/mob/living/living_target = dropped_atom
- if(isanimal(living_target))
- var/mob/living/simple_animal/animal = dropped_atom
- if (!animal.dextrous)
- return
- if(living_target.mobility_flags & MOBILITY_MOVE)
- INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)
- return
+ if(user != dropped_atom || !isliving(dropped_atom))
+ return
+ if(!HAS_TRAIT(dropped_atom, TRAIT_FENCE_CLIMBER) && !HAS_TRAIT(dropped_atom, TRAIT_CAN_HOLD_ITEMS)) // If you can hold items you can probably climb a fence
+ return
+ var/mob/living/living_target = dropped_atom
+ if(living_target.mobility_flags & MOBILITY_MOVE)
+ INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)
///Tries to climb onto the target if the forced movement of the mob allows it
/datum/element/climbable/proc/try_speedrun(datum/source, mob/bumpee)
diff --git a/code/datums/elements/content_barfer.dm b/code/datums/elements/content_barfer.dm
index 782574a22217..2959ff9d58e7 100644
--- a/code/datums/elements/content_barfer.dm
+++ b/code/datums/elements/content_barfer.dm
@@ -4,18 +4,17 @@
* Used for morphs and bileworms!
*/
/datum/element/content_barfer
- argument_hash_start_idx = 2
-/datum/element/content_barfer/Attach(datum/target, tally_string)
+/datum/element/content_barfer/Attach(datum/target)
. = ..()
if(!isliving(target))
return ELEMENT_INCOMPATIBLE
- RegisterSignals(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED), PROC_REF(barf_contents))
+ RegisterSignals(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED, COMSIG_MOB_CHANGED_TYPE), PROC_REF(barf_contents))
/datum/element/content_barfer/Detach(datum/target)
- UnregisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED))
+ UnregisterSignal(target, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_ON_WABBAJACKED, COMSIG_MOB_CHANGED_TYPE))
return ..()
/datum/element/content_barfer/proc/barf_contents(mob/living/target)
diff --git a/code/datums/elements/death_drops.dm b/code/datums/elements/death_drops.dm
index c02ba37cad72..070ed8063d25 100644
--- a/code/datums/elements/death_drops.dm
+++ b/code/datums/elements/death_drops.dm
@@ -16,7 +16,7 @@
if(!loot)
stack_trace("[type] added to [target] with NO LOOT.")
src.loot = loot
- RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death), override = TRUE)
/datum/element/death_drops/Detach(datum/target)
. = ..()
diff --git a/code/datums/elements/dextrous.dm b/code/datums/elements/dextrous.dm
new file mode 100644
index 000000000000..ef4fa290eb2b
--- /dev/null
+++ b/code/datums/elements/dextrous.dm
@@ -0,0 +1,73 @@
+/**
+ * Sets up the attachee to have hands and manages things like dropping items on death and displaying them on examine
+ * Actual hand performance is managed by code on /living/ and not encapsulated here, we just enable it
+ */
+/datum/element/dextrous
+
+/datum/element/dextrous/Attach(datum/target, hands_count = 2, hud_type = /datum/hud/dextrous)
+ . = ..()
+ if (!isliving(target) || iscarbon(target))
+ return ELEMENT_INCOMPATIBLE // Incompatible with the carbon typepath because that already has its own hand handling and doesn't need hand holding
+
+ var/mob/living/mob_parent = target
+ set_available_hands(mob_parent, hands_count)
+ mob_parent.set_hud_used(new hud_type(target))
+ mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
+ ADD_TRAIT(target, TRAIT_CAN_HOLD_ITEMS, REF(src))
+ RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_hand_clicked))
+ RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+
+/datum/element/dextrous/Detach(datum/source)
+ . = ..()
+ var/mob/living/mob_parent = source
+ set_available_hands(mob_parent, initial(mob_parent.default_num_hands))
+ var/initial_hud = initial(mob_parent.hud_type)
+ mob_parent.set_hud_used(new initial_hud(source))
+ mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
+ REMOVE_TRAIT(source, TRAIT_CAN_HOLD_ITEMS, REF(src))
+ UnregisterSignal(source, list(
+ COMSIG_PARENT_EXAMINE,
+ COMSIG_LIVING_DEATH,
+ COMSIG_LIVING_UNARMED_ATTACK,
+ ))
+
+/// Set up how many hands we should have
+/datum/element/dextrous/proc/set_available_hands(mob/living/hand_owner, hands_count)
+ hand_owner.drop_all_held_items()
+ var/held_items = list()
+ for (var/i in 1 to hands_count)
+ held_items += null
+ hand_owner.held_items = held_items
+ hand_owner.set_num_hands(hands_count)
+ hand_owner.set_usable_hands(hands_count)
+
+/// Drop our shit when we die
+/datum/element/dextrous/proc/on_death(mob/living/died, gibbed)
+ SIGNAL_HANDLER
+ died.drop_all_held_items()
+
+/// Try picking up items
+/datum/element/dextrous/proc/on_hand_clicked(mob/living/hand_haver, atom/target, proximity, modifiers)
+ SIGNAL_HANDLER
+ if (!proximity && target.loc != hand_haver)
+ var/obj/item/obj_item = target
+ if (istype(obj_item) && !obj_item.atom_storage && !(obj_item.item_flags & IN_STORAGE))
+ return NONE
+ if (!isitem(target) && (hand_haver.istate & ISTATE_HARM))
+ return NONE
+ if (hand_haver.istate & ISTATE_SECONDARY)
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand_secondary), hand_haver, modifiers)
+ else
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand), hand_haver, modifiers)
+ INVOKE_ASYNC(hand_haver, TYPE_PROC_REF(/mob, update_held_items))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Tell people what we are holding
+/datum/element/dextrous/proc/on_examined(mob/living/examined, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ for(var/obj/item/held_item in examined.held_items)
+ if(held_item.item_flags & (ABSTRACT|EXAMINE_SKIP|HAND_ITEM))
+ continue
+ examine_list += span_info("[examined.p_They()] [examined.p_have()] [held_item.get_examine_string(user)] in [examined.p_their()] \
+ [examined.get_held_index_name(examined.get_held_index_of_item(held_item))].")
diff --git a/code/datums/elements/door_pryer.dm b/code/datums/elements/door_pryer.dm
new file mode 100644
index 000000000000..cb30a48399c0
--- /dev/null
+++ b/code/datums/elements/door_pryer.dm
@@ -0,0 +1,70 @@
+/**
+ * Attached to a basic mob.
+ * Causes attacks on doors to attempt to open them.
+ */
+/datum/element/door_pryer
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// Time it takes to open a door with force
+ var/pry_time
+ /// Interaction key for if we force a door open
+ var/interaction_key
+
+/datum/element/door_pryer/Attach(datum/target, pry_time = 10 SECONDS, interaction_key = null)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ src.pry_time = pry_time
+ src.interaction_key = interaction_key
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK), PROC_REF(on_attack))
+
+/datum/element/door_pryer/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK))
+
+/// If we're targeting an airlock, open it
+/datum/element/door_pryer/proc/on_attack(mob/living/basic/attacker, atom/target, proximity_flag)
+ SIGNAL_HANDLER
+ if(!istype(target, /obj/machinery/door/airlock))
+ return
+ var/obj/machinery/door/airlock/airlock_target = target
+ if (!airlock_target.density)
+ return // It's already open numbnuts
+
+ if(DOING_INTERACTION_WITH_TARGET(attacker, target) || (!isnull(interaction_key) && DOING_INTERACTION(attacker, interaction_key)))
+ attacker.balloon_alert(attacker, "busy!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ if (airlock_target.locked || airlock_target.welded || airlock_target.seal)
+ if (!(attacker.istate & ISTATE_HARM))
+ airlock_target.balloon_alert(attacker, "it's sealed!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ return // Attack the door
+
+ INVOKE_ASYNC(src, PROC_REF(open_door), attacker, airlock_target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Try opening the door, and if we can't then try forcing it
+/datum/element/door_pryer/proc/open_door(mob/living/basic/attacker, obj/machinery/door/airlock/airlock_target)
+ if (!airlock_target.hasPower())
+ attacker.visible_message(span_warning("[attacker] forces the [airlock_target] to open."))
+ airlock_target.open(FORCING_DOOR_CHECKS)
+ return
+
+ if (airlock_target.allowed(attacker))
+ airlock_target.open(DEFAULT_DOOR_CHECKS)
+ return
+
+ attacker.visible_message(\
+ message = span_warning("[attacker] starts forcing the [airlock_target] open!"),
+ blind_message = span_hear("You hear a metal screeching sound."),
+ )
+ playsound(airlock_target, 'sound/machines/airlock_alien_prying.ogg', 100, TRUE)
+ airlock_target.balloon_alert(attacker, "prying...")
+ if(!do_after(attacker, pry_time, airlock_target))
+ airlock_target.balloon_alert(attacker, "interrupted!")
+ return
+ if(airlock_target.locked)
+ return
+ attacker.visible_message(span_warning("[attacker] forces the [airlock_target] to open."))
+ airlock_target.open(BYPASS_DOOR_CHECKS)
diff --git a/code/datums/elements/food/venue_price.dm b/code/datums/elements/food/venue_price.dm
index 1fcf9de040dc..be846d7c004d 100644
--- a/code/datums/elements/food/venue_price.dm
+++ b/code/datums/elements/food/venue_price.dm
@@ -21,19 +21,19 @@
UnregisterSignal(target, COMSIG_ITEM_SOLD_TO_CUSTOMER)
UnregisterSignal(target, COMSIG_REAGENT_SOLD_TO_CUSTOMER)
-/datum/element/venue_price/proc/item_sold(obj/item/thing_sold, mob/living/simple_animal/robot_customer/sold_to)
+/datum/element/venue_price/proc/item_sold(obj/item/thing_sold, mob/living/basic/robot_customer/sold_to)
SIGNAL_HANDLER
produce_cash(sold_to, thing_sold)
return TRANSACTION_SUCCESS
-/datum/element/venue_price/proc/reagent_sold(datum/reagent/reagent_sold, mob/living/simple_animal/robot_customer/sold_to, obj/item/container)
+/datum/element/venue_price/proc/reagent_sold(datum/reagent/reagent_sold, mob/living/basic/robot_customer/sold_to, obj/item/container)
SIGNAL_HANDLER
produce_cash(sold_to, container)
return TRANSACTION_SUCCESS
-/datum/element/venue_price/proc/produce_cash(mob/living/simple_animal/robot_customer/sold_to, obj/item/container)
+/datum/element/venue_price/proc/produce_cash(mob/living/basic/robot_customer/sold_to, obj/item/container)
new /obj/item/holochip(get_turf(container), venue_price)
playsound(container, 'sound/effects/cashregister.ogg', 60, TRUE)
diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm
index 86cd5c27ce6b..f3e5176d0972 100644
--- a/code/datums/elements/footstep.dm
+++ b/code/datums/elements/footstep.dm
@@ -40,6 +40,8 @@
footstep_sounds = GLOB.heavyfootstep
if(FOOTSTEP_MOB_SHOE)
footstep_sounds = GLOB.footstep
+ if(FOOTSTEP_MOB_RUST)
+ footstep_sounds = 'sound/effects/footstep/rustystep1.ogg'
if(FOOTSTEP_MOB_SLIME)
footstep_sounds = 'sound/effects/footstep/slime1.ogg'
if(FOOTSTEP_OBJ_MACHINE)
@@ -89,37 +91,32 @@
if(steps != 0 && !source.has_gravity()) // don't need to step as often when you hop around
return
- return turf
+
+ . = list(FOOTSTEP_MOB_SHOE = turf.footstep, FOOTSTEP_MOB_BAREFOOT = turf.barefootstep, FOOTSTEP_MOB_HEAVY = turf.heavyfootstep, FOOTSTEP_MOB_CLAW = turf.clawfootstep, STEP_SOUND_PRIORITY = STEP_SOUND_NO_PRIORITY)
+ SEND_SIGNAL(turf, COMSIG_TURF_PREPARE_STEP_SOUND, .)
+ return .
/datum/element/footstep/proc/play_simplestep(mob/living/source, atom/oldloc, direction, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
-
+ var/turf/source_loc = get_turf(source)
if (forced || SHOULD_DISABLE_FOOTSTEPS(source))
return
- var/turf/open/source_loc = prepare_step(source)
- if(!source_loc)
+ var/list/prepared_steps = prepare_step(source)
+ if(!prepared_steps)
return
+
if(isfile(footstep_sounds) || istext(footstep_sounds))
playsound(source_loc, footstep_sounds, volume, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
return
- var/turf_footstep
- switch(footstep_type)
- if(FOOTSTEP_MOB_CLAW)
- turf_footstep = source_loc.clawfootstep
- if(FOOTSTEP_MOB_BAREFOOT)
- turf_footstep = source_loc.barefootstep
- if(FOOTSTEP_MOB_HEAVY)
- turf_footstep = source_loc.heavyfootstep
- if(FOOTSTEP_MOB_SHOE)
- turf_footstep = source_loc.footstep
+
+ var/turf_footstep = prepared_steps[footstep_type]
if(!turf_footstep)
return
playsound(source_loc, pick(footstep_sounds[turf_footstep][1]), footstep_sounds[turf_footstep][2] * volume, TRUE, footstep_sounds[turf_footstep][3] + e_range, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
-
if (forced || SHOULD_DISABLE_FOOTSTEPS(source) || !momentum_change)
return
@@ -130,8 +127,8 @@
volume_multiplier = 0.6
range_adjustment = -2
- var/turf/open/source_loc = prepare_step(source)
- if(!source_loc)
+ var/list/prepared_steps = prepare_step(source)
+ if(!prepared_steps)
return
//cache for sanic speed (lists are references anyways)
@@ -142,20 +139,22 @@
if ((source.wear_suit?.body_parts_covered | source.w_uniform?.body_parts_covered | source.shoes?.body_parts_covered) & FEET)
// we are wearing shoes
- heard_clients = playsound(source_loc, pick(footstep_sounds[source_loc.footstep][1]),
- footstep_sounds[source_loc.footstep][2] * volume * volume_multiplier,
+ var/shoestep_type = prepared_steps[FOOTSTEP_MOB_SHOE]
+ heard_clients = playsound(source.loc, pick(footstep_sounds[shoestep_type][1]),
+ footstep_sounds[shoestep_type][2] * volume * volume_multiplier,
TRUE,
- footstep_sounds[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
+ footstep_sounds[shoestep_type][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
else
+ var/barefoot_type = prepared_steps[FOOTSTEP_MOB_BAREFOOT]
if(source.dna.species.special_step_sounds)
- heard_clients = playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary)
+ heard_clients = playsound(source.loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary)
else
var/static/list/bare_footstep_sounds = GLOB.barefootstep
- heard_clients = playsound(source_loc, pick(bare_footstep_sounds[source_loc.barefootstep][1]),
- bare_footstep_sounds[source_loc.barefootstep][2] * volume * volume_multiplier,
+ heard_clients = playsound(source.loc, pick(bare_footstep_sounds[barefoot_type][1]),
+ bare_footstep_sounds[barefoot_type][2] * volume * volume_multiplier,
TRUE,
- bare_footstep_sounds[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
+ bare_footstep_sounds[barefoot_type][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary, mixer_channel = CHANNEL_SOUND_FOOTSTEPS)
if(heard_clients)
play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE, override_list = heard_clients)
diff --git a/code/datums/elements/footstep_override.dm b/code/datums/elements/footstep_override.dm
new file mode 100644
index 000000000000..4e0c346c5be2
--- /dev/null
+++ b/code/datums/elements/footstep_override.dm
@@ -0,0 +1,80 @@
+///When attached, the footstep sound played by the footstep element will be replaced by this one's
+/datum/element/footstep_override
+ element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH_ON_HOST_DESTROY
+ argument_hash_start_idx = 2
+ ///The sound played for movables with claw step sound type.
+ var/clawfootstep
+ ///The sound played for movables with barefoot step sound type.
+ var/barefootstep
+ ///The sound played for movables with heavy step sound type.
+ var/heavyfootstep
+ ///The sound played for movables with shoed step sound type.
+ var/footstep
+ ///The priority this element has in relation to other elements of the same type attached to other movables on the same turf.
+ var/priority
+ /**
+ * A list of turfs occupied by the movables this element is attached to.
+ * Needed so it stops listening the turf's signals ONLY when it has no movable with the element.
+ */
+ var/list/occupied_turfs = list()
+
+/datum/element/footstep_override/Attach(atom/movable/target, clawfootstep = FOOTSTEP_HARD_CLAW, barefootstep = FOOTSTEP_HARD_BAREFOOT, heavyfootstep = FOOTSTEP_GENERIC_HEAVY, footstep = FOOTSTEP_FLOOR, priority = STEP_SOUND_NO_PRIORITY)
+ . = ..()
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.clawfootstep = clawfootstep
+ src.barefootstep = barefootstep
+ src.heavyfootstep = heavyfootstep
+ src.footstep = footstep
+ src.priority = priority
+
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ if(isturf(target.loc))
+ occupy_turf(target, target.loc)
+
+/datum/element/footstep_override/Detach(atom/movable/source)
+ if(isturf(source.loc))
+ vacate_turf(source, source.loc)
+ return ..()
+
+/datum/element/footstep_override/proc/on_moved(atom/movable/source, atom/oldloc)
+ SIGNAL_HANDLER
+ if(isturf(oldloc))
+ vacate_turf(source, oldloc)
+ if(isturf(source.loc))
+ occupy_turf(source, source.loc)
+
+/**
+ * Adds the movable to the list of movables with the element occupying the turf.
+ * If the turf was not on the list of occupied turfs before, a signal will be registered
+ * to it.
+ */
+/datum/element/footstep_override/proc/occupy_turf(atom/movable/movable, turf/location)
+ if(occupied_turfs[location])
+ occupied_turfs[location] |= movable
+ return
+ occupied_turfs[location] = list(movable)
+ RegisterSignal(location, COMSIG_TURF_PREPARE_STEP_SOUND, PROC_REF(prepare_steps))
+
+/**
+ * Removes the movable from the list of movables with the element occupying the turf.
+ * If the turf is no longer occupied, it'll be removed from the list, and the signal
+ * unregistered from it
+ */
+/datum/element/footstep_override/proc/vacate_turf(atom/movable/movable, turf/location)
+ LAZYREMOVE(occupied_turfs[location], movable)
+ if(!occupied_turfs[location])
+ occupied_turfs -= location
+ UnregisterSignal(location, COMSIG_TURF_PREPARE_STEP_SOUND)
+
+///Changes the sound types to be played if the element priority is higher than the one in the steps list.
+/datum/element/footstep_override/proc/prepare_steps(turf/source, list/steps)
+ SIGNAL_HANDLER
+ if(steps[STEP_SOUND_PRIORITY] > priority)
+ return
+ steps[FOOTSTEP_MOB_SHOE] = footstep
+ steps[FOOTSTEP_MOB_BAREFOOT] = barefootstep
+ steps[FOOTSTEP_MOB_HEAVY] = heavyfootstep
+ steps[FOOTSTEP_MOB_CLAW] = clawfootstep
+ steps[STEP_SOUND_PRIORITY] = priority
diff --git a/code/datums/elements/kneecapping.dm b/code/datums/elements/kneecapping.dm
index f0a1cd096812..f6ff92e6e0d5 100644
--- a/code/datums/elements/kneecapping.dm
+++ b/code/datums/elements/kneecapping.dm
@@ -7,7 +7,7 @@
* Kneecapping attacks have a wounding bonus between severe and critical+10 wound thresholds. Without some serious wound protecting
* armour this all but guarantees a wound of some sort. The attack is directed specifically at a limb and the limb takes the damage.
*
- * Requires the attacker to be aiming for either leg zone, which will be targetted specifically. They will than have a 3-second long
+ * Requires the attacker to be aiming for either leg zone, which will be targeted specifically. They will than have a 3-second long
* do_after before executing the attack.
*
* Kneecapping requires the target to either be on the floor, immobilised or buckled to something. And also to have an appropriate leg.
@@ -80,9 +80,11 @@
if(do_after(attacker, 3 SECONDS, target, interaction_key = weapon))
attacker.visible_message(span_warning("[attacker] swings [attacker.p_their()] [weapon] at [target]'s kneecaps!"), span_danger("You swing \the [weapon] at [target]'s kneecaps!"))
- var/datum/wound/blunt/severe/severe_wound_type = /datum/wound/blunt/severe
- var/datum/wound/blunt/critical/critical_wound_type = /datum/wound/blunt/critical
- leg.receive_damage(brute = weapon.force, wound_bonus = rand(initial(severe_wound_type.threshold_minimum), initial(critical_wound_type.threshold_minimum) + 10))
+
+ var/min_wound = leg.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_SEVERE, return_value_if_no_wound = 30, wound_source = weapon)
+ var/max_wound = leg.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_CRITICAL, return_value_if_no_wound = 50, wound_source = weapon)
+
+ leg.receive_damage(brute = weapon.force, wound_bonus = rand(min_wound, max_wound + 10))
target.emote("scream")
log_combat(attacker, target, "broke the kneecaps of", weapon)
target.update_damage_overlays()
diff --git a/code/datums/elements/mob_grabber.dm b/code/datums/elements/mob_grabber.dm
new file mode 100644
index 000000000000..a85c5dc48b25
--- /dev/null
+++ b/code/datums/elements/mob_grabber.dm
@@ -0,0 +1,30 @@
+/// Grab onto mobs we attack
+/datum/element/mob_grabber
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// What state must the mob be in to be grabbed?
+ var/minimum_stat
+ /// If someone else is already grabbing this, will we take it?
+ var/steal_from_others
+
+/datum/element/mob_grabber/Attach(datum/target, minimum_stat = SOFT_CRIT, steal_from_others = TRUE)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ src.minimum_stat = minimum_stat
+ src.steal_from_others = steal_from_others
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(grab_mob))
+
+/datum/element/mob_grabber/Detach(datum/source)
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ . = ..()
+
+/// Try and grab something we attacked
+/datum/element/mob_grabber/proc/grab_mob(mob/living/source, mob/living/target)
+ SIGNAL_HANDLER
+ if (!isliving(target) || !source.Adjacent(target) || target.stat < minimum_stat)
+ return
+ var/atom/currently_pulled = target.pulledby
+ if (!isnull(currently_pulled) && (!steal_from_others || currently_pulled == source))
+ return
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/mob/living, grabbedby), source)
diff --git a/code/datums/elements/ore_collecting.dm b/code/datums/elements/ore_collecting.dm
new file mode 100644
index 000000000000..30c9ecc7598d
--- /dev/null
+++ b/code/datums/elements/ore_collecting.dm
@@ -0,0 +1,26 @@
+/*
+ * A component to allow us to collect ore
+ */
+/datum/element/ore_collecting
+
+
+/datum/element/ore_collecting/Attach(datum/target)
+ . = ..()
+
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(collect_ore))
+
+/datum/element/ore_collecting/Detach(datum/target)
+ . = ..()
+ UnregisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
+
+/datum/element/ore_collecting/proc/collect_ore(mob/living/source, atom/target)
+ SIGNAL_HANDLER
+
+ if(!istype(target, /obj/item/stack/ore))
+ return
+
+ var/atom/movable/movable_target = target
+ movable_target.forceMove(source)
+ return COMPONENT_HOSTILE_NO_ATTACK
diff --git a/code/datums/elements/pet_bonus.dm b/code/datums/elements/pet_bonus.dm
index a3302645ee50..7c69927691bc 100644
--- a/code/datums/elements/pet_bonus.dm
+++ b/code/datums/elements/pet_bonus.dm
@@ -33,6 +33,7 @@
return
new /obj/effect/temp_visual/heart(pet.loc)
+ SEND_SIGNAL(pet, COMSIG_ANIMAL_PET, petter, modifiers)
if(emote_message && prob(33))
pet.manual_emote(emote_message)
petter.add_mood_event("petting_bonus", moodlet, pet)
diff --git a/code/datums/elements/relay_attackers.dm b/code/datums/elements/relay_attackers.dm
index f94fba531eed..cdc86e6058cb 100644
--- a/code/datums/elements/relay_attackers.dm
+++ b/code/datums/elements/relay_attackers.dm
@@ -15,6 +15,7 @@
RegisterSignal(target, COMSIG_ATOM_PREHITBY, PROC_REF(on_hitby))
RegisterSignal(target, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk))
RegisterSignal(target, COMSIG_ATOM_ATTACK_MECH, PROC_REF(on_attack_mech))
+ ADD_TRAIT(target, TRAIT_RELAYING_ATTACKER, REF(src))
/datum/element/relay_attackers/Detach(datum/source, ...)
. = ..()
@@ -30,22 +31,29 @@
COMSIG_ATOM_HULK_ATTACK,
COMSIG_ATOM_ATTACK_MECH,
))
+ REMOVE_TRAIT(source, TRAIT_RELAYING_ATTACKER, REF(src))
/datum/element/relay_attackers/proc/after_attackby(atom/target, obj/item/weapon, mob/attacker)
SIGNAL_HANDLER
if(weapon.force)
- relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : NONE)
+ relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
/datum/element/relay_attackers/proc/on_attack_generic(atom/target, mob/living/attacker, list/modifiers)
SIGNAL_HANDLER
- var/shoving = (attacker.istate & ISTATE_SECONDARY) ? ATTACKER_SHOVING : NONE
- if((attacker.istate & ISTATE_HARM) || (attacker.istate & ISTATE_SECONDARY))
- relay_attacker(target, attacker, shoving)
+ // Check for a shove.
+ if((attacker.istate & ISTATE_SECONDARY))
+ relay_attacker(target, attacker, ATTACKER_SHOVING)
+ return
+
+ // Else check for combat mode.
+ if((attacker.istate & ISTATE_HARM))
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ return
/datum/element/relay_attackers/proc/on_attack_npc(atom/target, mob/living/attacker)
SIGNAL_HANDLER
if(attacker.melee_damage_upper > 0)
- relay_attacker(target, attacker)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
/// Even if another component blocked this hit, someone still shot at us
/datum/element/relay_attackers/proc/on_bullet_act(atom/target, list/bullet_args, obj/projectile/hit_projectile)
@@ -54,7 +62,7 @@
return
if(!ismob(hit_projectile.firer))
return
- relay_attacker(target, hit_projectile.firer, hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : NONE)
+ relay_attacker(target, hit_projectile.firer, hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
/// Even if another component blocked this hit, someone still threw something
/datum/element/relay_attackers/proc/on_hitby(atom/target, atom/movable/hit_atom, datum/thrownthing/throwingdatum)
@@ -67,15 +75,15 @@
var/mob/thrown_by = hit_item.thrownby?.resolve()
if(!ismob(thrown_by))
return
- relay_attacker(target, thrown_by, hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : NONE)
+ relay_attacker(target, thrown_by, hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
/datum/element/relay_attackers/proc/on_attack_hulk(atom/target, mob/attacker)
SIGNAL_HANDLER
- relay_attacker(target, attacker)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
/datum/element/relay_attackers/proc/on_attack_mech(atom/target, obj/vehicle/sealed/mecha/mecha_attacker, mob/living/pilot)
SIGNAL_HANDLER
- relay_attacker(target, mecha_attacker)
+ relay_attacker(target, mecha_attacker, ATTACKER_DAMAGING_ATTACK)
/// Send out a signal identifying whoever just attacked us (usually a mob but sometimes a mech or turret)
/datum/element/relay_attackers/proc/relay_attacker(atom/victim, atom/attacker, attack_flags)
diff --git a/code/datums/elements/structure_repair.dm b/code/datums/elements/structure_repair.dm
new file mode 100644
index 000000000000..d3b26eed815b
--- /dev/null
+++ b/code/datums/elements/structure_repair.dm
@@ -0,0 +1,45 @@
+/// Intercepts attacks from mobs with this component to instead repair specified structures.
+/datum/element/structure_repair
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// How much to heal structures by
+ var/heal_amount
+ /// Typecache of types of structures to repair
+ var/list/structure_types_typecache
+
+/datum/element/structure_repair/Attach(
+ datum/target,
+ heal_amount = 5,
+ structure_types_typecache = typecacheof(list(/obj/structure)),
+)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.heal_amount = heal_amount
+ src.structure_types_typecache = structure_types_typecache
+ RegisterSignals(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET), PROC_REF(try_repair))
+
+/datum/element/structure_repair/Detach(datum/source)
+ UnregisterSignal(source, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
+ return ..()
+
+/// If the target is of a valid type, interrupt the attack chain to repair it instead
+/datum/element/structure_repair/proc/try_repair(mob/living/fixer, atom/target)
+ SIGNAL_HANDLER
+
+ if (!is_type_in_typecache(target, structure_types_typecache))
+ return
+
+ if (target.get_integrity() >= target.max_integrity)
+ target.balloon_alert(fixer, "not damaged!")
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ target.repair_damage(heal_amount)
+ fixer.Beam(target, icon_state = "sendbeam", time = 0.4 SECONDS)
+ fixer.visible_message(
+ span_danger("[fixer] repairs [target]."),
+ span_danger("You repair [target], leaving it at [round(target.get_integrity() * 100 / target.max_integrity)]% stability."),
+ )
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/datums/elements/waddling.dm b/code/datums/elements/waddling.dm
index eff201455d85..825b0c4e4cb8 100644
--- a/code/datums/elements/waddling.dm
+++ b/code/datums/elements/waddling.dm
@@ -4,27 +4,23 @@
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
- if(isliving(target))
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(LivingWaddle))
- else
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Waddle))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Waddle))
/datum/element/waddling/Detach(datum/source)
. = ..()
UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
-
-/datum/element/waddling/proc/LivingWaddle(mob/living/target)
+/datum/element/waddling/proc/Waddle(atom/movable/moved, atom/oldloc, direction, forced)
SIGNAL_HANDLER
-
- if(target.incapacitated() || target.body_position == LYING_DOWN)
+ if(forced || CHECK_MOVE_LOOP_FLAGS(moved, MOVEMENT_LOOP_OUTSIDE_CONTROL))
return
- Waddle(target)
-
-
-/datum/element/waddling/proc/Waddle(atom/movable/target)
- SIGNAL_HANDLER
+ if(isliving(moved))
+ var/mob/living/living_moved = moved
+ if (living_moved.incapacitated() || living_moved.body_position == LYING_DOWN)
+ return
+ waddling_animation(moved)
+/datum/element/waddling/proc/waddling_animation(atom/movable/target)
animate(target, pixel_z = 4, time = 0)
var/prev_trans = matrix(target.transform)
animate(pixel_z = 0, transform = turn(target.transform, pick(-12, 0, 12)), time=2)
diff --git a/code/datums/elements/wall_tearer.dm b/code/datums/elements/wall_tearer.dm
new file mode 100644
index 000000000000..19e84dd2720b
--- /dev/null
+++ b/code/datums/elements/wall_tearer.dm
@@ -0,0 +1,81 @@
+/// Returned if we can rip up this target
+#define WALL_TEAR_ALLOWED TRUE
+/// Returned if we can't rip up this target
+#define WALL_TEAR_INVALID FALSE
+/// Returned if we can't rip up the target but still don't want to attack it
+#define WALL_TEAR_FAIL_CANCEL_CHAIN -1
+
+/**
+ * Allows attached mobs to destroy walls over time, a little less unreasonable than the instant wall deletion of wall_smasher
+ */
+/datum/element/wall_tearer
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// Whether we can break reinforced walls
+ var/allow_reinforced
+ /// How long it takes for us to destroy a wall completely (its a 3 step process so this will be divided by three)
+ var/tear_time
+ /// How much longer it takes to break reinforced walls
+ var/reinforced_multiplier
+ /// What interaction key do we use for our interaction
+ var/do_after_key
+
+/datum/element/wall_tearer/Attach(datum/target, allow_reinforced = TRUE, tear_time = 4 SECONDS, reinforced_multiplier = 3, do_after_key = null)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ src.allow_reinforced = allow_reinforced
+ src.tear_time = tear_time
+ src.reinforced_multiplier = reinforced_multiplier
+ src.do_after_key = do_after_key
+ RegisterSignals(target, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_LIVING_UNARMED_ATTACK), PROC_REF(on_attacked_wall))
+
+/datum/element/wall_tearer/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, list(COMSIG_HOSTILE_PRE_ATTACKINGTARGET, COMSIG_LIVING_UNARMED_ATTACK))
+
+/// Try to tear up a wall
+/datum/element/wall_tearer/proc/on_attacked_wall(mob/living/tearer, atom/target, proximity_flag)
+ SIGNAL_HANDLER
+ if (DOING_INTERACTION_WITH_TARGET(tearer, target) || (!isnull(do_after_key) && DOING_INTERACTION(tearer, do_after_key)))
+ tearer.balloon_alert(tearer, "busy!")
+ return COMPONENT_HOSTILE_NO_ATTACK
+ var/is_valid = validate_target(target, tearer)
+ if (is_valid != WALL_TEAR_ALLOWED)
+ return is_valid == WALL_TEAR_FAIL_CANCEL_CHAIN ? COMPONENT_HOSTILE_NO_ATTACK : NONE
+ INVOKE_ASYNC(src, PROC_REF(rip_and_tear), tearer, target)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/datum/element/wall_tearer/proc/rip_and_tear(mob/living/tearer, atom/target)
+ // We need to do this three times to actually destroy it
+ var/rip_time = (istype(target, /turf/closed/wall/r_wall) ? tear_time * reinforced_multiplier : tear_time) / 3
+ if (rip_time > 0)
+ tearer.visible_message(span_warning("[tearer] begins tearing through [target]!"))
+ playsound(tearer, 'sound/machines/airlock_alien_prying.ogg', vol = 100, vary = TRUE)
+ target.balloon_alert(tearer, "tearing...")
+ if (!do_after(tearer, delay = rip_time, target = target, interaction_key = do_after_key))
+ tearer.balloon_alert(tearer, "interrupted!")
+ return
+ // Might have been replaced, removed, or reinforced during our do_after
+ var/is_valid = validate_target(target, tearer)
+ if (is_valid != WALL_TEAR_ALLOWED)
+ return
+ target.AddComponent(/datum/component/torn_wall)
+ is_valid = validate_target(target, tearer) // And now we might have just destroyed it
+ if (is_valid == WALL_TEAR_ALLOWED)
+ tearer.UnarmedAttack(target, proximity_flag = TRUE)
+
+/// Check if the target atom is a wall we can actually rip up
+/datum/element/wall_tearer/proc/validate_target(atom/target, mob/living/tearer)
+ if (!isclosedturf(target) || isindestructiblewall(target))
+ return WALL_TEAR_INVALID
+
+ var/reinforced = istype(target, /turf/closed/wall/r_wall)
+ if (!allow_reinforced && reinforced)
+ target.balloon_alert(tearer, "it's too strong!")
+ return WALL_TEAR_FAIL_CANCEL_CHAIN
+ return WALL_TEAR_ALLOWED
+
+#undef WALL_TEAR_ALLOWED
+#undef WALL_TEAR_INVALID
+#undef WALL_TEAR_FAIL_CANCEL_CHAIN
diff --git a/code/datums/elements/wall_walker.dm b/code/datums/elements/wall_walker.dm
new file mode 100644
index 000000000000..92ac3318c128
--- /dev/null
+++ b/code/datums/elements/wall_walker.dm
@@ -0,0 +1,49 @@
+/// This element will allow the mob it's attached to to pass through a specified type of wall, and drag anything through it.
+/datum/element/wall_walker
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// What kind of walls can we pass through?
+ var/wall_type
+
+/datum/element/wall_walker/Attach(
+ datum/target,
+ wall_type = /turf/closed/wall,
+)
+ . = ..()
+ if (!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+
+ src.wall_type = wall_type
+ RegisterSignal(target, COMSIG_LIVING_WALL_BUMP, PROC_REF(try_pass_wall))
+ RegisterSignal(target, COMSIG_LIVING_WALL_EXITED, PROC_REF(exit_wall))
+
+/datum/element/wall_walker/Detach(datum/source)
+ UnregisterSignal(source, list(COMSIG_LIVING_WALL_BUMP, COMSIG_LIVING_WALL_EXITED))
+ return ..()
+
+/// If the wall is of the proper type, pass into it and keep hold on whatever you're pulling
+/datum/element/wall_walker/proc/try_pass_wall(mob/living/passing_mob, turf/closed/bumped_wall)
+ if(!istype(bumped_wall, wall_type))
+ return
+
+ var/atom/movable/stored_pulling = passing_mob.pulling
+ if(stored_pulling) //force whatever you're pulling to come with you
+ stored_pulling.setDir(get_dir(stored_pulling.loc, passing_mob.loc))
+ stored_pulling.forceMove(passing_mob.loc)
+ passing_mob.forceMove(bumped_wall)
+
+ if(stored_pulling) //don't drop them because we went into a wall
+ passing_mob.start_pulling(stored_pulling, supress_message = TRUE)
+
+/// If the wall is of the proper type, pull whatever you're pulling into it
+/datum/element/wall_walker/proc/exit_wall(mob/living/passing_mob, turf/closed/exited_wall)
+ if(!istype(exited_wall, wall_type))
+ return
+
+ var/atom/movable/stored_pulling = passing_mob.pulling
+ if(isnull(stored_pulling))
+ return
+
+ stored_pulling.setDir(get_dir(stored_pulling.loc, passing_mob.loc))
+ stored_pulling.forceMove(exited_wall)
+ passing_mob.start_pulling(stored_pulling, supress_message = TRUE)
diff --git a/code/datums/elements/wheel.dm b/code/datums/elements/wheel.dm
new file mode 100644
index 000000000000..2bb8977ca5ca
--- /dev/null
+++ b/code/datums/elements/wheel.dm
@@ -0,0 +1,28 @@
+/// Element which spins you as you move
+/datum/element/wheel
+
+/datum/element/wheel/Attach(datum/target)
+ . = ..()
+ if(!ismovable(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+
+/datum/element/wheel/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
+
+/datum/element/wheel/proc/on_moved(atom/movable/moved, atom/oldloc, direction, forced)
+ SIGNAL_HANDLER
+ if(forced || CHECK_MOVE_LOOP_FLAGS(moved, MOVEMENT_LOOP_OUTSIDE_CONTROL))
+ return
+ if(isliving(moved))
+ var/mob/living/living_moved = moved
+ if (living_moved.incapacitated() || living_moved.body_position == LYING_DOWN)
+ return
+ var/rotation_degree = (360 / 3)
+ if(direction & SOUTHWEST)
+ rotation_degree *= -1
+
+ var/matrix/to_turn = matrix(moved.transform)
+ to_turn = turn(moved.transform, rotation_degree)
+ animate(moved, transform = to_turn, time = 0.1 SECONDS, flags = ANIMATION_PARALLEL)
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
new file mode 100644
index 000000000000..f8a0f4a062c6
--- /dev/null
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
@@ -0,0 +1,37 @@
+//
+// CARP
+//
+
+/datum/greyscale_config/carp
+ name = "Space Carp"
+ icon_file = 'icons/mob/simple/carp.dmi'
+ json_config = 'code/datums/greyscale/json_configs/carp.json'
+
+/datum/greyscale_config/carp_magic
+ name = "Magicarp"
+ icon_file = 'icons/mob/simple/carp.dmi'
+ json_config = 'code/datums/greyscale/json_configs/carp_magic.json'
+
+/datum/greyscale_config/carp_mega
+ name = "Megacarp"
+ icon_file = 'icons/mob/simple/broadMobs.dmi'
+ json_config = 'code/datums/greyscale/json_configs/carp_mega.json'
+
+/datum/greyscale_config/carp/disk_mouth
+ name = "Space Carp, Disk in Mouth"
+ json_config = 'code/datums/greyscale/json_configs/carp_disk_mouth.json'
+
+//
+// MISC
+//
+
+/datum/greyscale_config/garden_gnome
+ name = "Garden Gnome"
+ icon_file = 'icons/mob/simple/garden_gnome.dmi'
+ json_config = 'code/datums/greyscale/json_configs/garden_gnome.json'
+
+/datum/greyscale_config/gutlunch
+ name = "Gutlunch"
+ icon_file = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ json_config = 'code/datums/greyscale/json_configs/gutlunch.json'
+
diff --git a/code/datums/greyscale/json_configs/gutlunch.json b/code/datums/greyscale/json_configs/gutlunch.json
new file mode 100644
index 000000000000..0b9303381634
--- /dev/null
+++ b/code/datums/greyscale/json_configs/gutlunch.json
@@ -0,0 +1,15 @@
+{
+ "gutlunch": [
+ {
+ "type": "icon_state",
+ "icon_state": "gutlunch",
+ "blend_mode": "overlay",
+ "color_ids": [ 1 ]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "gutlunch_face",
+ "blend_mode": "overlay"
+ }
+ ]
+}
diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm
index 505ac87766a3..e407198faf3f 100644
--- a/code/datums/mapgen/CaveGenerator.dm
+++ b/code/datums/mapgen/CaveGenerator.dm
@@ -55,7 +55,7 @@
/mob/living/basic/mining/basilisk = 4,
/mob/living/basic/mining/goldgrub = 1,
/mob/living/basic/mining/goliath/ancient = 5,
- /mob/living/simple_animal/hostile/asteroid/hivelord = 3,
+ /mob/living/basic/mining/hivelord = 3,
)
mob_spawn_list = expand_weights(weighted_mob_spawn_list)
mob_spawn_no_mega_list = expand_weights(weighted_mob_spawn_list - SPAWN_MEGAFAUNA)
diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
index 3f6f5fd60acc..a6638919c81b 100644
--- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm
+++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
@@ -4,11 +4,11 @@
weighted_mob_spawn_list = list(
- /mob/living/basic/mining/lobstrosity = 15,
/mob/living/basic/mining/goldgrub = 10,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 50,
+ /mob/living/basic/mining/legion/snow = 50,
+ /mob/living/basic/mining/lobstrosity = 15,
+ /mob/living/basic/mining/wolf = 50,
/mob/living/simple_animal/hostile/asteroid/polarbear = 30,
- /mob/living/simple_animal/hostile/asteroid/wolf = 50,
/obj/structure/spawner/ice_moon = 3,
/obj/structure/spawner/ice_moon/polarbear = 3,
)
@@ -42,12 +42,12 @@
weighted_closed_turf_types = list(/turf/closed/mineral/random/snow/underground = 1)
weighted_mob_spawn_list = list(
SPAWN_MEGAFAUNA = 1,
+ /mob/living/basic/mining/ice_demon = 100,
/mob/living/basic/mining/ice_whelp = 60,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow = 100,
- /mob/living/simple_animal/hostile/asteroid/ice_demon = 100,
+ /mob/living/basic/mining/legion/snow = 100,
/obj/structure/spawner/ice_moon/demonic_portal = 6,
- /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6,
/obj/structure/spawner/ice_moon/demonic_portal/ice_whelp = 6,
+ /obj/structure/spawner/ice_moon/demonic_portal/snowlegion = 6,
)
weighted_megafauna_spawn_list = list(/mob/living/simple_animal/hostile/megafauna/colossus = 1)
weighted_flora_spawn_list = list(/obj/structure/flora/rock/icy/style_random = 6, /obj/structure/flora/rock/pile/icy/style_random = 6, /obj/structure/flora/ash/chilly = 1)
diff --git a/code/datums/mapgen/Cavegens/LavalandGenerator.dm b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
index be8294fada66..6f8aa7ed1bbf 100644
--- a/code/datums/mapgen/Cavegens/LavalandGenerator.dm
+++ b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
@@ -8,8 +8,8 @@
/obj/effect/spawner/random/lavaland_mob/legion = 30,
/obj/effect/spawner/random/lavaland_mob/watcher = 40,
/mob/living/basic/mining/bileworm = 20,
+ /mob/living/basic/mining/brimdemon = 20,
/mob/living/basic/mining/lobstrosity/lava = 20,
- /mob/living/simple_animal/hostile/asteroid/brimdemon = 20,
/mob/living/basic/mining/goldgrub = 10,
/obj/structure/spawner/lavaland = 2,
/obj/structure/spawner/lavaland/goliath = 3,
diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm
index a6cfbb3d7259..1ffa67397e0a 100644
--- a/code/datums/martial/_martial.dm
+++ b/code/datums/martial/_martial.dm
@@ -91,7 +91,3 @@
if(help_verb)
remove_verb(holder_living, help_verb)
return
-
-///Gets called when a projectile hits the owner. Returning anything other than BULLET_ACT_HIT will stop the projectile from hitting the mob.
-/datum/martial_art/proc/on_projectile_hit(mob/living/attacker, obj/projectile/P, def_zone)
- return BULLET_ACT_HIT
diff --git a/code/datums/martial/sleeping_carp.dm b/code/datums/martial/sleeping_carp.dm
index 72e6cd5ada37..2ed42452d406 100644
--- a/code/datums/martial/sleeping_carp.dm
+++ b/code/datums/martial/sleeping_carp.dm
@@ -15,11 +15,13 @@
return
target.add_traits(list(TRAIT_NOGUNS, TRAIT_HARDLY_WOUNDED, TRAIT_NODISMEMBER), SLEEPING_CARP_TRAIT)
RegisterSignal(target, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby))
+ RegisterSignal(target, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(hit_by_projectile))
target.faction |= FACTION_CARP //:D
/datum/martial_art/the_sleeping_carp/on_remove(mob/living/target)
target.remove_traits(list(TRAIT_NOGUNS, TRAIT_HARDLY_WOUNDED, TRAIT_NODISMEMBER), SLEEPING_CARP_TRAIT)
UnregisterSignal(target, COMSIG_PARENT_ATTACKBY)
+ UnregisterSignal(target, COMSIG_ATOM_PRE_BULLET_ACT)
target.faction -= FACTION_CARP //:(
. = ..()
@@ -113,6 +115,8 @@
return ..()
/datum/martial_art/the_sleeping_carp/proc/can_deflect(mob/living/carp_user)
+ if(!can_use(carp_user) || !carp_user.throw_mode)
+ return FALSE
if(carp_user.incapacitated(IGNORE_GRAB)) //NO STUN
return FALSE
if(!(carp_user.mobility_flags & MOBILITY_USE)) //NO UNABLE TO USE
@@ -124,17 +128,20 @@
return FALSE
return TRUE
-/datum/martial_art/the_sleeping_carp/on_projectile_hit(mob/living/carp_user, obj/projectile/P, def_zone)
- . = ..()
+/datum/martial_art/the_sleeping_carp/proc/hit_by_projectile(mob/living/carp_user, obj/projectile/hitting_projectile, def_zone)
+ SIGNAL_HANDLER
+
if(!can_deflect(carp_user))
- return BULLET_ACT_HIT
- if(carp_user.throw_mode)
- carp_user.visible_message(span_danger("[carp_user] effortlessly swats the projectile aside! They can block bullets with their bare hands!"), span_userdanger("You deflect the projectile!"))
- playsound(get_turf(carp_user), pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE)
- P.firer = carp_user
- P.set_angle(rand(0, 360))//SHING
- return BULLET_ACT_FORCE_PIERCE
- return BULLET_ACT_HIT
+ return NONE
+
+ carp_user.visible_message(
+ span_danger("[carp_user] effortlessly swats [hitting_projectile] aside! [carp_user.p_They()] can block bullets with [carp_user.p_their()] bare hands!"),
+ span_userdanger("You deflect [hitting_projectile]!"),
+ )
+ playsound(carp_user, pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE)
+ hitting_projectile.firer = carp_user
+ hitting_projectile.set_angle(rand(0, 360))//SHING
+ return COMPONENT_BULLET_PIERCED
///Signal from getting attacked with an item, for a special interaction with touch spells
/datum/martial_art/the_sleeping_carp/proc/on_attackby(mob/living/carp_user, obj/item/attack_weapon, mob/attacker, params)
diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm
index ae49549a68d9..76cce94bcafa 100644
--- a/code/datums/memory/_memory.dm
+++ b/code/datums/memory/_memory.dm
@@ -230,6 +230,7 @@
var/static/list/something_pool = list(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -237,9 +238,8 @@
/mob/living/basic/cow,
/mob/living/basic/cow/wisdom,
/mob/living/basic/crab,
- /mob/living/basic/spider/giant,
- /mob/living/basic/spider/giant/hunter,
- /mob/living/basic/mining/goliath,
+ /mob/living/basic/goat,
+ /mob/living/basic/gorilla,
/mob/living/basic/headslug,
/mob/living/basic/killer_tomato,
/mob/living/basic/lizard,
@@ -248,6 +248,7 @@
/mob/living/basic/morph,
/mob/living/basic/mouse,
/mob/living/basic/mushroom,
+ /mob/living/basic/parrot,
/mob/living/basic/pet/dog/breaddog,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
@@ -257,11 +258,7 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/hostile/retaliate/goat,
- /mob/living/simple_animal/parrot,
/mob/living/simple_animal/pet/cat,
/mob/living/simple_animal/pet/cat/cak,
/obj/item/food/sausage/american,
diff --git a/code/datums/mind/initialization.dm b/code/datums/mind/initialization.dm
index 12a5dddb229c..eb622cc5af54 100644
--- a/code/datums/mind/initialization.dm
+++ b/code/datums/mind/initialization.dm
@@ -11,6 +11,7 @@
mind.set_current(src)
// There's nowhere else to set this up, mind code makes me depressed
mind.antag_hud = add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/antagonist_hud, "combo_hud", mind)
+ SEND_SIGNAL(src, COMSIG_MOB_MIND_INITIALIZED, mind)
/mob/living/carbon/mind_initialize()
..()
diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index ef4cec4ea467..7603e275f8c3 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -143,6 +143,9 @@
mood_change = -4
timeout = 2 MINUTES
+/datum/mood_event/healsbadman/long_term
+ timeout = 10 MINUTES
+
/datum/mood_event/jittery
description = "I'm nervous and on edge and I can't stand still!!"
mood_change = -2
diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm
index 4186ade577f6..ca3f0820ea01 100644
--- a/code/datums/mutations/antenna.dm
+++ b/code/datums/mutations/antenna.dm
@@ -91,32 +91,11 @@
// chance to alert the read-ee
to_chat(cast_on, span_danger("You feel something foreign enter your mind."))
- var/list/recent_speech = list()
- var/list/say_log = list()
- var/log_source = cast_on.logging
- //this whole loop puts the read-ee's say logs into say_log in an easy to access way
- for(var/log_type in log_source)
- var/nlog_type = text2num(log_type)
- if(nlog_type & LOG_SAY)
- var/list/reversed = log_source[log_type]
- if(islist(reversed))
- say_log = reverse_range(reversed.Copy())
- break
-
- for(var/spoken_memory in say_log)
- //up to 3 random lines of speech, favoring more recent speech
- if(length(recent_speech) >= 3)
- break
- if(prob(50))
- continue
- // log messages with tags like telepathy are displayed like "(Telepathy to Ckey/(target)) "greetings"""
- // by splitting the text by using a " delimiter, we can grab JUST the greetings part
- recent_speech[spoken_memory] = splittext(say_log[spoken_memory], "\"", 1, 0, TRUE)[3]
-
+ var/list/recent_speech = cast_on.copy_recent_speech(copy_amount = 3, line_chance = 50)
if(length(recent_speech))
to_chat(owner, span_boldnotice("You catch some drifting memories of their past conversations..."))
for(var/spoken_memory in recent_speech)
- to_chat(owner, span_notice("[recent_speech[spoken_memory]]"))
+ to_chat(owner, span_notice("[spoken_memory]"))
if(iscarbon(cast_on))
var/mob/living/carbon/carbon_cast_on = cast_on
diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm
index e1784f009c8f..77bd426ed299 100644
--- a/code/datums/mutations/hulk.dm
+++ b/code/datums/mutations/hulk.dm
@@ -63,13 +63,19 @@
*arg1 is the arm to evaluate damage of and possibly break.
*/
/datum/mutation/human/hulk/proc/break_an_arm(obj/item/bodypart/arm)
+ var/severity
switch(arm.brute_dam)
if(45 to 50)
- arm.force_wound_upwards(/datum/wound/blunt/critical)
+ severity = WOUND_SEVERITY_CRITICAL
if(41 to 45)
- arm.force_wound_upwards(/datum/wound/blunt/severe)
+ severity = WOUND_SEVERITY_SEVERE
if(35 to 41)
- arm.force_wound_upwards(/datum/wound/blunt/moderate)
+ severity = WOUND_SEVERITY_MODERATE
+
+ if (isnull(severity))
+ return
+
+ owner.cause_wound_of_type_and_severity(WOUND_BLUNT, arm, severity, wound_source = "hulk smashing")
/datum/mutation/human/hulk/on_life(seconds_per_tick, times_fired)
if(owner.health < owner.crit_threshold)
diff --git a/code/datums/mutations/void_magnet.dm b/code/datums/mutations/void_magnet.dm
index 56b22d664a85..d6636b0b6306 100644
--- a/code/datums/mutations/void_magnet.dm
+++ b/code/datums/mutations/void_magnet.dm
@@ -60,7 +60,7 @@
/datum/action/cooldown/spell/void/cursed/proc/on_life(mob/living/source, seconds_per_tick, times_fired)
SIGNAL_HANDLER
- if(!isliving(source) || IS_IN_STASIS(source) || source.stat == DEAD || source.notransform)
+ if(!isliving(source) || IS_IN_STASIS(source) || source.stat == DEAD || HAS_TRAIT(source, TRAIT_NO_TRANSFORM))
return
if(!is_valid_target(source))
diff --git a/code/datums/proximity_monitor/fields/timestop.dm b/code/datums/proximity_monitor/fields/timestop.dm
index 7fad290fc74d..86ea41aee013 100644
--- a/code/datums/proximity_monitor/fields/timestop.dm
+++ b/code/datums/proximity_monitor/fields/timestop.dm
@@ -36,7 +36,7 @@
for(var/mob/living/to_check in GLOB.player_list)
if(HAS_TRAIT(to_check, TRAIT_TIME_STOP_IMMUNE))
immune[to_check] = TRUE
- for(var/mob/living/simple_animal/hostile/guardian/stand in GLOB.parasites)
+ for(var/mob/living/basic/guardian/stand in GLOB.parasites)
if(stand.summoner && HAS_TRAIT(stand.summoner, TRAIT_TIME_STOP_IMMUNE)) //It would only make sense that a person's stand would also be immune.
immune[stand] = TRUE
if(start)
diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm
index d6483e7e0c34..b0d73d36a919 100644
--- a/code/datums/ruins/lavaland.dm
+++ b/code/datums/ruins/lavaland.dm
@@ -268,3 +268,28 @@
cost = 5
suffix = "lavaland_surface_bileworm_nest.dmm"
allow_duplicates = FALSE
+
+/datum/map_template/ruin/lavaland/lava_phonebooth
+ name = "Phonebooth"
+ id = "lava_phonebooth"
+ description = "A venture by nanotrasen to help popularize the use of holopads. This one somehow made its way here."
+ suffix = "lavaland_surface_phonebooth.dmm"
+ allow_duplicates = FALSE
+ cost = 5
+
+/datum/map_template/ruin/lavaland/battle_site
+ name = "Battle Site"
+ id = "battle_site"
+ description = "The long past site of a battle between beast and humanoids. The victor is unknown, but the losers are clear."
+ suffix = "lavaland_battle_site.dmm"
+ allow_duplicates = TRUE
+ cost = 3
+
+/datum/map_template/ruin/lavaland/mook_village
+ name = "Mook Village"
+ id = "mook_village"
+ description = "A village hosting a community of friendly mooks!"
+ suffix = "lavaland_surface_mookvillage.dmm"
+ allow_duplicates = FALSE
+ cost = 5
+ always_place = TRUE
diff --git a/code/datums/saymode.dm b/code/datums/saymode.dm
index 78ec5e6af38a..60d3bd594e2d 100644
--- a/code/datums/saymode.dm
+++ b/code/datums/saymode.dm
@@ -77,8 +77,8 @@
/datum/saymode/binary/handle_message(mob/living/user, message, datum/language/language)
if(isdrone(user))
- var/mob/living/simple_animal/drone/D = user
- D.drone_chat(message)
+ var/mob/living/basic/drone/drone_user = user
+ drone_user.drone_chat(message)
return FALSE
if(user.binarycheck())
user.robot_talk(message)
diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm
index a328506bb959..0eb7668c6d8e 100644
--- a/code/datums/station_traits/negative_traits.dm
+++ b/code/datums/station_traits/negative_traits.dm
@@ -381,7 +381,7 @@
if(istype(current_thing, /obj/machinery/vending) && prob(45))
var/obj/machinery/vending/vendor_to_trash = current_thing
if(prob(50))
- vendor_to_trash.tilt(get_turf(vendor_to_trash))
+ vendor_to_trash.tilt(get_turf(vendor_to_trash), 0) // crit effects can do some real weird shit, lets disable it
if(prob(50))
vendor_to_trash.take_damage(150)
diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm
index 494008bb8f79..2c86b6188417 100644
--- a/code/datums/station_traits/neutral_traits.dm
+++ b/code/datums/station_traits/neutral_traits.dm
@@ -148,7 +148,7 @@
show_in_report = FALSE // Selective attention test. Did you spot the gorilla?
/// The gorilla we created, we only hold this ref until the round starts.
- var/mob/living/simple_animal/hostile/gorilla/cargo_domestic/cargorilla
+ var/mob/living/basic/gorilla/cargorilla/cargorilla
/datum/station_trait/cargorilla/New()
. = ..()
@@ -158,8 +158,8 @@
/datum/station_trait/cargorilla/proc/replace_cargo(datum/source)
SIGNAL_HANDLER
- var/mob/living/simple_animal/sloth/cargo_sloth = GLOB.cargo_sloth
- if(!cargo_sloth)
+ var/mob/living/basic/sloth/cargo_sloth = GLOB.cargo_sloth
+ if(isnull(cargo_sloth))
return
cargorilla = new(cargo_sloth.loc)
@@ -189,7 +189,7 @@
cargorilla = null
/// Get us a ghost for the gorilla.
-/datum/station_trait/cargorilla/proc/get_ghost_for_gorilla(mob/living/simple_animal/hostile/gorilla/cargo_domestic/gorilla)
+/datum/station_trait/cargorilla/proc/get_ghost_for_gorilla(mob/living/basic/gorilla/cargorilla/gorilla)
if(QDELETED(gorilla))
return
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index 72dece18b8bd..4dea910b43a6 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -294,10 +294,10 @@
/datum/status_effect/hippocratic_oath/proc/consume_owner()
owner.visible_message(span_notice("[owner]'s soul is absorbed into the rod, relieving the previous snake of its duty."))
var/list/chems = list(/datum/reagent/medicine/sal_acid, /datum/reagent/medicine/c2/convermol, /datum/reagent/medicine/oxandrolone)
- var/mob/living/simple_animal/hostile/retaliate/snake/healSnake = new(owner.loc, pick(chems))
- healSnake.name = "Asclepius's Snake"
- healSnake.real_name = "Asclepius's Snake"
- healSnake.desc = "A mystical snake previously trapped upon the Rod of Asclepius, now freed of its burden. Unlike the average snake, its bites contain chemicals with minor healing properties."
+ var/mob/living/basic/snake/spawned = new(owner.loc, pick(chems))
+ spawned.name = "Asclepius's Snake"
+ spawned.real_name = "Asclepius's Snake"
+ spawned.desc = "A mystical snake previously trapped upon the Rod of Asclepius, now freed of its burden. Unlike the average snake, its bites contain chemicals with minor healing properties."
new /obj/effect/decal/cleanable/ash(owner.loc)
new /obj/item/rod_of_asclepius(owner.loc)
owner.investigate_log("has been consumed by the Rod of Asclepius.", INVESTIGATE_DEATHS)
diff --git a/code/datums/status_effects/debuffs/choke.dm b/code/datums/status_effects/debuffs/choke.dm
index 16c64376cb85..d2e502d75de7 100644
--- a/code/datums/status_effects/debuffs/choke.dm
+++ b/code/datums/status_effects/debuffs/choke.dm
@@ -215,8 +215,8 @@
if(iscarbon(victim))
var/mob/living/carbon/carbon_victim = victim
var/obj/item/bodypart/chest = carbon_victim.get_bodypart(BODY_ZONE_CHEST)
- if(chest)
- chest.force_wound_upwards(/datum/wound/blunt/severe)
+ carbon_victim.cause_wound_of_type_and_severity(WOUND_BLUNT, chest, WOUND_SEVERITY_SEVERE, wound_source = "human force to the chest")
+
playsound(owner, 'sound/creatures/crack_vomit.ogg', 120, extrarange = 5, falloff_exponent = 4)
vomit_up()
diff --git a/code/datums/status_effects/debuffs/debuffs.dm b/code/datums/status_effects/debuffs/debuffs.dm
index a1708a69d893..d4b4f789f0e5 100644
--- a/code/datums/status_effects/debuffs/debuffs.dm
+++ b/code/datums/status_effects/debuffs/debuffs.dm
@@ -397,7 +397,9 @@
var/still_bleeding = FALSE
for(var/datum/wound/bleeding_thing as anything in throat.wounds)
- if(bleeding_thing.wound_type == WOUND_SLASH && bleeding_thing.severity > WOUND_SEVERITY_MODERATE)
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[bleeding_thing.type]
+
+ if(pregen_data.wounding_types_valid(list(WOUND_SLASH)) && bleeding_thing.severity > WOUND_SEVERITY_MODERATE && bleeding_thing.blood_flow > 0)
still_bleeding = TRUE
break
if(!still_bleeding)
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index 369c4dc44e5d..9ffdd73a741f 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -256,10 +256,10 @@
if(firelight_type)
firelight_ref = WEAKREF(new firelight_type(owner))
- SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
cache_stacks()
update_overlay()
update_particles()
+ SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
return TRUE
/**
diff --git a/code/datums/status_effects/wound_effects.dm b/code/datums/status_effects/wound_effects.dm
index d0bdbd536024..ed0b7b555e46 100644
--- a/code/datums/status_effects/wound_effects.dm
+++ b/code/datums/status_effects/wound_effects.dm
@@ -54,11 +54,11 @@
right = C.get_bodypart(BODY_ZONE_R_LEG)
update_limp()
RegisterSignal(C, COMSIG_MOVABLE_MOVED, PROC_REF(check_step))
- RegisterSignals(C, list(COMSIG_CARBON_GAIN_WOUND, COMSIG_CARBON_LOSE_WOUND, COMSIG_CARBON_ATTACH_LIMB, COMSIG_CARBON_REMOVE_LIMB), PROC_REF(update_limp))
+ RegisterSignals(C, list(COMSIG_CARBON_GAIN_WOUND, COMSIG_CARBON_POST_LOSE_WOUND, COMSIG_CARBON_ATTACH_LIMB, COMSIG_CARBON_REMOVE_LIMB), PROC_REF(update_limp))
return TRUE
/datum/status_effect/limp/on_remove()
- UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_GAIN_WOUND, COMSIG_CARBON_LOSE_WOUND, COMSIG_CARBON_ATTACH_LIMB, COMSIG_CARBON_REMOVE_LIMB))
+ UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_GAIN_WOUND, COMSIG_CARBON_POST_LOSE_WOUND, COMSIG_CARBON_ATTACH_LIMB, COMSIG_CARBON_REMOVE_LIMB))
/atom/movable/screen/alert/status_effect/limp
name = "Limping"
@@ -165,51 +165,41 @@
if(W == linked_wound)
qdel(src)
+/datum/status_effect/wound/nextmove_modifier()
+ var/mob/living/carbon/C = owner
-// bones
-/datum/status_effect/wound/blunt
-
-/datum/status_effect/wound/blunt/on_apply()
- . = ..()
- RegisterSignal(owner, COMSIG_MOB_SWAP_HANDS, PROC_REF(on_swap_hands))
- on_swap_hands()
-
-/datum/status_effect/wound/blunt/on_remove()
- . = ..()
- UnregisterSignal(owner, COMSIG_MOB_SWAP_HANDS)
- var/mob/living/carbon/wound_owner = owner
- wound_owner.remove_actionspeed_modifier(/datum/actionspeed_modifier/blunt_wound)
-
-/datum/status_effect/wound/blunt/proc/on_swap_hands()
- SIGNAL_HANDLER
+ if(C.get_active_hand() == linked_limb)
+ return linked_wound.get_action_delay_mult()
- var/mob/living/carbon/wound_owner = owner
- if(wound_owner.get_active_hand() == linked_limb)
- wound_owner.add_actionspeed_modifier(/datum/actionspeed_modifier/blunt_wound, (linked_wound.interaction_efficiency_penalty - 1))
- else
- wound_owner.remove_actionspeed_modifier(/datum/actionspeed_modifier/blunt_wound)
+ return ..()
-/datum/status_effect/wound/blunt/nextmove_modifier()
+/datum/status_effect/wound/nextmove_adjust()
var/mob/living/carbon/C = owner
if(C.get_active_hand() == linked_limb)
- return linked_wound.interaction_efficiency_penalty
+ return linked_wound.get_action_delay_increment()
+
+ return ..()
- return 1
+
+// bones
+/datum/status_effect/wound/blunt/bone
// blunt
-/datum/status_effect/wound/blunt/moderate
+/datum/status_effect/wound/blunt/bone/moderate
id = "disjoint"
-/datum/status_effect/wound/blunt/severe
+/datum/status_effect/wound/blunt/bone/severe
id = "hairline"
-/datum/status_effect/wound/blunt/critical
+/datum/status_effect/wound/blunt/bone/critical
id = "compound"
+
// slash
-/datum/status_effect/wound/slash/moderate
+
+/datum/status_effect/wound/slash/flesh/moderate
id = "abrasion"
-/datum/status_effect/wound/slash/severe
+/datum/status_effect/wound/slash/flesh/severe
id = "laceration"
-/datum/status_effect/wound/slash/critical
+/datum/status_effect/wound/slash/flesh/critical
id = "avulsion"
// pierce
/datum/status_effect/wound/pierce/moderate
@@ -219,9 +209,9 @@
/datum/status_effect/wound/pierce/critical
id = "rupture"
// burns
-/datum/status_effect/wound/burn/moderate
+/datum/status_effect/wound/burn/flesh/moderate
id = "seconddeg"
-/datum/status_effect/wound/burn/severe
+/datum/status_effect/wound/burn/flesh/severe
id = "thirddeg"
-/datum/status_effect/wound/burn/critical
+/datum/status_effect/wound/burn/flesh/critical
id = "fourthdeg"
diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm
index ce0175849090..72a326a2eebb 100644
--- a/code/datums/storage/storage.dm
+++ b/code/datums/storage/storage.dm
@@ -404,6 +404,7 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
to_insert.forceMove(resolve_location)
item_insertion_feedback(user, to_insert, override)
resolve_location.update_appearance()
+ SEND_SIGNAL(to_insert, COMSIG_ITEM_STORED)
return TRUE
/**
diff --git a/code/datums/wires/airlock.dm b/code/datums/wires/airlock.dm
index 1414fb55a441..e289110a6814 100644
--- a/code/datums/wires/airlock.dm
+++ b/code/datums/wires/airlock.dm
@@ -69,14 +69,14 @@
/datum/wires/airlock/interactable(mob/user)
if(!..())
return FALSE
- var/obj/machinery/door/airlock/A = holder
- if(!issilicon(user) && A.isElectrified())
+ var/obj/machinery/door/airlock/airlock = holder
+ if(!issilicon(user) && !isdrone(user) && airlock.isElectrified())
var/mob/living/carbon/carbon_user = user
if (!istype(carbon_user) || carbon_user.should_electrocute(src))
return FALSE
- if(A.is_secure())
+ if(airlock.is_secure())
return FALSE
- if(A.panel_open)
+ if(airlock.panel_open)
return TRUE
/datum/wires/airlock/get_status()
diff --git a/code/datums/wounds/_wound_static_data.dm b/code/datums/wounds/_wound_static_data.dm
new file mode 100644
index 000000000000..7a59ea57413a
--- /dev/null
+++ b/code/datums/wounds/_wound_static_data.dm
@@ -0,0 +1,199 @@
+// This datum is merely a singleton instance that allows for custom "can be applied" behaviors without instantiating a wound instance.
+// For example: You can make a pregen_data subtype for your wound that overrides can_be_applied_to to only apply to specifically slimeperson limbs.
+// Without this, youre stuck with very static initial variables.
+
+/// A singleton datum that holds pre-gen and static data about a wound. Each wound datum should have a corresponding wound_pregen_data.
+/datum/wound_pregen_data
+ /// The typepath of the wound we will be handling and storing data of. NECESSARY IF THIS IS A NON-ABSTRACT TYPE!
+ var/datum/wound/wound_path_to_generate
+
+ /// Will this be instantiated?
+ var/abstract = FALSE
+
+ /// If true, our wound can be selected in ordinary wound rolling. If this is set to false, our wound can only be directly instantiated by use of specific typepath.
+ var/can_be_randomly_generated = TRUE
+
+ /// A list of biostates a limb must have to receive our wound, in wounds.dm.
+ var/required_limb_biostate
+ /// If false, we will check if the limb has all of our required biostates instead of just any.
+ var/require_any_biostate = FALSE
+
+ /// If false, we will iterate through wounds on a given limb, and if any match our type, we wont add our wound.
+ var/duplicates_allowed = FALSE
+
+ /// If we require BIO_BLOODED, we will not add our wound if this is true and the limb cannot bleed.
+ var/ignore_cannot_bleed = TRUE // a lot of bleed wounds should still be applied for purposes of mangling flesh
+
+ /// A list of bodyzones we are applicable to.
+ var/list/viable_zones = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
+ /// The types of attack that can generate this wound. E.g. WOUND_SLASH = A sharp attack can cause this, WOUND_BLUNT = an attack with no sharpness/an attack with sharpness against a limb with mangled exterior can cause this.
+ var/list/required_wounding_types
+ /// If true, this wound can only be generated by all [required_wounding_types] at once, not just any.
+ var/match_all_wounding_types = FALSE
+
+ /// The weight that will be used if, by the end of wound selection, there are multiple valid wounds. This will be inserted into pick_weight, so use integers.
+ var/weight = WOUND_DEFAULT_WEIGHT
+
+ /// The minimum injury roll a attack must get to generate us. Affected by our wound's threshold_penalty and series_threshold_penalty, as well as the attack's wound_bonus. See check_wounding_mods().
+ var/threshold_minimum
+
+ /// The series of wounds this is in. See wounds.dm (the defines file) for a more detailed explanation - but tldr is that no 2 wounds of the same series can be on a limb.
+ var/wound_series
+
+ /// If true, we will attempt to, during a random wound roll, overpower and remove other wound typepaths from the possible wounds list using [competition_mode] and [overpower_wounds_of_even_severity].
+ var/compete_for_wounding = TRUE
+ /// The competition mode with which we will remove other wounds from a possible wound roll assuming [compete_for_wounding] is TRUE. See wounds.dm, the defines file, for more information on what these do.
+ var/competition_mode = WOUND_COMPETITION_OVERPOWER_LESSERS
+ /// If this and [compete_for_wounding] is true, we will remove wounds of an even severity to us during a random wound roll.
+ var/overpower_wounds_of_even_severity = FALSE
+
+ /// A list of BIO_ defines that will be iterated over in order to determine the scar file our wound will generate.
+ /// Use generate_scar_priorities to create a custom list.
+ var/list/scar_priorities
+
+/datum/wound_pregen_data/New()
+ . = ..()
+
+ if (!abstract)
+ if (required_limb_biostate == null)
+ stack_trace("required_limb_biostate null - please set it! occured on: [src.type]")
+ if (wound_path_to_generate == null)
+ stack_trace("wound_path_to_generate null - please set it! occured on: [src.type]")
+
+ scar_priorities = generate_scar_priorities()
+
+/// Should return a list of BIO_ biostate priorities, in order. See [scar_priorities] for further documentation.
+/datum/wound_pregen_data/proc/generate_scar_priorities()
+ RETURN_TYPE(/list)
+
+ var/list/priorities = list(
+ "[BIO_FLESH]",
+ "[BIO_BONE]",
+ )
+
+ return priorities
+
+// this proc is the primary reason this datum exists - a singleton instance so we can always run this proc even without the wound existing
+/**
+ * Args:
+ * * obj/item/bodypart/limb: The limb we are considering.
+ * * list/suggested_wounding_types: The wounding types to be checked against the wounding types we require. Defaults to required_wounding_types.
+ * * datum/wound/old_wound: If we would replace a wound, this would be said wound. Nullable.
+ * * random_roll = FALSE: If this is in the context of a random wound generation, and this wound wasn't specifically checked.
+ *
+ * Returns:
+ * FALSE if the limb cannot be wounded, if the wounding types dont match ours (via wounding_types_valid()), if we have a higher severity wound already in our series,
+ * if we have a biotype mismatch, if the limb isnt in a viable zone, or if theres any duplicate wound types.
+ * TRUE otherwise.
+ */
+/datum/wound_pregen_data/proc/can_be_applied_to(obj/item/bodypart/limb, list/suggested_wounding_types = required_wounding_types, datum/wound/old_wound, random_roll = FALSE, duplicates_allowed = src.duplicates_allowed, care_about_existing_wounds = TRUE)
+ SHOULD_BE_PURE(TRUE)
+
+ if (!istype(limb) || !limb.owner)
+ return FALSE
+
+ if (random_roll && !can_be_randomly_generated)
+ return FALSE
+
+ if (HAS_TRAIT(limb.owner, TRAIT_NEVER_WOUNDED) || (limb.owner.status_flags & GODMODE))
+ return FALSE
+
+ if (!wounding_types_valid(suggested_wounding_types))
+ return FALSE
+
+ if (care_about_existing_wounds)
+ for (var/datum/wound/preexisting_wound as anything in limb.wounds)
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[preexisting_wound.type]
+ if (pregen_data.wound_series == wound_series)
+ if (preexisting_wound.severity >= initial(wound_path_to_generate.severity))
+ return FALSE
+
+ if (!ignore_cannot_bleed && ((required_limb_biostate & BIO_BLOODED) && !limb.can_bleed()))
+ return FALSE
+
+ if (!biostate_valid(limb.biological_state))
+ return FALSE
+
+ if (!(limb.body_zone in viable_zones))
+ return FALSE
+
+ // we accept promotions and demotions, but no point in redundancy. This should have already been checked wherever the wound was rolled and applied for (see: bodypart damage code), but we do an extra check
+ // in case we ever directly add wounds
+ if (!duplicates_allowed)
+ for (var/datum/wound/preexisting_wound as anything in limb.wounds)
+ if (preexisting_wound.type == wound_path_to_generate && (preexisting_wound != old_wound))
+ return FALSE
+ return TRUE
+
+/// Returns true if we have the given biostates, or any biostate in it if check_for_any is true. False otherwise.
+/datum/wound_pregen_data/proc/biostate_valid(biostate)
+ if (require_any_biostate)
+ if (!(biostate & required_limb_biostate))
+ return FALSE
+ else if (!((biostate & required_limb_biostate) == required_limb_biostate)) // check for all
+ return FALSE
+
+ return TRUE
+
+/**
+ * A simple getter for [weight], with arguments supplied to allow custom behavior.
+ *
+ * Args:
+ * * obj/item/bodypart/limb: The limb we are contemplating being added to. Nullable.
+ * * woundtype: The woundtype of the assumed attack that would generate us. Nullable.
+ * * damage: The raw damage that would cause us. Nullable.
+ * * attack_direction: The direction of the attack that'd cause us. Nullable.
+ * * damage_source: The entity that would cause us. Nullable.
+ *
+ * Returns:
+ * Our weight.
+ */
+/datum/wound_pregen_data/proc/get_weight(obj/item/bodypart/limb, woundtype, damage, attack_direction, damage_source)
+ return weight
+
+/// Returns TRUE if we use WOUND_ALL, or we require all types and have all/if we require any and have any, FALSE otherwise.
+/datum/wound_pregen_data/proc/wounding_types_valid(list/suggested_wounding_types)
+ if (WOUND_ALL in required_wounding_types)
+ return TRUE
+ if (!length(suggested_wounding_types))
+ return FALSE
+
+ for (var/iter_wounding_type as anything in suggested_wounding_types)
+ if (!(iter_wounding_type in required_wounding_types))
+ if (match_all_wounding_types)
+ return FALSE
+ else
+ if (!match_all_wounding_types)
+ return TRUE
+
+ return match_all_wounding_types // if we get here, we've matched everything
+
+/**
+ * A simple getter for [threshold_minimum], with arguments supplied to allow custom behavior.
+ *
+ * Args:
+ * * obj/item/bodypart/part: The limb we are contemplating being added to.
+ * * attack_direction: The direction of the attack that'd generate us. Nullable.
+ * * damage_source: The source of the damage that'd cause us. Nullable.
+ */
+/datum/wound_pregen_data/proc/get_threshold_for(obj/item/bodypart/part, attack_direction, damage_source)
+ return threshold_minimum
+
+/// Returns a new instance of our wound datum.
+/datum/wound_pregen_data/proc/generate_instance(obj/item/bodypart/limb, ...)
+ RETURN_TYPE(/datum/wound)
+
+ return new wound_path_to_generate
+
+/datum/wound_pregen_data/Destroy(force, ...)
+ var/error_message = "[src], a singleton wound pregen data instance, was destroyed! This should not happen!"
+ if (force)
+ error_message += " NOTE: This Destroy() was called with force == TRUE. This instance will be deleted and replaced with a new one."
+ stack_trace(error_message)
+
+ if (!force)
+ return QDEL_HINT_LETMELIVE
+
+ . = ..()
+
+ GLOB.all_wound_pregen_data[wound_path_to_generate] = new src.type //recover
diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm
index 6f64a559ae1e..65669db9fd14 100644
--- a/code/datums/wounds/_wounds.dm
+++ b/code/datums/wounds/_wounds.dm
@@ -14,6 +14,13 @@
deciding what specific wound will be applied. I'd like to have a few different types of wounds for at least some of the choices, but I'm just doing rough generals for now. Expect polishing
*/
+#define WOUND_CRITICAL_BLUNT_DISMEMBER_BONUS 15
+
+// Applied into wounds when they're scanned with the wound analyzer, halves time to treat them manually.
+#define TRAIT_WOUND_SCANNED "wound_scanned"
+// I dunno lol
+#define ANALYZER_TRAIT "analyzer_trait"
+
/datum/wound
/// What it's named
var/name = "Wound"
@@ -24,51 +31,61 @@
/// What the limb looks like on a cursory examine
var/examine_desc = "is badly hurt"
+ /// Simple description, shortened for clarity if defined. Otherwise just takes the normal desc in the analyzer proc.
+ var/simple_desc
+ /// Simple analyzer's wound description, which focuses less on the clinical aspect of the wound and more on easily readable treatment instructions.
+ var/simple_treat_text = "Go to medbay idiot"
+ /// Improvised remedies indicated by the first aid analyzer only.
+ var/homemade_treat_text = "Remember to drink lots of water!"
+
+
+ /// If this wound can generate a scar.
+ var/can_scar = TRUE
+
+ /// The default file we take our scar descriptions from, if we fail to get the ideal file.
+ var/default_scar_file
+
/// needed for "your arm has a compound fracture" vs "your arm has some third degree burns"
var/a_or_from = "a"
/// The visible message when this happens
var/occur_text = ""
/// This sound will be played upon the wound being applied
var/sound_effect
+ /// The volume of [sound_effect]
+ var/sound_volume = 70
/// Either WOUND_SEVERITY_TRIVIAL (meme wounds like stubbed toe), WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, or WOUND_SEVERITY_CRITICAL (or maybe WOUND_SEVERITY_LOSS)
var/severity = WOUND_SEVERITY_MODERATE
- /// The list of wounds it belongs in, WOUND_LIST_BLUNT, WOUND_LIST_SLASH, or WOUND_LIST_BURN
- var/wound_type
- /// What body zones can we affect
- var/list/viable_zones = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
/// Who owns the body part that we're wounding
var/mob/living/carbon/victim = null
- /// The bodypart we're parented to
+ /// The bodypart we're parented to. Not guaranteed to be non-null, especially after/during removal or if we haven't been applied
var/obj/item/bodypart/limb = null
/// Specific items such as bandages or sutures that can try directly treating this wound
var/list/treatable_by
/// Specific items such as bandages or sutures that can try directly treating this wound only if the user has the victim in an aggressive grab or higher
var/list/treatable_by_grabbed
- /// Tools with the specified tool flag will also be able to try directly treating this wound
- var/treatable_tool
+ /// Any tools with any of the flags in this list will be usable to try directly treating this wound
+ var/list/treatable_tools
/// How long it will take to treat this wound with a standard effective tool, assuming it doesn't need surgery
var/base_treat_time = 5 SECONDS
/// Using this limb in a do_after interaction will multiply the length by this duration (arms)
var/interaction_efficiency_penalty = 1
/// Incoming damage on this limb will be multiplied by this, to simulate tenderness and vulnerability (mostly burns).
- var/damage_mulitplier_penalty = 1
+ var/damage_multiplier_penalty = 1
/// If set and this wound is applied to a leg, we take this many deciseconds extra per step on this leg
var/limp_slowdown
/// If this wound has a limp_slowdown and is applied to a leg, it has this chance to limp each step
var/limp_chance
/// How much we're contributing to this limb's bleed_rate
var/blood_flow
- /// Essentially, keeps track of whether or not this wound is capable of bleeding (in case the owner has the NOBLOOD species trait)
- var/no_bleeding = FALSE
- /// The minimum we need to roll on [/obj/item/bodypart/proc/check_wounding] to begin suffering this wound, see check_wounding_mods() for more
- var/threshold_minimum
/// How much having this wound will add to all future check_wounding() rolls on this limb, to allow progression to worse injuries with repeated damage
var/threshold_penalty
+ /// How much having this wound will add to all future check_wounding() rolls on this limb, but only for wounds of its own series
+ var/series_threshold_penalty = 0
/// If we need to process each life tick
var/processes = FALSE
@@ -79,27 +96,83 @@
var/status_effect_type
/// If we're operating on this wound and it gets healed, we'll nix the surgery too
var/datum/surgery/attached_surgery
- /// if you're a lazy git and just throw them in cryo, the wound will go away after accumulating severity * 25 power
+ /// if you're a lazy git and just throw them in cryo, the wound will go away after accumulating severity * [base_xadone_progress_to_qdel] power
var/cryo_progress
+ /// The base amount of [cryo_progress] required to have ourselves fully healed by cryo. Multiplied against severity.
+ var/base_xadone_progress_to_qdel = 33
+
/// What kind of scars this wound will create description wise once healed
var/scar_keyword = "generic"
/// If we've already tried scarring while removing (remove_wound can be called twice in a del chain, let's be nice to our code yeah?) TODO: make this cleaner
var/already_scarred = FALSE
- /// If we forced this wound through badmin smite, we won't count it towards the round totals
- var/from_smite
+ /// The source of how we got the wound, typically a weapon.
+ var/wound_source
/// What flags apply to this wound
- var/wound_flags = (FLESH_WOUND | BONE_WOUND | ACCEPTS_GAUZE)
+ var/wound_flags = (ACCEPTS_GAUZE)
+
+ /// The unique ID of our wound for use with [actionspeed_mod]. Defaults to REF(src).
+ var/unique_id
+ /// The actionspeed modifier we will use in case we are on the arms and have a interaction penalty. Qdelled on destroy.
+ var/datum/actionspeed_modifier/wound_interaction_inefficiency/actionspeed_mod
+
+/datum/wound/New()
+ . = ..()
+
+ unique_id = generate_unique_id()
+ update_actionspeed_modifier()
/datum/wound/Destroy()
- if(attached_surgery)
- QDEL_NULL(attached_surgery)
- remove_wound()
- set_limb(null)
- victim = null
+ QDEL_NULL(attached_surgery)
+ if (limb)
+ remove_wound()
+
+ QDEL_NULL(actionspeed_mod)
+
return ..()
+/// If we should have an actionspeed_mod, ensures we do and updates its slowdown. Otherwise, ensures we dont have one
+/// by qdeleting any existing modifier.
+/datum/wound/proc/update_actionspeed_modifier()
+ if (should_have_actionspeed_modifier())
+ if (!actionspeed_mod)
+ generate_actionspeed_modifier()
+ actionspeed_mod.multiplicative_slowdown = get_effective_actionspeed_modifier()
+ victim?.update_actionspeed()
+ else
+ remove_actionspeed_modifier()
+
+/// Returns TRUE if we have an interaction_efficiency_penalty, and if we are on the arms, FALSE otherwise.
+/datum/wound/proc/should_have_actionspeed_modifier()
+ return (limb && victim && (limb.body_zone == BODY_ZONE_L_ARM || limb.body_zone == BODY_ZONE_R_ARM) && interaction_efficiency_penalty != 0)
+
+/// If we have no actionspeed_mod, generates a new one with our unique ID, sets actionspeed_mod to it, then returns it.
+/datum/wound/proc/generate_actionspeed_modifier()
+ RETURN_TYPE(/datum/actionspeed_modifier)
+
+ if (actionspeed_mod)
+ return actionspeed_mod
+
+ var/datum/actionspeed_modifier/wound_interaction_inefficiency/new_modifier = new /datum/actionspeed_modifier/wound_interaction_inefficiency(unique_id, src)
+ new_modifier.multiplicative_slowdown = get_effective_actionspeed_modifier()
+ victim?.add_actionspeed_modifier(new_modifier)
+
+ actionspeed_mod = new_modifier
+ return actionspeed_mod
+
+/// If we have an actionspeed_mod, qdels it and sets our ref of it to null.
+/datum/wound/proc/remove_actionspeed_modifier()
+ if (!actionspeed_mod)
+ return
+
+ victim?.remove_actionspeed_modifier(actionspeed_mod)
+ QDEL_NULL(actionspeed_mod)
+
+/// Generates the ID we use for [unique_id], which is also set as our actionspeed mod's ID
+/datum/wound/proc/generate_unique_id()
+ return REF(src) // unique, cannot change, a perfect id
+
/**
* apply_wound() is used once a wound type is instantiated to assign it to a bodypart, and actually come into play.
*
@@ -111,31 +184,16 @@
* * smited- If this is a smite, we don't care about this wound for stat tracking purposes (not yet implemented)
* * attack_direction: For bloodsplatters, if relevant
*/
-/datum/wound/proc/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null)
- if(!istype(L) || !L.owner || !(L.body_zone in viable_zones) || (!IS_ORGANIC_LIMB(L) && !HAS_TRAIT(L.owner, TRAIT_ROBOT_CAN_BLEED)) || HAS_TRAIT(L.owner, TRAIT_NEVER_WOUNDED))
- qdel(src)
- return
+/datum/wound/proc/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown")
- // Checks for biological state, to ensure only valid wounds are applied on the limb
- if(((wound_flags & BONE_WOUND) && !(L.biological_state & BIO_BONE)) || ((wound_flags & FLESH_WOUND) && !(L.biological_state & BIO_FLESH)))
+ if (!can_be_applied_to(L, old_wound))
qdel(src)
- return
-
- // we accept promotions and demotions, but no point in redundancy. This should have already been checked wherever the wound was rolled and applied for (see: bodypart damage code), but we do an extra check
- // in case we ever directly add wounds
- for(var/i in L.wounds)
- var/datum/wound/preexisting_wound = i
- if((preexisting_wound.type == type) && (preexisting_wound != old_wound))
- qdel(src)
- return
+ return FALSE
set_victim(L.owner)
set_limb(L)
LAZYADD(victim.all_wounds, src)
LAZYADD(limb.wounds, src)
- //it's ok to not typecheck, humans are the only ones that deal with wounds
- var/mob/living/carbon/human/human_victim = victim
- no_bleeding = HAS_TRAIT(human_victim, TRAIT_NOBLOOD)
update_descriptions()
limb.update_wounds()
if(status_effect_type)
@@ -155,18 +213,41 @@
var/msg = span_danger("[victim]'s [limb.plaintext_zone] [occur_text]!")
var/vis_dist = COMBAT_MESSAGE_RANGE
- if(severity != WOUND_SEVERITY_MODERATE)
+ if(severity > WOUND_SEVERITY_MODERATE)
msg = "[msg]"
vis_dist = DEFAULT_MESSAGE_RANGE
victim.visible_message(msg, span_userdanger("Your [limb.plaintext_zone] [occur_text]!"), vision_distance = vis_dist)
if(sound_effect)
- playsound(L.owner, sound_effect, 70 + 20 * severity, TRUE)
+ playsound(L.owner, sound_effect, sound_volume + (20 * severity), TRUE)
wound_injury(old_wound, attack_direction = attack_direction)
if(!demoted)
second_wind()
+ return TRUE
+
+/// Returns TRUE if we can be applied to the limb.
+/datum/wound/proc/can_be_applied_to(obj/item/bodypart/L, datum/wound/old_wound)
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[type]
+
+ // We assume we aren't being randomly applied - we have no reason to believe we are
+ // And, besides, if we were, you could just as easily check our pregen data rather than run this proc
+ // Generally speaking this proc is called in apply_wound, which is called when the caller is already confidant in its ability to be applied
+ return pregen_data.can_be_applied_to(L, old_wound = old_wound)
+
+/// Returns the zones we can be applied to.
+/datum/wound/proc/get_viable_zones()
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[type]
+
+ return pregen_data.viable_zones
+
+/// Returns the biostate we require to be applied.
+/datum/wound/proc/get_required_biostate()
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[type]
+
+ return pregen_data.required_limb_biostate
+
// Updates descriptive texts for the wound, in case it can get altered for whatever reason
/datum/wound/proc/update_descriptions()
return
@@ -175,13 +256,66 @@
SIGNAL_HANDLER
set_victim(null)
+/// Setter for [victim]. Should completely transfer signals, attributes, etc. To the new victim - if there is any, as it can be null.
/datum/wound/proc/set_victim(new_victim)
if(victim)
+ UnregisterSignal(victim, list(COMSIG_PARENT_QDELETING, COMSIG_MOB_SWAP_HANDS, COMSIG_CARBON_POST_REMOVE_LIMB, COMSIG_CARBON_POST_ATTACH_LIMB))
UnregisterSignal(victim, COMSIG_PARENT_QDELETING)
+ UnregisterSignal(victim, COMSIG_MOB_SWAP_HANDS)
+ UnregisterSignal(victim, COMSIG_CARBON_POST_REMOVE_LIMB)
+ if (actionspeed_mod)
+ victim.remove_actionspeed_modifier(actionspeed_mod) // no need to qdelete it, just remove it from our victim
+
remove_wound_from_victim()
victim = new_victim
if(victim)
RegisterSignal(victim, COMSIG_PARENT_QDELETING, PROC_REF(null_victim))
+ RegisterSignals(victim, list(COMSIG_MOB_SWAP_HANDS, COMSIG_CARBON_POST_REMOVE_LIMB, COMSIG_CARBON_POST_ATTACH_LIMB), PROC_REF(add_or_remove_actionspeed_mod))
+
+ if (limb)
+ start_limping_if_we_should() // the status effect already handles removing itself
+ add_or_remove_actionspeed_mod()
+
+/// Proc called to change the variable `limb` and react to the event.
+/datum/wound/proc/set_limb(obj/item/bodypart/new_value, replaced = FALSE)
+ if(limb == new_value)
+ return FALSE //Limb can either be a reference to something or `null`. Returning the number variable makes it clear no change was made.
+ . = limb
+ if(limb) // if we're nulling limb, we're basically detaching from it, so we should remove ourselves in that case
+ UnregisterSignal(limb, COMSIG_PARENT_QDELETING)
+ UnregisterSignal(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED))
+ LAZYREMOVE(limb.wounds, src)
+ limb.update_wounds(replaced)
+ if (disabling)
+ limb.remove_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src))
+
+ limb = new_value
+
+ // POST-CHANGE
+
+ if (limb)
+ RegisterSignal(limb, COMSIG_PARENT_QDELETING, PROC_REF(source_died))
+ RegisterSignals(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED), PROC_REF(gauze_state_changed))
+ if (disabling)
+ limb.add_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src))
+
+ if (victim)
+ start_limping_if_we_should() // the status effect already handles removing itself
+ add_or_remove_actionspeed_mod()
+
+ update_inefficiencies()
+
+/datum/wound/proc/add_or_remove_actionspeed_mod()
+ update_actionspeed_modifier()
+ if (actionspeed_mod)
+ if(victim.get_active_hand() == limb)
+ victim.add_actionspeed_modifier(actionspeed_mod, TRUE)
+ else
+ victim.remove_actionspeed_modifier(actionspeed_mod)
+
+/datum/wound/proc/start_limping_if_we_should()
+ if ((limb.body_zone == BODY_ZONE_L_LEG || limb.body_zone == BODY_ZONE_R_LEG) && limp_slowdown > 0 && limp_chance > 0)
+ victim.apply_status_effect(/datum/status_effect/limp)
/datum/wound/proc/source_died()
SIGNAL_HANDLER
@@ -190,15 +324,26 @@
/// Remove the wound from whatever it's afflicting, and cleans up whateverstatus effects it had or modifiers it had on interaction times. ignore_limb is used for detachments where we only want to forget the victim
/datum/wound/proc/remove_wound(ignore_limb, replaced = FALSE)
//TODO: have better way to tell if we're getting removed without replacement (full heal) scar stuff
+ var/old_victim = victim
+ var/old_limb = limb
+
set_disabling(FALSE)
- if(limb && !already_scarred && !replaced)
+ if(limb && can_scar && !already_scarred && !replaced)
already_scarred = TRUE
var/datum/scar/new_scar = new
new_scar.generate(limb, src)
- remove_wound_from_victim()
+
+ remove_actionspeed_modifier()
+
+ null_victim() // we use the proc here because some behaviors may depend on changing victim to some new value
+
if(limb && !ignore_limb)
- LAZYREMOVE(limb.wounds, src)
- limb.update_wounds(replaced)
+ set_limb(null, replaced) // since we're removing limb's ref to us, we should do the same
+ // if you want to keep the ref, do it externally, theres no reason for us to remember it
+
+ if (ismob(old_victim))
+ var/mob/mob_victim = old_victim
+ SEND_SIGNAL(mob_victim, COMSIG_CARBON_POST_LOSE_WOUND, src, old_limb, ignore_limb, replaced)
/datum/wound/proc/remove_wound_from_victim()
if(!victim)
@@ -211,17 +356,15 @@
/**
* replace_wound() is used when you want to replace the current wound with a new wound, presumably of the same category, just of a different severity (either up or down counts)
*
- * This proc actually instantiates the new wound based off the specific type path passed, then returns the new instantiated wound datum.
- *
* Arguments:
- * * new_type- The TYPE PATH of the wound you want to replace this, like /datum/wound/slash/severe
+ * * new_wound- The wound instance you want to replace this
* * smited- If this is a smite, we don't care about this wound for stat tracking purposes (not yet implemented)
*/
-/datum/wound/proc/replace_wound(new_type, smited = FALSE, attack_direction = attack_direction)
- var/datum/wound/new_wound = new new_type
+/datum/wound/proc/replace_wound(datum/wound/new_wound, smited = FALSE, attack_direction = attack_direction)
already_scarred = TRUE
+ var/obj/item/bodypart/cached_limb = limb // remove_wound() nulls limb so we have to track it locally
remove_wound(replaced=TRUE)
- new_wound.apply_wound(limb, old_wound = src, smited = smited, attack_direction = attack_direction)
+ new_wound.apply_wound(cached_limb, old_wound = src, smited = smited, attack_direction = attack_direction, wound_source = wound_source)
. = new_wound
qdel(src)
@@ -229,24 +372,6 @@
/datum/wound/proc/wound_injury(datum/wound/old_wound = null, attack_direction = null)
return
-
-/// Proc called to change the variable `limb` and react to the event.
-/datum/wound/proc/set_limb(new_value)
- if(limb == new_value)
- return FALSE //Limb can either be a reference to something or `null`. Returning the number variable makes it clear no change was made.
- . = limb
- if(limb)
- UnregisterSignal(limb, COMSIG_PARENT_QDELETING)
- limb = new_value
- RegisterSignal(new_value, COMSIG_PARENT_QDELETING, PROC_REF(source_died))
- if(. && disabling)
- var/obj/item/bodypart/old_limb = .
- old_limb.remove_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src))
- if(limb)
- if(disabling)
- limb.add_traits(list(TRAIT_PARALYSIS, TRAIT_DISABLED_BY_WOUND), REF(src))
-
-
/// Proc called to change the variable `disabling` and react to the event.
/datum/wound/proc/set_disabling(new_value)
if(disabling == new_value)
@@ -261,6 +386,60 @@
if(limb?.can_be_disabled)
limb.update_disabled()
+/// Setter for [interaction_efficiency_penalty]. Updates the actionspeed of our actionspeed mod.
+/datum/wound/proc/set_interaction_efficiency_penalty(new_value)
+ var/should_update = (new_value != interaction_efficiency_penalty)
+
+ interaction_efficiency_penalty = new_value
+
+ if (should_update)
+ update_actionspeed_modifier()
+
+/// Returns a "adjusted" interaction_efficiency_penalty that will be used for the actionspeed mod.
+/datum/wound/proc/get_effective_actionspeed_modifier()
+ return interaction_efficiency_penalty - 1
+
+/// Returns the decisecond multiplier of any click interactions, assuming our limb is being used.
+/datum/wound/proc/get_action_delay_mult()
+ SHOULD_BE_PURE(TRUE)
+
+ return interaction_efficiency_penalty
+
+/// Returns the decisecond increment of any click interactions, assuming our limb is being used.
+/datum/wound/proc/get_action_delay_increment()
+ SHOULD_BE_PURE(TRUE)
+
+ return 0
+
+/// Signal proc for if gauze has been applied or removed from our limb.
+/datum/wound/proc/gauze_state_changed()
+ SIGNAL_HANDLER
+
+ if (wound_flags & ACCEPTS_GAUZE)
+ update_inefficiencies()
+
+/// Updates our limping and interaction penalties in accordance with our gauze.
+/datum/wound/proc/update_inefficiencies()
+ if (wound_flags & ACCEPTS_GAUZE)
+ if(limb.body_zone in list(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
+ if(limb.current_gauze?.splint_factor)
+ limp_slowdown = initial(limp_slowdown) * limb.current_gauze.splint_factor
+ limp_chance = initial(limp_chance) * limb.current_gauze.splint_factor
+ else
+ limp_slowdown = initial(limp_slowdown)
+ limp_chance = initial(limp_chance)
+ else if(limb.body_zone in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ if(limb.current_gauze?.splint_factor)
+ set_interaction_efficiency_penalty(1 + ((get_effective_actionspeed_modifier()) * limb.current_gauze.splint_factor))
+ else
+ set_interaction_efficiency_penalty(initial(interaction_efficiency_penalty))
+
+ if(initial(disabling))
+ set_disabling(!limb.current_gauze)
+
+ limb.update_wounds()
+
+ start_limping_if_we_should()
/// Additional beneficial effects when the wound is gained, in case you want to give a temporary boost to allow the victim to try an escape or last stand
/datum/wound/proc/second_wind()
@@ -294,25 +473,7 @@
if(I.force && (tendee.istate & ISTATE_HARM))
return FALSE
- var/allowed = FALSE
-
- // check if we have a valid treatable tool
- if(I.tool_behaviour == treatable_tool)
- allowed = TRUE
- else if(treatable_tool == TOOL_CAUTERY && I.get_temperature() && user == victim) // allow improvised cauterization on yourself without an aggro grab
- allowed = TRUE
- // failing that, see if we're aggro grabbing them and if we have an item that works for aggro grabs only
- else if(user.pulling == victim && user.grab_state >= GRAB_AGGRESSIVE && check_grab_treatments(I, user))
- allowed = TRUE
- // failing THAT, we check if we have a generally allowed item
- else
- for(var/allowed_type in treatable_by)
- if(istype(I, allowed_type))
- allowed = TRUE
- break
-
- // if none of those apply, we return false to avoid interrupting
- if(!allowed)
+ if(!item_can_treat(I, user))
return FALSE
// now that we've determined we have a valid attempt at treating, we can stomp on their dreams if we're already interacting with the patient or if their part is obscured
@@ -328,8 +489,22 @@
return TRUE
// lastly, treat them
- treat(I, user)
- return TRUE
+ return treat(I, user) // we allow treat to return a value so it can control if the item does its normal interaction or not
+
+/// Returns TRUE if the item can be used to treat our wounds. Hooks into treat() - only things that return TRUE here may be used there.
+/datum/wound/proc/item_can_treat(obj/item/potential_treater, mob/user)
+ // check if we have a valid treatable tool
+ if(potential_treater.tool_behaviour in treatable_tools)
+ return TRUE
+ if(TOOL_CAUTERY in treatable_tools && potential_treater.get_temperature() && user == victim) // allow improvised cauterization on yourself without an aggro grab
+ return TRUE
+ // failing that, see if we're aggro grabbing them and if we have an item that works for aggro grabs only
+ if(user.pulling == victim && user.grab_state >= GRAB_AGGRESSIVE && check_grab_treatments(potential_treater, user))
+ return TRUE
+ // failing THAT, we check if we have a generally allowed item
+ for(var/allowed_type in treatable_by)
+ if(istype(potential_treater, allowed_type))
+ return TRUE
/// Return TRUE if we have an item that can only be used while aggro grabbed (unhanded aggro grab treatments go in [/datum/wound/proc/try_handling]). Treatment is still is handled in [/datum/wound/proc/treat]
/datum/wound/proc/check_grab_treatments(obj/item/I, mob/user)
@@ -351,16 +526,27 @@
/datum/wound/proc/still_exists()
return (!QDELETED(src) && limb)
-/// When our parent bodypart is hurt
-/datum/wound/proc/receive_damage(wounding_type, wounding_dmg, wound_bonus, attack_direction)
+/// When our parent bodypart is hurt.
+/datum/wound/proc/receive_damage(wounding_type, wounding_dmg, wound_bonus, attack_direction, damage_source)
return
/// Called from cryoxadone and pyroxadone when they're proc'ing. Wounds will slowly be fixed separately from other methods when these are in effect. crappy name but eh
/datum/wound/proc/on_xadone(power)
cryo_progress += power
- if(cryo_progress > 33 * severity)
+
+ return handle_xadone_progress()
+
+/// Does various actions based on [cryo_progress]. By default, qdeletes the wound past a certain threshold.
+/datum/wound/proc/handle_xadone_progress()
+ if(cryo_progress > get_xadone_progress_to_qdel())
qdel(src)
+/// Returns the amount of [cryo_progress] we need to be qdeleted.
+/datum/wound/proc/get_xadone_progress_to_qdel()
+ SHOULD_BE_PURE(TRUE)
+
+ return base_xadone_progress_to_qdel * severity
+
/// When synthflesh is applied to the victim, we call this. No sense in setting up an entire chem reaction system for wounds when we only care for a few chems. Probably will change in the future
/datum/wound/proc/on_synthflesh(power)
return
@@ -410,8 +596,50 @@
* * mob/user: The user examining the wound's owner, if that matters
*/
/datum/wound/proc/get_examine_description(mob/user)
- . = "[victim.p_their(TRUE)] [limb.plaintext_zone] [examine_desc]"
- . = severity <= WOUND_SEVERITY_MODERATE ? "[.]." : "[.]!"
+ . = get_wound_description(user)
+ if(HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ . += span_notice("\nThere is a holo-image next to the wound that seems to contain indications for treatment.")
+
+ return .
+
+/datum/wound/proc/get_wound_description(mob/user)
+ var/desc
+
+ if ((wound_flags & ACCEPTS_GAUZE) && limb.current_gauze)
+ var/sling_condition = get_gauze_condition()
+ desc = "[victim.p_Their()] [limb.plaintext_zone] is [sling_condition] fastened in a sling of [limb.current_gauze.name]"
+ else
+ desc = "[victim.p_Their()] [limb.plaintext_zone] [examine_desc]"
+
+ desc = modify_desc_before_span(desc, user)
+
+ return get_desc_intensity(desc)
+
+/// A hook proc used to modify desc before it is spanned via [get_desc_intensity]. Useful for inserting spans yourself.
+/datum/wound/proc/modify_desc_before_span(desc, mob/user)
+ return desc
+
+/datum/wound/proc/get_gauze_condition()
+ SHOULD_BE_PURE(TRUE)
+ if (!limb.current_gauze)
+ return null
+
+ switch(limb.current_gauze.absorption_capacity)
+ if(0 to 1.25)
+ return "just barely"
+ if(1.25 to 2.75)
+ return "loosely"
+ if(2.75 to 4)
+ return "mostly"
+ if(4 to INFINITY)
+ return "tightly"
+
+/// Spans [desc] based on our severity.
+/datum/wound/proc/get_desc_intensity(desc)
+ SHOULD_BE_PURE(TRUE)
+ if (severity > WOUND_SEVERITY_MODERATE)
+ return span_bold("[desc]!")
+ return "[desc]."
/datum/wound/proc/get_scanner_description(mob/user)
return "Type: [name]\nSeverity: [severity_text()]\nDescription: [desc]\nRecommended Treatment: [treat_text]"
@@ -426,3 +654,50 @@
return "Severe"
if(WOUND_SEVERITY_CRITICAL)
return "Critical"
+
+/// Returns TRUE if our limb is the head or chest, FALSE otherwise.
+/// Essential in the sense of "we cannot live without it".
+/datum/wound/proc/limb_essential()
+ return (limb.body_zone == BODY_ZONE_HEAD || limb.body_zone == BODY_ZONE_CHEST)
+
+/// Getter proc for our scar_keyword, in case we might have some custom scar gen logic.
+/datum/wound/proc/get_scar_keyword(obj/item/bodypart/scarred_limb, add_to_scars)
+ return scar_keyword
+
+/// Getter proc for our scar_file, in case we might have some custom scar gen logic.
+/datum/wound/proc/get_scar_file(obj/item/bodypart/scarred_limb, add_to_scars)
+ var/datum/wound_pregen_data/pregen_data = get_pregen_data()
+ // basically we iterate over biotypes until we find the one we want
+ // fleshy burns will look for flesh then bone
+ // dislocations will look for flesh, then bone, then metal
+ var/file = default_scar_file
+ for (var/biotype as anything in pregen_data.scar_priorities)
+ if (scarred_limb.biological_state & text2num(biotype))
+ file = GLOB.biotypes_to_scar_file[biotype]
+ break
+
+ return file
+
+/// Returns what string is displayed when a limb that has sustained this wound is examined
+/// (This is examining the LIMB ITSELF, when it's not attached to someone.)
+/datum/wound/proc/get_limb_examine_description()
+ return
+
+/// Gets the flat percentage chance increment of a dismember occuring, if a dismember is attempted (requires mangled flesh and bone). returning 15 = +15%.
+/datum/wound/proc/get_dismember_chance_bonus(existing_chance)
+ SHOULD_BE_PURE(TRUE)
+
+ var/datum/wound_pregen_data/pregen_data = get_pregen_data()
+
+ if (WOUND_BLUNT in pregen_data.required_wounding_types && severity >= WOUND_SEVERITY_CRITICAL)
+ return WOUND_CRITICAL_BLUNT_DISMEMBER_BONUS // we only require mangled bone (T2 blunt), but if there's a critical blunt, we'll add 15% more
+
+/// Returns our pregen data, which is practically guaranteed to exist, so this proc can safely be used raw.
+/// In fact, since it's RETURN_TYPEd to wound_pregen_data, you can even directly access the variables without having to store the value of this proc in a typed variable.
+/// Ex. get_pregen_data().wound_series
+/datum/wound/proc/get_pregen_data()
+ RETURN_TYPE(/datum/wound_pregen_data)
+
+ return GLOB.all_wound_pregen_data[type]
+
+#undef WOUND_CRITICAL_BLUNT_DISMEMBER_BONUS
diff --git a/code/datums/wounds/blunt.dm b/code/datums/wounds/blunt.dm
new file mode 100644
index 000000000000..219b7dd8805c
--- /dev/null
+++ b/code/datums/wounds/blunt.dm
@@ -0,0 +1,3 @@
+/datum/wound/blunt
+ name = "Blunt Wound"
+ sound_effect = 'sound/effects/wounds/crack1.ogg'
diff --git a/code/datums/wounds/bones.dm b/code/datums/wounds/bones.dm
index 9f68916f9021..f3d0ea3ac001 100644
--- a/code/datums/wounds/bones.dm
+++ b/code/datums/wounds/bones.dm
@@ -4,11 +4,19 @@
*/
// TODO: well, a lot really, but i'd kill to get overlays and a bonebreaking effect like Blitz: The League, similar to electric shock skeletons
-/datum/wound/blunt
+/datum/wound_pregen_data/bone
+ abstract = TRUE
+ required_limb_biostate = BIO_BONE
+
+ required_wounding_types = list(WOUND_BLUNT)
+
+ wound_series = WOUND_SERIES_BONE_BLUNT_BASIC
+
+/datum/wound/blunt/bone
name = "Blunt (Bone) Wound"
- sound_effect = 'sound/effects/wounds/crack1.ogg'
- wound_type = WOUND_BLUNT
- wound_flags = (BONE_WOUND | ACCEPTS_GAUZE)
+ wound_flags = (ACCEPTS_GAUZE)
+
+ default_scar_file = BONE_SCAR_FILE
/// Have we been bone gel'd?
var/gelled
@@ -32,16 +40,13 @@
/*
Overwriting of base procs
*/
-/datum/wound/blunt/wound_injury(datum/wound/old_wound = null, attack_direction = null)
+/datum/wound/blunt/bone/wound_injury(datum/wound/old_wound = null, attack_direction = null)
// hook into gaining/losing gauze so crit bone wounds can re-enable/disable depending if they're slung or not
- RegisterSignals(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED), PROC_REF(update_inefficiencies))
-
if(limb.body_zone == BODY_ZONE_HEAD && brain_trauma_group)
processes = TRUE
active_trauma = victim.gain_trauma_type(brain_trauma_group, TRAUMA_RESILIENCE_WOUND)
next_trauma_cycle = world.time + (rand(100-WOUND_BONE_HEAD_TIME_VARIANCE, 100+WOUND_BONE_HEAD_TIME_VARIANCE) * 0.01 * trauma_cycle_cooldown)
- RegisterSignal(victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(attack_with_hurt_hand))
if(limb.held_index && victim.get_item_for_held_index(limb.held_index) && (disabling || prob(30 * severity)))
var/obj/item/I = victim.get_item_for_held_index(limb.held_index)
if(istype(I, /obj/item/offhand))
@@ -51,19 +56,29 @@
victim.visible_message(span_danger("[victim] drops [I] in shock!"), span_warning("The force on your [limb.plaintext_zone] causes you to drop [I]!"), vision_distance=COMBAT_MESSAGE_RANGE)
update_inefficiencies()
+ return ..()
+
+/datum/wound/blunt/bone/set_victim(new_victim)
+
+ if (victim)
+ UnregisterSignal(victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
+ if (new_victim)
+ RegisterSignal(new_victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK, PROC_REF(attack_with_hurt_hand))
+
+ return ..()
-/datum/wound/blunt/remove_wound(ignore_limb, replaced)
+/datum/wound/blunt/bone/remove_wound(ignore_limb, replaced)
limp_slowdown = 0
limp_chance = 0
QDEL_NULL(active_trauma)
- if(limb)
- UnregisterSignal(limb, list(COMSIG_BODYPART_GAUZED, COMSIG_BODYPART_GAUZE_DESTROYED))
- if(victim)
- UnregisterSignal(victim, COMSIG_HUMAN_EARLY_UNARMED_ATTACK)
return ..()
-/datum/wound/blunt/handle_process(seconds_per_tick, times_fired)
+/datum/wound/blunt/bone/handle_process(seconds_per_tick, times_fired)
. = ..()
+
+ if (!victim || IS_IN_STASIS(victim))
+ return
+
if(limb.body_zone == BODY_ZONE_HEAD && brain_trauma_group && world.time > next_trauma_cycle)
if(active_trauma)
QDEL_NULL(active_trauma)
@@ -96,7 +111,7 @@
remove_wound()
/// If we're a human who's punching something with a broken arm, we might hurt ourselves doing so
-/datum/wound/blunt/proc/attack_with_hurt_hand(mob/M, atom/target, proximity)
+/datum/wound/blunt/bone/proc/attack_with_hurt_hand(mob/M, atom/target, proximity)
SIGNAL_HANDLER
if(victim.get_active_hand() != limb || !(victim.istate & ISTATE_HARM) || !ismob(target) || severity <= WOUND_SEVERITY_MODERATE)
@@ -117,7 +132,7 @@
return COMPONENT_CANCEL_ATTACK_CHAIN
-/datum/wound/blunt/receive_damage(wounding_type, wounding_dmg, wound_bonus)
+/datum/wound/blunt/bone/receive_damage(wounding_type, wounding_dmg, wound_bonus)
if(!victim || wounding_dmg < WOUND_MINIMUM_DAMAGE)
return
if(ishuman(victim))
@@ -143,99 +158,74 @@
new /obj/effect/temp_visual/dir_setting/bloodsplatter(victim.loc, victim.dir)
victim.add_splatter_floor(get_step(victim.loc, victim.dir))
+/datum/wound/blunt/bone/modify_desc_before_span(desc)
+ . = ..()
-/datum/wound/blunt/get_examine_description(mob/user)
- if(!limb.current_gauze && !gelled && !taped)
- return ..()
-
- var/list/msg = list()
- if(!limb.current_gauze)
- msg += "[victim.p_their(TRUE)] [limb.plaintext_zone] [examine_desc]"
- else
- var/sling_condition = ""
- // how much life we have left in these bandages
- switch(limb.current_gauze.absorption_capacity)
- if(0 to 1.25)
- sling_condition = "just barely"
- if(1.25 to 2.75)
- sling_condition = "loosely"
- if(2.75 to 4)
- sling_condition = "mostly"
- if(4 to INFINITY)
- sling_condition = "tightly"
-
- msg += "[victim.p_their(TRUE)] [limb.plaintext_zone] is [sling_condition] fastened in a sling of [limb.current_gauze.name]"
-
- if(taped)
- msg += ", [span_notice("and appears to be reforming itself under some surgical tape!")]"
- else if(gelled)
- msg += ", [span_notice("with fizzing flecks of blue bone gel sparking off the bone!")]"
- else
- msg += "!"
- return "[msg.Join()]"
+ if (!limb.current_gauze)
+ if(taped)
+ . += ", [span_notice("and appears to be reforming itself under some surgical tape!")]"
+ else if(gelled)
+ . += ", [span_notice("with fizzing flecks of blue bone gel sparking off the bone!")]"
/*
- New common procs for /datum/wound/blunt/
+ New common procs for /datum/wound/blunt/bone/
*/
-/datum/wound/blunt/proc/update_inefficiencies()
- SIGNAL_HANDLER
-
- if(limb.body_zone in list(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG))
- if(limb.current_gauze?.splint_factor)
- limp_slowdown = initial(limp_slowdown) * limb.current_gauze.splint_factor
- limp_chance = initial(limp_chance) * limb.current_gauze.splint_factor
- else
- limp_slowdown = initial(limp_slowdown)
- limp_chance = initial(limp_chance)
- victim.apply_status_effect(/datum/status_effect/limp)
- else if(limb.body_zone in list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
- if(limb.current_gauze?.splint_factor)
- interaction_efficiency_penalty = 1 + ((interaction_efficiency_penalty - 1) * limb.current_gauze.splint_factor)
- else
- interaction_efficiency_penalty = initial(interaction_efficiency_penalty)
+/datum/wound/blunt/bone/get_scar_file(obj/item/bodypart/scarred_limb, add_to_scars)
+ if (scarred_limb.biological_state & BIO_BONE && (!(scarred_limb.biological_state & BIO_FLESH))) // only bone
+ return BONE_SCAR_FILE
+ else if (scarred_limb.biological_state & BIO_FLESH && (!(scarred_limb.biological_state & BIO_BONE)))
+ return FLESH_SCAR_FILE
- if(initial(disabling))
- set_disabling(!limb.current_gauze)
-
- limb.update_wounds()
+ return ..()
/// Joint Dislocation (Moderate Blunt)
-/datum/wound/blunt/moderate
+/datum/wound/blunt/bone/moderate
name = "Joint Dislocation"
- desc = "Patient's bone has been unset from socket, causing pain and reduced motor function."
+ desc = "Patient's limb has been unset from socket, causing pain and reduced motor function."
treat_text = "Recommended application of bonesetter to affected limb, though manual relocation by applying an aggressive grab to the patient and helpfully interacting with afflicted limb may suffice."
examine_desc = "is awkwardly janked out of place"
occur_text = "janks violently and becomes unseated"
severity = WOUND_SEVERITY_MODERATE
- viable_zones = list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
interaction_efficiency_penalty = 1.3
limp_slowdown = 3
limp_chance = 50
- threshold_minimum = 35
threshold_penalty = 15
- treatable_tool = TOOL_BONESET
- wound_flags = (BONE_WOUND)
- status_effect_type = /datum/status_effect/wound/blunt/moderate
- scar_keyword = "bluntmoderate"
+ treatable_tools = list(TOOL_BONESET)
+ status_effect_type = /datum/status_effect/wound/blunt/bone/moderate
+ scar_keyword = "dislocate"
+
+/datum/wound_pregen_data/bone/dislocate
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/blunt/bone/moderate
+
+ required_limb_biostate = BIO_JOINTED
-/datum/wound/blunt/moderate/Destroy()
+ threshold_minimum = 35
+
+/datum/wound/blunt/bone/moderate/Destroy()
if(victim)
UnregisterSignal(victim, COMSIG_LIVING_DOORCRUSHED)
return ..()
-/datum/wound/blunt/moderate/wound_injury(datum/wound/old_wound, attack_direction = null)
- . = ..()
- RegisterSignal(victim, COMSIG_LIVING_DOORCRUSHED, PROC_REF(door_crush))
+/datum/wound/blunt/bone/moderate/set_victim(new_victim)
+
+ if (victim)
+ UnregisterSignal(victim, COMSIG_LIVING_DOORCRUSHED)
+ if (new_victim)
+ RegisterSignal(new_victim, COMSIG_LIVING_DOORCRUSHED, PROC_REF(door_crush))
+
+ return ..()
/// Getting smushed in an airlock/firelock is a last-ditch attempt to try relocating your limb
-/datum/wound/blunt/moderate/proc/door_crush()
+/datum/wound/blunt/bone/moderate/proc/door_crush()
SIGNAL_HANDLER
if(prob(40))
victim.visible_message(span_danger("[victim]'s dislocated [limb.plaintext_zone] pops back into place!"), span_userdanger("Your dislocated [limb.plaintext_zone] pops back into place! Ow!"))
remove_wound()
-/datum/wound/blunt/moderate/try_handling(mob/living/carbon/human/user)
+/datum/wound/blunt/bone/moderate/try_handling(mob/living/carbon/human/user)
if(user.pulling != victim || user.zone_selected != limb.body_zone)
return FALSE
@@ -253,7 +243,7 @@
return TRUE
/// If someone is snapping our dislocated joint back into place by hand with an aggro grab and help intent
-/datum/wound/blunt/moderate/proc/chiropractice(mob/living/carbon/human/user)
+/datum/wound/blunt/bone/moderate/proc/chiropractice(mob/living/carbon/human/user)
var/time = base_treat_time
if(!do_after(user, time, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
@@ -272,7 +262,7 @@
chiropractice(user)
/// If someone is snapping our dislocated joint into a fracture by hand with an aggro grab and harm or disarm intent
-/datum/wound/blunt/moderate/proc/malpractice(mob/living/carbon/human/user)
+/datum/wound/blunt/bone/moderate/proc/malpractice(mob/living/carbon/human/user)
var/time = base_treat_time
if(!do_after(user, time, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
@@ -290,13 +280,18 @@
malpractice(user)
-/datum/wound/blunt/moderate/treat(obj/item/I, mob/user)
+/datum/wound/blunt/bone/moderate/treat(obj/item/I, mob/user)
+ var/scanned = HAS_TRAIT(src, TRAIT_WOUND_SCANNED)
+ var/self_penalty_mult = user == victim ? 1.5 : 1
+ var/scanned_mult = scanned ? 0.5 : 1
+ var/treatment_delay = base_treat_time * self_penalty_mult * scanned_mult
+
if(victim == user)
- victim.visible_message(span_danger("[user] begins resetting [victim.p_their()] [limb.plaintext_zone] with [I]."), span_warning("You begin resetting your [limb.plaintext_zone] with [I]..."))
+ victim.visible_message(span_danger("[user] begins [scanned ? "expertly" : ""] resetting [victim.p_their()] [limb.plaintext_zone] with [I]."), span_warning("You begin resetting your [limb.plaintext_zone] with [I][scanned ? ", keeping the holo-image's indications in mind" : ""]..."))
else
- user.visible_message(span_danger("[user] begins resetting [victim]'s [limb.plaintext_zone] with [I]."), span_notice("You begin resetting [victim]'s [limb.plaintext_zone] with [I]..."))
+ user.visible_message(span_danger("[user] begins [scanned ? "expertly" : ""] resetting [victim]'s [limb.plaintext_zone] with [I]."), span_notice("You begin resetting [victim]'s [limb.plaintext_zone] with [I][scanned ? ", keeping the holo-image's indications in mind" : ""]..."))
- if(!do_after(user, base_treat_time * (user == victim ? 1.5 : 1), target = victim, extra_checks=CALLBACK(src, PROC_REF(still_exists))))
+ if(!do_after(user, treatment_delay, target = victim, extra_checks=CALLBACK(src, PROC_REF(still_exists))))
return
if(victim == user)
@@ -314,7 +309,7 @@
Severe (Hairline Fracture)
*/
-/datum/wound/blunt/severe
+/datum/wound/blunt/bone/severe
name = "Hairline Fracture"
desc = "Patient's bone has suffered a crack in the foundation, causing serious pain and reduced limb functionality."
treat_text = "Recommended light surgical application of bone gel, though a sling of medical gauze will prevent worsening situation."
@@ -325,19 +320,25 @@
interaction_efficiency_penalty = 2
limp_slowdown = 6
limp_chance = 60
- threshold_minimum = 60
threshold_penalty = 30
treatable_by = list(/obj/item/stack/sticky_tape/surgical, /obj/item/stack/medical/bone_gel)
- status_effect_type = /datum/status_effect/wound/blunt/severe
+ status_effect_type = /datum/status_effect/wound/blunt/bone/severe
scar_keyword = "bluntsevere"
brain_trauma_group = BRAIN_TRAUMA_MILD
trauma_cycle_cooldown = 1.5 MINUTES
internal_bleeding_chance = 40
- wound_flags = (BONE_WOUND | ACCEPTS_GAUZE | MANGLES_BONE)
+ wound_flags = (ACCEPTS_GAUZE | MANGLES_INTERIOR)
regen_ticks_needed = 120 // ticks every 2 seconds, 240 seconds, so roughly 4 minutes default
+/datum/wound_pregen_data/bone/hairline
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/blunt/bone/severe
+
+ threshold_minimum = 60
+
/// Compound Fracture (Critical Blunt)
-/datum/wound/blunt/critical
+/datum/wound/blunt/bone/critical
name = "Compound Fracture"
desc = "Patient's bones have suffered multiple gruesome fractures, causing significant pain and near uselessness of limb."
treat_text = "Immediate binding of affected limb, followed by surgical intervention ASAP."
@@ -349,40 +350,45 @@
limp_slowdown = 7
limp_chance = 70
sound_effect = 'sound/effects/wounds/crack2.ogg'
- threshold_minimum = 115
threshold_penalty = 50
disabling = TRUE
treatable_by = list(/obj/item/stack/sticky_tape/surgical, /obj/item/stack/medical/bone_gel)
- status_effect_type = /datum/status_effect/wound/blunt/critical
+ status_effect_type = /datum/status_effect/wound/blunt/bone/critical
scar_keyword = "bluntcritical"
brain_trauma_group = BRAIN_TRAUMA_SEVERE
trauma_cycle_cooldown = 2.5 MINUTES
internal_bleeding_chance = 60
- wound_flags = (BONE_WOUND | ACCEPTS_GAUZE | MANGLES_BONE)
+ wound_flags = (ACCEPTS_GAUZE | MANGLES_INTERIOR)
regen_ticks_needed = 240 // ticks every 2 seconds, 480 seconds, so roughly 8 minutes default
+/datum/wound_pregen_data/bone/compound
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/blunt/bone/critical
+
+ threshold_minimum = 115
+
// doesn't make much sense for "a" bone to stick out of your head
-/datum/wound/blunt/critical/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null)
+/datum/wound/blunt/bone/critical/apply_wound(obj/item/bodypart/L, silent = FALSE, datum/wound/old_wound = null, smited = FALSE, attack_direction = null, wound_source = "Unknown")
if(L.body_zone == BODY_ZONE_HEAD)
occur_text = "splits open, exposing a bare, cracked skull through the flesh and blood"
examine_desc = "has an unsettling indent, with bits of skull poking out"
. = ..()
/// if someone is using bone gel on our wound
-/datum/wound/blunt/proc/gel(obj/item/stack/medical/bone_gel/I, mob/user)
+/datum/wound/blunt/bone/proc/gel(obj/item/stack/medical/bone_gel/I, mob/user)
// skellies get treated nicer with bone gel since their "reattach dismembered limbs by hand" ability sucks when it's still critically wounded
if((limb.biological_state & BIO_BONE) && !(limb.biological_state & BIO_FLESH))
- skelly_gel(I, user)
- return
+ return skelly_gel(I, user)
if(gelled)
to_chat(user, span_warning("[user == victim ? "Your" : "[victim]'s"] [limb.plaintext_zone] is already coated with bone gel!"))
- return
+ return TRUE
user.visible_message(span_danger("[user] begins hastily applying [I] to [victim]'s' [limb.plaintext_zone]..."), span_warning("You begin hastily applying [I] to [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone], disregarding the warning label..."))
if(!do_after(user, base_treat_time * 1.5 * (user == victim ? 1.5 : 1), target = victim, extra_checks=CALLBACK(src, PROC_REF(still_exists))))
- return
+ return TRUE
I.use(1)
victim.emote("scream")
@@ -405,15 +411,16 @@
if(prob(25 + (20 * (severity - 2)) - painkiller_bonus)) // 25%/45% chance to fail self-applying with severe and critical wounds, modded by painkillers
victim.visible_message(span_danger("[victim] fails to finish applying [I] to [victim.p_their()] [limb.plaintext_zone], passing out from the pain!"), span_notice("You pass out from the pain of applying [I] to your [limb.plaintext_zone] before you can finish!"))
victim.AdjustUnconscious(5 SECONDS)
- return
+ return TRUE
victim.visible_message(span_notice("[victim] finishes applying [I] to [victim.p_their()] [limb.plaintext_zone], grimacing from the pain!"), span_notice("You finish applying [I] to your [limb.plaintext_zone], and your bones explode in pain!"))
limb.receive_damage(25, wound_bonus=CANT_WOUND)
victim.stamina.adjust(-100)
gelled = TRUE
+ return TRUE
/// skellies are less averse to bone gel, since they're literally all bone
-/datum/wound/blunt/proc/skelly_gel(obj/item/stack/medical/bone_gel/I, mob/user)
+/datum/wound/blunt/bone/proc/skelly_gel(obj/item/stack/medical/bone_gel/I, mob/user)
if(gelled)
to_chat(user, span_warning("[user == victim ? "Your" : "[victim]'s"] [limb.plaintext_zone] is already coated with bone gel!"))
return
@@ -432,20 +439,21 @@
gelled = TRUE
processes = TRUE
+ return TRUE
/// if someone is using surgical tape on our wound
-/datum/wound/blunt/proc/tape(obj/item/stack/sticky_tape/surgical/I, mob/user)
+/datum/wound/blunt/bone/proc/tape(obj/item/stack/sticky_tape/surgical/I, mob/user)
if(!gelled)
to_chat(user, span_warning("[user == victim ? "Your" : "[victim]'s"] [limb.plaintext_zone] must be coated with bone gel to perform this emergency operation!"))
- return
+ return TRUE
if(taped)
to_chat(user, span_warning("[user == victim ? "Your" : "[victim]'s"] [limb.plaintext_zone] is already wrapped in [I.name] and reforming!"))
- return
+ return TRUE
user.visible_message(span_danger("[user] begins applying [I] to [victim]'s' [limb.plaintext_zone]..."), span_warning("You begin applying [I] to [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone]..."))
if(!do_after(user, base_treat_time * (user == victim ? 1.5 : 1), target = victim, extra_checks=CALLBACK(src, PROC_REF(still_exists))))
- return
+ return TRUE
if(victim == user)
regen_ticks_needed *= 1.5
@@ -459,14 +467,15 @@
taped = TRUE
processes = TRUE
+ return TRUE
-/datum/wound/blunt/treat(obj/item/I, mob/user)
+/datum/wound/blunt/bone/treat(obj/item/I, mob/user)
if(istype(I, /obj/item/stack/medical/bone_gel))
- gel(I, user)
+ return gel(I, user)
else if(istype(I, /obj/item/stack/sticky_tape/surgical))
- tape(I, user)
+ return tape(I, user)
-/datum/wound/blunt/get_scanner_description(mob/user)
+/datum/wound/blunt/bone/get_scanner_description(mob/user)
. = ..()
. += "
"
diff --git a/code/datums/wounds/burns.dm b/code/datums/wounds/burns.dm
index 21b5e0040e8a..37f4072ffbf9 100644
--- a/code/datums/wounds/burns.dm
+++ b/code/datums/wounds/burns.dm
@@ -7,20 +7,24 @@
/datum/wound/burn
name = "Burn Wound"
a_or_from = "from"
- wound_type = WOUND_BURN
- processes = TRUE
sound_effect = 'sound/effects/wounds/sizzle1.ogg'
- wound_flags = (FLESH_WOUND | ACCEPTS_GAUZE)
+
+/datum/wound/burn/flesh
+ name = "Burn (Flesh) Wound"
+ a_or_from = "from"
+ processes = TRUE
+
+ default_scar_file = FLESH_SCAR_FILE
treatable_by = list(/obj/item/stack/medical/ointment, /obj/item/stack/medical/mesh) // sterilizer and alcohol will require reagent treatments, coming soon
- // Flesh damage vars
+ // Flesh damage vars
/// How much damage to our flesh we currently have. Once both this and infestation reach 0, the wound is considered healed
var/flesh_damage = 5
/// Our current counter for how much flesh regeneration we have stacked from regenerative mesh/synthflesh/whatever, decrements each tick and lowers flesh_damage
var/flesh_healing = 0
- // Infestation vars (only for severe and critical)
+ // Infestation vars (only for severe and critical)
/// How quickly infection breeds on this burn if we don't have disinfectant
var/infestation_rate = 0
/// Our current level of infection
@@ -31,8 +35,11 @@
/// Once we reach infestation beyond WOUND_INFESTATION_SEPSIS, we get this many warnings before the limb is completely paralyzed (you'd have to ignore a really bad burn for a really long time for this to happen)
var/strikes_to_lose_limb = 3
+/datum/wound/burn/flesh/handle_process(seconds_per_tick, times_fired)
+
+ if (!victim || IS_IN_STASIS(victim))
+ return
-/datum/wound/burn/handle_process(seconds_per_tick, times_fired)
. = ..()
if(strikes_to_lose_limb == 0) // we've already hit sepsis, nothing more to do
victim.adjustToxLoss(0.25 * seconds_per_tick)
@@ -128,7 +135,7 @@
var/datum/brain_trauma/severe/paralysis/sepsis = new (limb.body_zone)
victim.gain_trauma(sepsis)
-/datum/wound/burn/get_examine_description(mob/user)
+/datum/wound/burn/flesh/get_wound_description(mob/user)
if(strikes_to_lose_limb <= 0)
return span_deadsay("[victim.p_their(TRUE)] [limb.plaintext_zone] has locked up completely and is non-functional.")
@@ -161,8 +168,8 @@
return "[condition.Join()]"
-/datum/wound/burn/get_scanner_description(mob/user)
- if(strikes_to_lose_limb == 0)
+/datum/wound/burn/flesh/get_scanner_description(mob/user)
+ if(strikes_to_lose_limb <= 0) // Unclear if it can go below 0, best to not take the chance
var/oopsie = "Type: [name]\nSeverity: [severity_text()]"
oopsie += "
Infection Level: [span_deadsay("The body part has suffered complete sepsis and must be removed. Amputate or augment limb immediately.")]
"
return oopsie
@@ -194,12 +201,12 @@
*/
/// if someone is using ointment or mesh on our burns
-/datum/wound/burn/proc/ointmentmesh(obj/item/stack/medical/I, mob/user)
+/datum/wound/burn/flesh/proc/ointmentmesh(obj/item/stack/medical/I, mob/user)
user.visible_message(span_notice("[user] begins applying [I] to [victim]'s [limb.plaintext_zone]..."), span_notice("You begin applying [I] to [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone]..."))
if (I.amount <= 0)
- return
+ return TRUE
if(!do_after(user, (user == victim ? I.self_delay : I.other_delay), extra_checks = CALLBACK(src, PROC_REF(still_exists))))
- return
+ return TRUE
limb.heal_damage(I.heal_brute, I.heal_burn)
user.visible_message(span_green("[user] applies [I] to [victim]."), span_green("You apply [I] to [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone]."))
@@ -209,36 +216,38 @@
if((infestation <= 0 || sanitization >= infestation) && (flesh_damage <= 0 || flesh_healing > flesh_damage))
to_chat(user, span_notice("You've done all you can with [I], now you must wait for the flesh on [victim]'s [limb.plaintext_zone] to recover."))
+ return TRUE
else
- try_treating(I, user)
+ return try_treating(I, user)
/// Paramedic UV penlights
-/datum/wound/burn/proc/uv(obj/item/flashlight/pen/paramedic/I, mob/user)
+/datum/wound/burn/flesh/proc/uv(obj/item/flashlight/pen/paramedic/I, mob/user)
if(!COOLDOWN_FINISHED(I, uv_cooldown))
to_chat(user, span_notice("[I] is still recharging!"))
- return
+ return TRUE
if(infestation <= 0 || infestation < sanitization)
to_chat(user, span_notice("There's no infection to treat on [victim]'s [limb.plaintext_zone]!"))
- return
+ return TRUE
user.visible_message(span_notice("[user] flashes the burns on [victim]'s [limb] with [I]."), span_notice("You flash the burns on [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]."), vision_distance=COMBAT_MESSAGE_RANGE)
sanitization += I.uv_power
COOLDOWN_START(I, uv_cooldown, I.uv_cooldown_length)
+ return TRUE
-/datum/wound/burn/treat(obj/item/I, mob/user)
+/datum/wound/burn/flesh/treat(obj/item/I, mob/user)
if(istype(I, /obj/item/stack/medical/ointment))
- ointmentmesh(I, user)
+ return ointmentmesh(I, user)
else if(istype(I, /obj/item/stack/medical/mesh))
var/obj/item/stack/medical/mesh/mesh_check = I
if(!mesh_check.is_open)
to_chat(user, span_warning("You need to open [mesh_check] first."))
return
- ointmentmesh(mesh_check, user)
+ return ointmentmesh(mesh_check, user)
else if(istype(I, /obj/item/flashlight/pen/paramedic))
- uv(I, user)
+ return uv(I, user)
// people complained about burns not healing on stasis beds, so in addition to checking if it's cured, they also get the special ability to very slowly heal on stasis beds if they have the healing effects stored
-/datum/wound/burn/on_stasis(seconds_per_tick, times_fired)
+/datum/wound/burn/flesh/on_stasis(seconds_per_tick, times_fired)
. = ..()
if(strikes_to_lose_limb == 0) // we've already hit sepsis, nothing more to do
if(SPT_PROB(0.5, seconds_per_tick))
@@ -253,60 +262,108 @@
if(sanitization > 0)
infestation = max(infestation - (0.1 * WOUND_BURN_SANITIZATION_RATE * seconds_per_tick), 0)
-/datum/wound/burn/on_synthflesh(amount)
+/datum/wound/burn/flesh/on_synthflesh(amount)
flesh_healing += amount * 0.5 // 20u patch will heal 10 flesh standard
+/datum/wound_pregen_data/flesh_burn
+ abstract = TRUE
+
+ required_wounding_types = list(WOUND_BURN)
+ required_limb_biostate = BIO_FLESH
+
+ wound_series = WOUND_SERIES_FLESH_BURN_BASIC
+
+/datum/wound/burn/get_limb_examine_description()
+ return span_warning("The flesh on this limb appears badly cooked.")
+
// we don't even care about first degree burns, straight to second
-/datum/wound/burn/moderate
+/datum/wound/burn/flesh/moderate
name = "Second Degree Burns"
desc = "Patient is suffering considerable burns with mild skin penetration, weakening limb integrity and increased burning sensations."
treat_text = "Recommended application of topical ointment or regenerative mesh to affected region."
examine_desc = "is badly burned and breaking out in blisters"
occur_text = "breaks out with violent red burns"
severity = WOUND_SEVERITY_MODERATE
- damage_mulitplier_penalty = 1.1
- threshold_minimum = 40
threshold_penalty = 30 // burns cause significant decrease in limb integrity compared to other wounds
- status_effect_type = /datum/status_effect/wound/burn/moderate
+ status_effect_type = /datum/status_effect/wound/burn/flesh/moderate
flesh_damage = 5
scar_keyword = "burnmoderate"
-/datum/wound/burn/severe
+/datum/wound_pregen_data/flesh_burn/second_degree
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/flesh/moderate
+
+ threshold_minimum = 40
+
+/datum/wound/burn/flesh/severe
name = "Third Degree Burns"
desc = "Patient is suffering extreme burns with full skin penetration, creating serious risk of infection and greatly reduced limb integrity."
treat_text = "Recommended immediate disinfection and excision of any infected skin, followed by bandaging and ointment."
examine_desc = "appears seriously charred, with aggressive red splotches"
occur_text = "chars rapidly, exposing ruined tissue and spreading angry red burns"
severity = WOUND_SEVERITY_SEVERE
- damage_mulitplier_penalty = 1.2
- threshold_minimum = 80
threshold_penalty = 40
- status_effect_type = /datum/status_effect/wound/burn/severe
+ status_effect_type = /datum/status_effect/wound/burn/flesh/severe
treatable_by = list(/obj/item/flashlight/pen/paramedic, /obj/item/stack/medical/ointment, /obj/item/stack/medical/mesh)
infestation_rate = 0.07 // appx 9 minutes to reach sepsis without any treatment
flesh_damage = 12.5
scar_keyword = "burnsevere"
-/datum/wound/burn/critical
+/datum/wound_pregen_data/flesh_burn/third_degree
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/flesh/severe
+
+ threshold_minimum = 80
+
+/datum/wound/burn/flesh/critical
name = "Catastrophic Burns"
desc = "Patient is suffering near complete loss of tissue and significantly charred muscle and bone, creating life-threatening risk of infection and negligible limb integrity."
treat_text = "Immediate surgical debriding of any infected skin, followed by potent tissue regeneration formula and bandaging."
examine_desc = "is a ruined mess of blanched bone, melted fat, and charred tissue"
occur_text = "vaporizes as flesh, bone, and fat melt together in a horrifying mess"
severity = WOUND_SEVERITY_CRITICAL
- damage_mulitplier_penalty = 1.3
sound_effect = 'sound/effects/wounds/sizzle2.ogg'
- threshold_minimum = 140
threshold_penalty = 80
- status_effect_type = /datum/status_effect/wound/burn/critical
+ status_effect_type = /datum/status_effect/wound/burn/flesh/critical
treatable_by = list(/obj/item/flashlight/pen/paramedic, /obj/item/stack/medical/ointment, /obj/item/stack/medical/mesh)
infestation_rate = 0.075 // appx 4.33 minutes to reach sepsis without any treatment
flesh_damage = 20
scar_keyword = "burncritical"
+/datum/wound_pregen_data/flesh_burn/fourth_degree
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/flesh/critical
+
+ threshold_minimum = 140
+
///special severe wound caused by sparring interference or other god related punishments.
-/datum/wound/burn/severe/brand
+/datum/wound/burn/flesh/severe/brand
name = "Holy Brand"
desc = "Patient is suffering extreme burns from a strange brand marking, creating serious risk of infection and greatly reduced limb integrity."
examine_desc = "appears to have holy symbols painfully branded into their flesh, leaving severe burns."
occur_text = "chars rapidly into a strange pattern of holy symbols, burned into the flesh."
+
+/datum/wound_pregen_data/flesh_burn/third_degree/holy
+ abstract = FALSE
+ can_be_randomly_generated = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/flesh/severe/brand
+/// special severe wound caused by the cursed slot machine.
+
+/datum/wound/burn/flesh/severe/cursed_brand
+ name = "Ancient Brand"
+ desc = "Patient is suffering extreme burns with oddly ornate brand markings, creating serious risk of infection and greatly reduced limb integrity."
+ examine_desc = "appears to have ornate symbols painfully branded into their flesh, leaving severe burns"
+ occur_text = "chars rapidly into a pattern that can only be described as an agglomeration of several financial symbols, burned into the flesh"
+
+/datum/wound/burn/flesh/severe/cursed_brand/get_limb_examine_description()
+ return span_warning("The flesh on this limb has several ornate symbols burned into it, with pitting throughout.")
+
+/datum/wound_pregen_data/flesh_burn/third_degree/cursed_brand
+ abstract = FALSE
+ can_be_randomly_generated = FALSE
+
+ wound_path_to_generate = /datum/wound/burn/flesh/severe/cursed_brand
diff --git a/code/datums/wounds/loss.dm b/code/datums/wounds/loss.dm
index b1853840ced1..479656a8a429 100644
--- a/code/datums/wounds/loss.dm
+++ b/code/datums/wounds/loss.dm
@@ -1,3 +1,15 @@
+/datum/wound_pregen_data/loss
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/loss
+ required_limb_biostate = NONE
+ require_any_biostate = TRUE
+
+ required_wounding_types = list(WOUND_ALL)
+
+ wound_series = WOUND_SERIES_LOSS_BASIC
+
+ threshold_minimum = WOUND_DISMEMBER_OUTRIGHT_THRESH // not actually used since dismembering is handled differently, but may as well assign it since we got it
/datum/wound/loss
name = "Dismemberment Wound"
@@ -5,15 +17,17 @@
sound_effect = 'sound/effects/dismember.ogg'
severity = WOUND_SEVERITY_LOSS
- threshold_minimum = WOUND_DISMEMBER_OUTRIGHT_THRESH // not actually used since dismembering is handled differently, but may as well assign it since we got it
status_effect_type = null
scar_keyword = "dismember"
wound_flags = null
already_scarred = TRUE // We manually assign scars for dismembers through endround missing limbs and aheals
+ /// The wounding_type of the attack that caused us. Used to generate the description of our scar. Currently unused, but primarily exists in case non-biological wounds are added.
+ var/loss_wounding_type
+
/// Our special proc for our special dismembering, the wounding type only matters for what text we have
-/datum/wound/loss/proc/apply_dismember(obj/item/bodypart/dismembered_part, wounding_type=WOUND_SLASH, outright = FALSE, attack_direction)
- if(!istype(dismembered_part) || !dismembered_part.owner || !(dismembered_part.body_zone in viable_zones) || isalien(dismembered_part.owner) || !dismembered_part.can_dismember())
+/datum/wound/loss/proc/apply_dismember(obj/item/bodypart/dismembered_part, wounding_type = WOUND_SLASH, outright = FALSE, attack_direction)
+ if(!istype(dismembered_part) || !dismembered_part.owner || !(dismembered_part.body_zone in get_viable_zones()) || isalien(dismembered_part.owner) || !dismembered_part.can_dismember())
qdel(src)
return
@@ -22,10 +36,31 @@
if(dismembered_part.body_zone == BODY_ZONE_CHEST)
occur_text = "is split open, causing [victim.p_their()] internal [dismembered_part.bodytype & BODYTYPE_ROBOTIC? "components": "organs"] to spill out!"
self_msg = "is split open, causing your internal organs to spill out!"
- else if(outright)
+ else
+ occur_text = dismembered_part.get_dismember_message(wounding_type, outright)
+
+ var/msg = span_bolddanger("[victim]'s [dismembered_part.plaintext_zone] [occur_text]")
+
+ victim.visible_message(msg, span_userdanger("Your [dismembered_part.plaintext_zone] [self_msg ? self_msg : occur_text]"))
+
+ loss_wounding_type = wounding_type
+
+ set_limb(dismembered_part)
+ second_wind()
+ log_wound(victim, src)
+ if(dismembered_part.can_bleed() && wounding_type != WOUND_BURN && victim.blood_volume)
+ victim.spray_blood(attack_direction, severity)
+ dismembered_part.dismember(wounding_type == WOUND_BURN ? BURN : BRUTE, wounding_type = wounding_type)
+ qdel(src)
+ return TRUE
+
+/obj/item/bodypart/proc/get_dismember_message(wounding_type, outright)
+ var/occur_text
+
+ if(outright)
switch(wounding_type)
if(WOUND_BLUNT)
- occur_text = "is outright smashed to a [dismembered_part.bodytype & BODYTYPE_ROBOTIC? "misshapen heap of scrap": "gross pulp"], severing it completely!"
+ occur_text = "is outright smashed to a gross pulp, severing it completely!"
if(WOUND_SLASH)
occur_text = "is outright slashed off, severing it completely!"
if(WOUND_PIERCE)
@@ -33,25 +68,17 @@
if(WOUND_BURN)
occur_text = "is outright incinerated, falling to dust!"
else
+ var/bone_text = get_internal_description()
+ var/tissue_text = get_external_description()
+
switch(wounding_type)
if(WOUND_BLUNT)
- occur_text = "is shattered through the last [dismembered_part.bodytype & BODYTYPE_ROBOTIC? "bit of wires": "bone"] holding it together, severing it completely!"
+ occur_text = "is shattered through the last [bone_text] holding it together, severing it completely!"
if(WOUND_SLASH)
- occur_text = "is slashed through the last [dismembered_part.bodytype & BODYTYPE_ROBOTIC? "bit of wires": "tissue"] holding it together, severing it completely!"
+ occur_text = "is slashed through the last [tissue_text] holding it together, severing it completely!"
if(WOUND_PIERCE)
- occur_text = "is pierced through the last [dismembered_part.bodytype & BODYTYPE_ROBOTIC? "bit of wires": "tissue"] holding it together, severing it completely!"
+ occur_text = "is pierced through the last [tissue_text] holding it together, severing it completely!"
if(WOUND_BURN)
occur_text = "is completely incinerated, falling to dust!"
- var/msg = span_bolddanger("[victim]'s [dismembered_part.plaintext_zone] [occur_text]")
-
- victim.visible_message(msg, span_userdanger("Your [dismembered_part.plaintext_zone] [self_msg ? self_msg : occur_text]"))
-
- set_limb(dismembered_part)
- second_wind()
- log_wound(victim, src)
- if(wounding_type != WOUND_BURN && victim.blood_volume)
- victim.spray_blood(attack_direction, severity)
- dismembered_part.dismember(wounding_type == WOUND_BURN ? BURN : BRUTE)
- qdel(src)
- return TRUE
+ return occur_text
diff --git a/code/datums/wounds/pierce.dm b/code/datums/wounds/pierce.dm
index 1612b3159a6f..b52926dec16d 100644
--- a/code/datums/wounds/pierce.dm
+++ b/code/datums/wounds/pierce.dm
@@ -2,16 +2,19 @@
/*
Piercing wounds
*/
-
/datum/wound/pierce
+
+/datum/wound/pierce/bleed
name = "Piercing Wound"
sound_effect = 'sound/weapons/slice.ogg'
processes = TRUE
- wound_type = WOUND_PIERCE
treatable_by = list(/obj/item/stack/medical/suture)
- treatable_tool = TOOL_CAUTERY
+ treatable_tools = list(TOOL_CAUTERY)
base_treat_time = 3 SECONDS
- wound_flags = (FLESH_WOUND | ACCEPTS_GAUZE)
+ wound_flags = (ACCEPTS_GAUZE | CAN_BE_GRASPED)
+
+ default_scar_file = FLESH_SCAR_FILE
+
/// How much blood we start losing when this wound is first applied
var/initial_flow
@@ -23,13 +26,15 @@
/// If we let off blood when hit, the max blood lost is this * the incoming damage
var/internal_bleeding_coefficient
-/datum/wound/pierce/wound_injury(datum/wound/old_wound = null, attack_direction = null)
+/datum/wound/pierce/bleed/wound_injury(datum/wound/old_wound = null, attack_direction = null)
set_blood_flow(initial_flow)
- if(!no_bleeding && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY)
+ if(limb.can_bleed() && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY)
victim.spray_blood(attack_direction, severity)
-/datum/wound/pierce/receive_damage(wounding_type, wounding_dmg, wound_bonus)
- if(victim.stat == DEAD || (wounding_dmg < 5) || no_bleeding || !victim.blood_volume || !prob(internal_bleeding_chance + wounding_dmg))
+ return ..()
+
+/datum/wound/pierce/bleed/receive_damage(wounding_type, wounding_dmg, wound_bonus)
+ if(victim.stat == DEAD || (wounding_dmg < 5) || !limb.can_bleed() || !victim.blood_volume || !prob(internal_bleeding_chance + wounding_dmg))
return
if(limb.current_gauze?.splint_factor)
wounding_dmg *= (1 - limb.current_gauze.splint_factor)
@@ -50,9 +55,9 @@
new /obj/effect/temp_visual/dir_setting/bloodsplatter(victim.loc, victim.dir)
victim.add_splatter_floor(get_step(victim.loc, victim.dir))
-/datum/wound/pierce/get_bleed_rate_of_change()
+/datum/wound/pierce/bleed/get_bleed_rate_of_change()
//basically if a species doesn't bleed, the wound is stagnant and will not heal on it's own (nor get worse)
- if(no_bleeding)
+ if(!limb.can_bleed())
return BLOOD_FLOW_STEADY
if(HAS_TRAIT(victim, TRAIT_BLOODY_MESS))
return BLOOD_FLOW_INCREASING
@@ -60,10 +65,13 @@
return BLOOD_FLOW_DECREASING
return BLOOD_FLOW_STEADY
-/datum/wound/pierce/handle_process(seconds_per_tick, times_fired)
+/datum/wound/pierce/bleed/handle_process(seconds_per_tick, times_fired)
+ if (!victim || IS_IN_STASIS(victim))
+ return
+
set_blood_flow(min(blood_flow, WOUND_SLASH_MAX_BLOODFLOW))
- if(!no_bleeding)
+ if(limb.can_bleed())
if(victim.bodytemperature < (BODYTEMP_NORMAL - 10))
adjust_blood_flow(-0.1 * seconds_per_tick)
if(SPT_PROB(2.5, seconds_per_tick))
@@ -79,36 +87,45 @@
if(blood_flow <= 0)
qdel(src)
-/datum/wound/pierce/on_stasis(seconds_per_tick, times_fired)
+/datum/wound/pierce/bleed/on_stasis(seconds_per_tick, times_fired)
. = ..()
if(blood_flow <= 0)
qdel(src)
-/datum/wound/pierce/check_grab_treatments(obj/item/I, mob/user)
+/datum/wound/pierce/bleed/check_grab_treatments(obj/item/I, mob/user)
if(I.get_temperature()) // if we're using something hot but not a cautery, we need to be aggro grabbing them first, so we don't try treating someone we're eswording
return TRUE
-/datum/wound/pierce/treat(obj/item/I, mob/user)
+/datum/wound/pierce/bleed/treat(obj/item/I, mob/user)
if(istype(I, /obj/item/stack/medical/suture))
- suture(I, user)
+ return suture(I, user)
else if(I.tool_behaviour == TOOL_CAUTERY || I.get_temperature())
- tool_cauterize(I, user)
+ return tool_cauterize(I, user)
-/datum/wound/pierce/on_xadone(power)
+/datum/wound/pierce/bleed/on_xadone(power)
. = ..()
- adjust_blood_flow(-0.03 * power) // i think it's like a minimum of 3 power, so .09 blood_flow reduction per tick is pretty good for 0 effort
-/datum/wound/pierce/on_synthflesh(power)
+ if (limb) // parent can cause us to be removed, so its reasonable to check if we're still applied
+ adjust_blood_flow(-0.03 * power) // i think it's like a minimum of 3 power, so .09 blood_flow reduction per tick is pretty good for 0 effort
+
+/datum/wound/pierce/bleed/on_synthflesh(power)
. = ..()
adjust_blood_flow(-0.025 * power) // 20u * 0.05 = -1 blood flow, less than with slashes but still good considering smaller bleed rates
/// If someone is using a suture to close this puncture
-/datum/wound/pierce/proc/suture(obj/item/stack/medical/suture/I, mob/user)
+/datum/wound/pierce/bleed/proc/suture(obj/item/stack/medical/suture/I, mob/user)
var/self_penalty_mult = (user == victim ? 1.4 : 1)
- user.visible_message(span_notice("[user] begins stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
- if(!do_after(user, base_treat_time * self_penalty_mult, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
- return
- var/bleeding_wording = (no_bleeding ? "holes" : "bleeding")
+ var/treatment_delay = base_treat_time * self_penalty_mult
+
+ if(HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ treatment_delay *= 0.5
+ user.visible_message(span_notice("[user] begins expertly stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I], keeping the holo-image information in mind..."))
+ else
+ user.visible_message(span_notice("[user] begins stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
+
+ if(!do_after(user, treatment_delay, target = victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+ var/bleeding_wording = (!limb.can_bleed() ? "holes" : "bleeding")
user.visible_message(span_green("[user] stitches up some of the [bleeding_wording] on [victim]."), span_green("You stitch up some of the [bleeding_wording] on [user == victim ? "yourself" : "[victim]"]."))
var/blood_sutured = I.stop_bleeding / self_penalty_mult
adjust_blood_flow(-blood_sutured)
@@ -116,20 +133,29 @@
I.use(1)
if(blood_flow > 0)
- try_treating(I, user)
+ return try_treating(I, user)
else
to_chat(user, span_green("You successfully close the hole in [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone]."))
+ return TRUE
/// If someone is using either a cautery tool or something with heat to cauterize this pierce
-/datum/wound/pierce/proc/tool_cauterize(obj/item/I, mob/user)
+/datum/wound/pierce/bleed/proc/tool_cauterize(obj/item/I, mob/user)
+
var/improv_penalty_mult = (I.tool_behaviour == TOOL_CAUTERY ? 1 : 1.25) // 25% longer and less effective if you don't use a real cautery
var/self_penalty_mult = (user == victim ? 1.5 : 1) // 50% longer and less effective if you do it to yourself
- user.visible_message(span_danger("[user] begins cauterizing [victim]'s [limb.plaintext_zone] with [I]..."), span_warning("You begin cauterizing [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
- if(!do_after(user, base_treat_time * self_penalty_mult * improv_penalty_mult, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
- return
+ var/treatment_delay = base_treat_time * self_penalty_mult * improv_penalty_mult
- var/bleeding_wording = (no_bleeding ? "holes" : "bleeding")
+ if(HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ treatment_delay *= 0.5
+ user.visible_message(span_danger("[user] begins expertly cauterizing [victim]'s [limb.plaintext_zone] with [I]..."), span_warning("You begin cauterizing [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I], keeping the holo-image indications in mind..."))
+ else
+ user.visible_message(span_danger("[user] begins cauterizing [victim]'s [limb.plaintext_zone] with [I]..."), span_warning("You begin cauterizing [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
+
+ if(!do_after(user, treatment_delay, target = victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+
+ var/bleeding_wording = (!limb.can_bleed() ? "holes" : "bleeding")
user.visible_message(span_green("[user] cauterizes some of the [bleeding_wording] on [victim]."), span_green("You cauterize some of the [bleeding_wording] on [victim]."))
limb.receive_damage(burn = 2 + severity, wound_bonus = CANT_WOUND)
if(prob(30))
@@ -138,10 +164,30 @@
adjust_blood_flow(-blood_cauterized)
if(blood_flow > 0)
- try_treating(I, user)
+ return try_treating(I, user)
+ return TRUE
+
+/datum/wound_pregen_data/flesh_pierce
+ abstract = TRUE
-/datum/wound/pierce/moderate
- name = "Minor Breakage"
+ required_limb_biostate = (BIO_FLESH)
+ required_wounding_types = list(WOUND_PIERCE)
+
+ wound_series = WOUND_SERIES_FLESH_PUNCTURE_BLEED
+
+/datum/wound_pregen_data/flesh_pierce
+ abstract = TRUE
+
+ required_limb_biostate = (BIO_FLESH)
+ required_wounding_types = list(WOUND_PIERCE)
+
+ wound_series = WOUND_SERIES_FLESH_PUNCTURE_BLEED
+
+/datum/wound/pierce/get_limb_examine_description()
+ return span_warning("The flesh on this limb appears badly perforated.")
+
+/datum/wound/pierce/bleed/moderate
+ name = "Minor Skin Breakage"
desc = "Patient's skin has been broken open, causing severe bruising and minor internal bleeding in affected area."
treat_text = "Treat affected site with bandaging or exposure to extreme cold. In dire cases, brief exposure to vacuum may suffice." // space is cold in ss13, so it's like an ice pack!
examine_desc = "has a small, circular hole, gently bleeding"
@@ -152,17 +198,23 @@
gauzed_clot_rate = 0.8
internal_bleeding_chance = 30
internal_bleeding_coefficient = 1.25
- threshold_minimum = 30
threshold_penalty = 20
status_effect_type = /datum/status_effect/wound/pierce/moderate
scar_keyword = "piercemoderate"
-/datum/wound/pierce/moderate/update_descriptions()
- if(no_bleeding)
+/datum/wound_pregen_data/flesh_pierce/breakage
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/pierce/bleed/moderate
+
+ threshold_minimum = 30
+
+/datum/wound/pierce/bleed/moderate/update_descriptions()
+ if(!limb.can_bleed())
examine_desc = "has a small, circular hole"
occur_text = "splits a small hole open"
-/datum/wound/pierce/severe
+/datum/wound/pierce/bleed/severe
name = "Open Puncture"
desc = "Patient's internal tissue is penetrated, causing sizeable internal bleeding and reduced limb stability."
treat_text = "Repair punctures in skin by suture or cautery, extreme cold may also work."
@@ -174,16 +226,22 @@
gauzed_clot_rate = 0.6
internal_bleeding_chance = 60
internal_bleeding_coefficient = 1.5
- threshold_minimum = 50
threshold_penalty = 35
status_effect_type = /datum/status_effect/wound/pierce/severe
scar_keyword = "piercesevere"
-/datum/wound/pierce/severe/update_descriptions()
- if(no_bleeding)
+/datum/wound_pregen_data/flesh_pierce/open_puncture
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/pierce/bleed/severe
+
+ threshold_minimum = 50
+
+/datum/wound/pierce/bleed/severe/update_descriptions()
+ if(!limb.can_bleed())
occur_text = "tears a hole open"
-/datum/wound/pierce/critical
+/datum/wound/pierce/bleed/critical
name = "Ruptured Cavity"
desc = "Patient's internal tissue and circulatory system is shredded, causing significant internal bleeding and damage to internal organs."
treat_text = "Surgical repair of puncture wound, followed by supervised resanguination."
@@ -195,8 +253,14 @@
gauzed_clot_rate = 0.4
internal_bleeding_chance = 80
internal_bleeding_coefficient = 1.75
- threshold_minimum = 100
threshold_penalty = 50
status_effect_type = /datum/status_effect/wound/pierce/critical
scar_keyword = "piercecritical"
- wound_flags = (FLESH_WOUND | ACCEPTS_GAUZE | MANGLES_FLESH)
+ wound_flags = (ACCEPTS_GAUZE | MANGLES_EXTERIOR | CAN_BE_GRASPED)
+
+/datum/wound_pregen_data/flesh_pierce/cavity
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/pierce/bleed/critical
+
+ threshold_minimum = 100
diff --git a/code/datums/wounds/scars/_scars.dm b/code/datums/wounds/scars/_scars.dm
index 743ac98e730d..5ee38f34f67a 100644
--- a/code/datums/wounds/scars/_scars.dm
+++ b/code/datums/wounds/scars/_scars.dm
@@ -22,11 +22,14 @@
var/visibility = 2
/// Whether this scar can actually be covered up by clothing
var/coverable = TRUE
- /// Obviously, scars that describe damaged flesh wouldn't apply to a skeleton (in some cases like bone wounds, there can be different descriptions for skeletons and fleshy humanoids)
- var/biology = BIO_FLESH_BONE
/// If we're a persistent scar or may become one, we go in this character slot
var/persistent_character_slot = 0
+ /// The biostates we require from a limb to give them our scar.
+ var/required_limb_biostate
+ /// If false, we will only check to see if a limb has ALL our biostates, instead of just any.
+ var/check_any_biostates
+
/datum/scar/Destroy(force, ...)
if(limb)
LAZYREMOVE(limb.scars, src)
@@ -47,6 +50,19 @@
* * add_to_scars- Should always be TRUE unless you're just storing a scar for later usage, like how cuts want to store a scar for the highest severity of cut, rather than the severity when the wound is fully healed (probably demoted to moderate)
*/
/datum/scar/proc/generate(obj/item/bodypart/BP, datum/wound/W, add_to_scars=TRUE)
+
+ if (!W.can_scar)
+ qdel(src)
+ return
+
+ var/datum/wound_pregen_data/pregen_data = GLOB.all_wound_pregen_data[W.type]
+ if (!pregen_data)
+ qdel(src)
+ return
+
+ required_limb_biostate = pregen_data.required_limb_biostate
+ check_any_biostates = pregen_data.require_any_biostate
+
limb = BP
RegisterSignal(limb, COMSIG_PARENT_QDELETING, PROC_REF(limb_gone))
@@ -59,12 +75,13 @@
if(victim)
LAZYADD(victim.all_scars, src)
- biology = limb?.biological_state || BIO_FLESH_BONE
+ var/scar_file = W.get_scar_file(BP, add_to_scars)
+ var/scar_keyword = W.get_scar_keyword(BP, add_to_scars)
+ if (!scar_file || !scar_keyword)
+ qdel(src)
+ return
- if((biology & BIO_BONE) && !(biology & BIO_FLESH))
- description = pick_list(BONE_SCAR_FILE, W.scar_keyword) || "general disfigurement"
- else // no specific support for flesh w/o bone scars since it's not really useful
- description = pick_list(FLESH_SCAR_FILE, W.scar_keyword) || "general disfigurement"
+ description = pick_list(W.get_scar_file(BP, add_to_scars), W.get_scar_keyword(BP, add_to_scars)) || "general disfigurement"
precise_location = pick_list_replacements(SCAR_LOC_FILE, limb.body_zone)
switch(W.severity)
@@ -86,22 +103,28 @@
LAZYADD(victim.all_scars, src)
/// Used to "load" a persistent scar
-/datum/scar/proc/load(obj/item/bodypart/BP, version, description, specific_location, severity=WOUND_SEVERITY_SEVERE, biology=BIO_FLESH_BONE, char_slot)
- if(!IS_ORGANIC_LIMB(BP))
+/datum/scar/proc/load(obj/item/bodypart/BP, version, description, specific_location, severity = WOUND_SEVERITY_SEVERE, required_limb_biostate = BIO_STANDARD_UNJOINTED, char_slot, check_any_biostates = FALSE)
+ if(!BP.scarrable)
qdel(src)
return
limb = BP
RegisterSignal(limb, COMSIG_PARENT_QDELETING, PROC_REF(limb_gone))
- if(limb.owner)
- victim = limb.owner
- if(limb.biological_state != biology)
+ if (isnull(check_any_biostates)) // so we dont break old scars. NOTE: REMOVE AFTER VERSION NUMBER MOVES PAST 3
+ check_any_biostates = FALSE
+ if (check_any_biostates)
+ if (!(limb.biological_state & required_limb_biostate))
qdel(src)
return
+ else if (!((limb.biological_state & required_limb_biostate) == required_limb_biostate)) // check for all
+ qdel(src)
+ return
+ if(limb.owner)
+ victim = limb.owner
LAZYADD(victim.all_scars, src)
src.severity = severity
- src.biology = biology
+ src.required_limb_biostate = required_limb_biostate
persistent_character_slot = char_slot
LAZYADD(limb.scars, src)
@@ -163,9 +186,9 @@
/// Used to format a scar to save for either persistent scars, or for changeling disguises
/datum/scar/proc/format()
- return "[SCAR_CURRENT_VERSION]|[limb.body_zone]|[description]|[precise_location]|[severity]|[biology]|[persistent_character_slot]"
+ return "[SCAR_CURRENT_VERSION]|[limb.body_zone]|[description]|[precise_location]|[severity]|[required_limb_biostate]|[persistent_character_slot]|[check_any_biostates]"
/// Used to format a scar to save in preferences for persistent scars
-/datum/scar/proc/format_amputated(body_zone)
- description = pick_list(FLESH_SCAR_FILE, "dismember")
- return "[SCAR_CURRENT_VERSION]|[body_zone]|[description]|amputated|[WOUND_SEVERITY_LOSS]|[BIO_FLESH_BONE]|[persistent_character_slot]"
+/datum/scar/proc/format_amputated(body_zone, scar_file = FLESH_SCAR_FILE)
+ description = pick_list(scar_file, "dismember")
+ return "[SCAR_CURRENT_VERSION]|[body_zone]|[description]|amputated|[WOUND_SEVERITY_LOSS]|[required_limb_biostate]|[persistent_character_slot]|[check_any_biostates]"
diff --git a/code/datums/wounds/scars/_static_scar_data.dm b/code/datums/wounds/scars/_static_scar_data.dm
new file mode 100644
index 000000000000..942dcffff9c2
--- /dev/null
+++ b/code/datums/wounds/scars/_static_scar_data.dm
@@ -0,0 +1,20 @@
+GLOBAL_LIST_INIT_TYPED(all_static_scar_data, /datum/static_scar_data, generate_static_scar_data())
+
+/proc/generate_static_scar_data()
+ RETURN_TYPE(/list/datum/static_scar_data)
+
+ var/list/datum/wound_pregen_data/data = list()
+
+ for (var/datum/wound_pregen_data/path as anything in typecacheof(path = /datum/static_scar_data, ignore_root_path = TRUE))
+ if (initial(path.abstract))
+ continue
+
+ var/datum/wound_pregen_data/pregen_data = new path
+ data[pregen_data.wound_path_to_generate] = pregen_data
+
+ return data
+
+/datum/static_scar_data
+ var/abstract = FALSE
+
+
diff --git a/code/datums/wounds/slash.dm b/code/datums/wounds/slash.dm
index 181bfc2a012d..c2869c947375 100644
--- a/code/datums/wounds/slash.dm
+++ b/code/datums/wounds/slash.dm
@@ -6,13 +6,25 @@
/datum/wound/slash
name = "Slashing (Cut) Wound"
sound_effect = 'sound/weapons/slice.ogg'
+
+/datum/wound_pregen_data/flesh_slash
+ abstract = TRUE
+
+ required_wounding_types = list(WOUND_SLASH)
+ required_limb_biostate = BIO_FLESH
+
+ wound_series = WOUND_SERIES_FLESH_SLASH_BLEED
+
+/datum/wound/slash/flesh
+ name = "Slashing (Cut) Flesh Wound"
processes = TRUE
- wound_type = WOUND_SLASH
treatable_by = list(/obj/item/stack/medical/suture)
treatable_by_grabbed = list(/obj/item/gun/energy/laser)
- treatable_tool = TOOL_CAUTERY
+ treatable_tools = list(TOOL_CAUTERY)
base_treat_time = 3 SECONDS
- wound_flags = (FLESH_WOUND | ACCEPTS_GAUZE)
+ wound_flags = (ACCEPTS_GAUZE|CAN_BE_GRASPED)
+
+ default_scar_file = FLESH_SCAR_FILE
/// How much blood we start losing when this wound is first applied
var/initial_flow
@@ -30,7 +42,12 @@
/// A bad system I'm using to track the worst scar we earned (since we can demote, we want the biggest our wound has been, not what it was when it was cured (probably moderate))
var/datum/scar/highest_scar
-/datum/wound/slash/wound_injury(datum/wound/slash/old_wound = null, attack_direction = null)
+/datum/wound/slash/flesh/Destroy()
+ highest_scar = null
+
+ return ..()
+
+/datum/wound/slash/flesh/wound_injury(datum/wound/slash/flesh/old_wound = null, attack_direction = null)
if(old_wound)
set_blood_flow(max(old_wound.blood_flow, initial_flow))
if(old_wound.severity > severity && old_wound.highest_scar)
@@ -38,7 +55,7 @@
old_wound.clear_highest_scar()
else
set_blood_flow(initial_flow)
- if(!no_bleeding && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY)
+ if(limb.can_bleed() && attack_direction && victim.blood_volume > BLOOD_VOLUME_OKAY)
victim.spray_blood(attack_direction, severity)
if(!highest_scar)
@@ -46,24 +63,26 @@
set_highest_scar(new_scar)
new_scar.generate(limb, src, add_to_scars=FALSE)
-/datum/wound/slash/proc/set_highest_scar(datum/scar/new_scar)
+ return ..()
+
+/datum/wound/slash/flesh/proc/set_highest_scar(datum/scar/new_scar)
if(highest_scar)
UnregisterSignal(highest_scar, COMSIG_PARENT_QDELETING)
if(new_scar)
RegisterSignal(new_scar, COMSIG_PARENT_QDELETING, PROC_REF(clear_highest_scar))
highest_scar = new_scar
-/datum/wound/slash/proc/clear_highest_scar(datum/source)
+/datum/wound/slash/flesh/proc/clear_highest_scar(datum/source)
SIGNAL_HANDLER
set_highest_scar(null)
-/datum/wound/slash/remove_wound(ignore_limb, replaced)
+/datum/wound/slash/flesh/remove_wound(ignore_limb, replaced)
if(!replaced && highest_scar)
already_scarred = TRUE
highest_scar.lazy_attach(limb)
return ..()
-/datum/wound/slash/get_examine_description(mob/user)
+/datum/wound/slash/flesh/get_wound_description(mob/user)
if(!limb.current_gauze)
return ..()
@@ -82,11 +101,16 @@
return "[msg.Join()]"
-/datum/wound/slash/receive_damage(wounding_type, wounding_dmg, wound_bonus)
+/datum/wound/slash/flesh/receive_damage(wounding_type, wounding_dmg, wound_bonus)
+ if (!victim) // if we are dismembered, we can still take damage, its fine to check here
+ return
+
if(victim.stat != DEAD && wound_bonus != CANT_WOUND && wounding_type == WOUND_SLASH) // can't stab dead bodies to make it bleed faster this way
adjust_blood_flow(WOUND_SLASH_DAMAGE_FLOW_COEFF * wounding_dmg)
-/datum/wound/slash/drag_bleed_amount()
+ return ..()
+
+/datum/wound/slash/flesh/drag_bleed_amount()
// say we have 3 severe cuts with 3 blood flow each, pretty reasonable
// compare with being at 100 brute damage before, where you bled (brute/100 * 2), = 2 blood per tile
var/bleed_amt = min(blood_flow * 0.1, 1) // 3 * 3 * 0.1 = 0.9 blood total, less than before! the share here is .3 blood of course.
@@ -97,9 +121,9 @@
return bleed_amt
-/datum/wound/slash/get_bleed_rate_of_change()
+/datum/wound/slash/flesh/get_bleed_rate_of_change()
//basically if a species doesn't bleed, the wound is stagnant and will not heal on it's own (nor get worse)
- if(no_bleeding)
+ if(!limb.can_bleed())
return BLOOD_FLOW_STEADY
if(HAS_TRAIT(victim, TRAIT_BLOODY_MESS))
return BLOOD_FLOW_INCREASING
@@ -108,9 +132,13 @@
if(clot_rate < 0)
return BLOOD_FLOW_INCREASING
-/datum/wound/slash/handle_process(seconds_per_tick, times_fired)
+/datum/wound/slash/flesh/handle_process(seconds_per_tick, times_fired)
+
+ if (!victim || IS_IN_STASIS(victim))
+ return
+
// in case the victim has the NOBLOOD trait, the wound will simply not clot on it's own
- if(!no_bleeding)
+ if(limb.can_bleed())
set_blood_flow(min(blood_flow, WOUND_SLASH_MAX_BLOODFLOW))
if(HAS_TRAIT(victim, TRAIT_BLOODY_MESS))
@@ -123,7 +151,7 @@
adjust_blood_flow(-limb.current_gauze.absorption_rate * seconds_per_tick)
limb.seep_gauze(limb.current_gauze.absorption_rate * seconds_per_tick)
//otherwise, only clot if it's a bleeder
- else if(!no_bleeding)
+ else if(limb.can_bleed())
adjust_blood_flow(-clot_rate * seconds_per_tick)
if(blood_flow > highest_flow)
@@ -131,36 +159,36 @@
if(blood_flow < minimum_flow)
if(demotes_to)
- replace_wound(demotes_to)
+ replace_wound(new demotes_to)
else
- to_chat(victim, span_green("The cut on your [limb.plaintext_zone] has [no_bleeding ? "healed up" : "stopped bleeding"]!"))
+ to_chat(victim, span_green("The cut on your [limb.plaintext_zone] has [!limb.can_bleed() ? "healed up" : "stopped bleeding"]!"))
qdel(src)
-/datum/wound/slash/on_stasis(seconds_per_tick, times_fired)
+/datum/wound/slash/flesh/on_stasis(seconds_per_tick, times_fired)
if(blood_flow >= minimum_flow)
return
if(demotes_to)
- replace_wound(demotes_to)
+ replace_wound(new demotes_to)
return
qdel(src)
/* BEWARE, THE BELOW NONSENSE IS MADNESS. bones.dm looks more like what I have in mind and is sufficiently clean, don't pay attention to this messiness */
-/datum/wound/slash/check_grab_treatments(obj/item/I, mob/user)
+/datum/wound/slash/flesh/check_grab_treatments(obj/item/I, mob/user)
if(istype(I, /obj/item/gun/energy/laser))
return TRUE
if(I.get_temperature()) // if we're using something hot but not a cautery, we need to be aggro grabbing them first, so we don't try treating someone we're eswording
return TRUE
-/datum/wound/slash/treat(obj/item/I, mob/user)
+/datum/wound/slash/flesh/treat(obj/item/I, mob/user)
if(istype(I, /obj/item/gun/energy/laser))
- las_cauterize(I, user)
+ return las_cauterize(I, user)
else if(I.tool_behaviour == TOOL_CAUTERY || I.get_temperature())
- tool_cauterize(I, user)
+ return tool_cauterize(I, user)
else if(istype(I, /obj/item/stack/medical/suture))
- suture(I, user)
+ return suture(I, user)
-/datum/wound/slash/try_handling(mob/living/carbon/human/user)
+/datum/wound/slash/flesh/try_handling(mob/living/carbon/human/user)
if(user.pulling != victim || user.zone_selected != limb.body_zone || !victim.try_inject(user, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE))
return FALSE
if(DOING_INTERACTION_WITH_TARGET(user, victim))
@@ -175,16 +203,41 @@
return TRUE
-/datum/wound/slash/on_xadone(power)
+/// if a felinid is licking this cut to reduce bleeding
+/datum/wound/slash/flesh/proc/lick_wounds(mob/living/carbon/human/user)
+ // transmission is one way patient -> felinid since google said cat saliva is antiseptic or whatever, and also because felinids are already risking getting beaten for this even without people suspecting they're spreading a deathvirus
+ for(var/i in victim.diseases)
+ var/datum/disease/iter_disease = i
+ if(iter_disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
+ continue
+ user.ForceContractDisease(iter_disease)
+
+ user.visible_message(span_notice("[user] begins licking the wounds on [victim]'s [limb.plaintext_zone]."), span_notice("You begin licking the wounds on [victim]'s [limb.plaintext_zone]..."), ignored_mobs=victim)
+ to_chat(victim, span_notice("[user] begins to lick the wounds on your [limb.plaintext_zone]."))
+ if(!do_after(user, base_treat_time, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return
+
+ user.visible_message(span_notice("[user] licks the wounds on [victim]'s [limb.plaintext_zone]."), span_notice("You lick some of the wounds on [victim]'s [limb.plaintext_zone]"), ignored_mobs=victim)
+ to_chat(victim, span_green("[user] licks the wounds on your [limb.plaintext_zone]!"))
+ adjust_blood_flow(-0.5)
+
+ if(blood_flow > minimum_flow)
+ try_handling(user)
+ else if(demotes_to)
+ to_chat(user, span_green("You successfully lower the severity of [victim]'s cuts."))
+
+/datum/wound/slash/flesh/on_xadone(power)
. = ..()
- adjust_blood_flow(-0.03 * power) // i think it's like a minimum of 3 power, so .09 blood_flow reduction per tick is pretty good for 0 effort
-/datum/wound/slash/on_synthflesh(power)
+ if (limb) // parent can cause us to be removed, so its reasonable to check if we're still applied
+ adjust_blood_flow(-0.03 * power) // i think it's like a minimum of 3 power, so .09 blood_flow reduction per tick is pretty good for 0 effort
+
+/datum/wound/slash/flesh/on_synthflesh(power)
. = ..()
adjust_blood_flow(-0.075 * power) // 20u * 0.075 = -1.5 blood flow, pretty good for how little effort it is
/// If someone's putting a laser gun up to our cut to cauterize it
-/datum/wound/slash/proc/las_cauterize(obj/item/gun/energy/laser/lasgun, mob/user)
+/datum/wound/slash/flesh/proc/las_cauterize(obj/item/gun/energy/laser/lasgun, mob/user)
var/self_penalty_mult = (user == victim ? 1.25 : 1)
user.visible_message(span_warning("[user] begins aiming [lasgun] directly at [victim]'s [limb.plaintext_zone]..."), span_userdanger("You begin aiming [lasgun] directly at [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone]..."))
if(!do_after(user, base_treat_time * self_penalty_mult, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
@@ -197,16 +250,17 @@
victim.emote("scream")
adjust_blood_flow(-1 * (damage / (5 * self_penalty_mult))) // 20 / 5 = 4 bloodflow removed, p good
victim.visible_message(span_warning("The cuts on [victim]'s [limb.plaintext_zone] scar over!"))
+ return TRUE
/// If someone is using either a cautery tool or something with heat to cauterize this cut
-/datum/wound/slash/proc/tool_cauterize(obj/item/I, mob/user)
+/datum/wound/slash/flesh/proc/tool_cauterize(obj/item/I, mob/user)
var/improv_penalty_mult = (I.tool_behaviour == TOOL_CAUTERY ? 1 : 1.25) // 25% longer and less effective if you don't use a real cautery
var/self_penalty_mult = (user == victim ? 1.5 : 1) // 50% longer and less effective if you do it to yourself
user.visible_message(span_danger("[user] begins cauterizing [victim]'s [limb.plaintext_zone] with [I]..."), span_warning("You begin cauterizing [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
if(!do_after(user, base_treat_time * self_penalty_mult * improv_penalty_mult, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
return
- var/bleeding_wording = (no_bleeding ? "cuts" : "bleeding")
+ var/bleeding_wording = (!limb.can_bleed() ? "cuts" : "bleeding")
user.visible_message(span_green("[user] cauterizes some of the [bleeding_wording] on [victim]."), span_green("You cauterize some of the [bleeding_wording] on [victim]."))
limb.receive_damage(burn = 2 + severity, wound_bonus = CANT_WOUND)
if(prob(30))
@@ -215,18 +269,27 @@
adjust_blood_flow(-blood_cauterized)
if(blood_flow > minimum_flow)
- try_treating(I, user)
+ return try_treating(I, user)
else if(demotes_to)
to_chat(user, span_green("You successfully lower the severity of [user == victim ? "your" : "[victim]'s"] cuts."))
+ return TRUE
+ return FALSE
/// If someone is using a suture to close this cut
-/datum/wound/slash/proc/suture(obj/item/stack/medical/suture/I, mob/user)
+/datum/wound/slash/flesh/proc/suture(obj/item/stack/medical/suture/I, mob/user)
var/self_penalty_mult = (user == victim ? 1.4 : 1)
user.visible_message(span_notice("[user] begins stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
+ var/treatment_delay = base_treat_time * self_penalty_mult
- if(!do_after(user, base_treat_time * self_penalty_mult, target=victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
- return
- var/bleeding_wording = (no_bleeding ? "cuts" : "bleeding")
+ if(HAS_TRAIT(src, TRAIT_WOUND_SCANNED))
+ treatment_delay *= 0.5
+ user.visible_message(span_notice("[user] begins expertly stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I], keeping the holo-image information in mind..."))
+ else
+ user.visible_message(span_notice("[user] begins stitching [victim]'s [limb.plaintext_zone] with [I]..."), span_notice("You begin stitching [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]..."))
+
+ if(!do_after(user, treatment_delay, target = victim, extra_checks = CALLBACK(src, PROC_REF(still_exists))))
+ return TRUE
+ var/bleeding_wording = (!limb.can_bleed() ? "cuts" : "bleeding")
user.visible_message(span_green("[user] stitches up some of the [bleeding_wording] on [victim]."), span_green("You stitch up some of the [bleeding_wording] on [user == victim ? "yourself" : "[victim]"]."))
var/blood_sutured = I.stop_bleeding / self_penalty_mult
adjust_blood_flow(-blood_sutured)
@@ -234,11 +297,16 @@
I.use(1)
if(blood_flow > minimum_flow)
- try_treating(I, user)
+ return try_treating(I, user)
else if(demotes_to)
to_chat(user, span_green("You successfully lower the severity of [user == victim ? "your" : "[victim]'s"] cuts."))
+ return TRUE
+ return TRUE
+
+/datum/wound/slash/get_limb_examine_description()
+ return span_warning("The flesh on this limb appears badly lacerated.")
-/datum/wound/slash/moderate
+/datum/wound/slash/flesh/moderate
name = "Rough Abrasion"
desc = "Patient's skin has been badly scraped, generating moderate blood loss."
treat_text = "Application of clean bandages or first-aid grade sutures, followed by food and rest."
@@ -249,16 +317,22 @@
initial_flow = 2
minimum_flow = 0.5
clot_rate = 0.05
- threshold_minimum = 20
threshold_penalty = 10
- status_effect_type = /datum/status_effect/wound/slash/moderate
+ status_effect_type = /datum/status_effect/wound/slash/flesh/moderate
scar_keyword = "slashmoderate"
-/datum/wound/slash/moderate/update_descriptions()
- if(no_bleeding)
+/datum/wound/slash/flesh/moderate/update_descriptions()
+ if(!limb.can_bleed())
occur_text = "is cut open"
-/datum/wound/slash/severe
+/datum/wound_pregen_data/flesh_slash/abrasion
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/slash/flesh/moderate
+
+ threshold_minimum = 20
+
+/datum/wound/slash/flesh/severe
name = "Open Laceration"
desc = "Patient's skin is ripped clean open, allowing significant blood loss."
treat_text = "Speedy application of first-aid grade sutures and clean bandages, followed by vitals monitoring to ensure recovery."
@@ -269,48 +343,74 @@
initial_flow = 3.25
minimum_flow = 2.75
clot_rate = 0.03
- threshold_minimum = 50
threshold_penalty = 25
- demotes_to = /datum/wound/slash/moderate
- status_effect_type = /datum/status_effect/wound/slash/severe
+ demotes_to = /datum/wound/slash/flesh/moderate
+ status_effect_type = /datum/status_effect/wound/slash/flesh/severe
scar_keyword = "slashsevere"
-/datum/wound/slash/severe/update_descriptions()
- if(no_bleeding)
+/datum/wound_pregen_data/flesh_slash/laceration
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/slash/flesh/severe
+
+ threshold_minimum = 50
+
+/datum/wound/slash/flesh/severe/update_descriptions()
+ if(!limb.can_bleed())
occur_text = "is ripped open"
-/datum/wound/slash/critical
+/datum/wound/slash/flesh/critical
name = "Weeping Avulsion"
desc = "Patient's skin is completely torn open, along with significant loss of tissue. Extreme blood loss will lead to quick death without intervention."
treat_text = "Immediate bandaging and either suturing or cauterization, followed by supervised resanguination."
examine_desc = "is carved down to the bone, spraying blood wildly"
- examine_desc = "is carved down to the bone"
occur_text = "is torn open, spraying blood wildly"
- occur_text = "is torn open"
sound_effect = 'sound/effects/wounds/blood3.ogg'
severity = WOUND_SEVERITY_CRITICAL
initial_flow = 4
minimum_flow = 3.85
clot_rate = -0.015 // critical cuts actively get worse instead of better
- threshold_minimum = 80
threshold_penalty = 40
- demotes_to = /datum/wound/slash/severe
- status_effect_type = /datum/status_effect/wound/slash/critical
+ demotes_to = /datum/wound/slash/flesh/severe
+ status_effect_type = /datum/status_effect/wound/slash/flesh/critical
scar_keyword = "slashcritical"
- wound_flags = (FLESH_WOUND | ACCEPTS_GAUZE | MANGLES_FLESH)
+ wound_flags = (ACCEPTS_GAUZE | MANGLES_EXTERIOR | CAN_BE_GRASPED)
+
+/datum/wound/slash/flesh/critical/update_descriptions()
+ if (!limb.can_bleed())
+ occur_text = "is torn open"
+
+/datum/wound_pregen_data/flesh_slash/avulsion
+ abstract = FALSE
+
+ wound_path_to_generate = /datum/wound/slash/flesh/critical
-/datum/wound/slash/moderate/many_cuts
+ threshold_minimum = 80
+
+/datum/wound/slash/flesh/moderate/many_cuts
name = "Numerous Small Slashes"
desc = "Patient's skin has numerous small slashes and cuts, generating moderate blood loss."
examine_desc = "has a ton of small cuts"
occur_text = "is cut numerous times, leaving many small slashes."
+/datum/wound_pregen_data/flesh_slash/abrasion/cuts
+ abstract = FALSE
+ can_be_randomly_generated = FALSE
+
+ wound_path_to_generate = /datum/wound/slash/flesh/moderate/many_cuts
+
// Subtype for cleave (heretic spell)
-/datum/wound/slash/critical/cleave
+/datum/wound/slash/flesh/critical/cleave
name = "Burning Avulsion"
examine_desc = "is ruptured, spraying blood wildly"
clot_rate = 0.01
-/datum/wound/slash/critical/cleave/update_descriptions()
- if(no_bleeding)
+/datum/wound/slash/flesh/critical/cleave/update_descriptions()
+ if(!limb.can_bleed())
occur_text = "is ruptured"
+
+/datum/wound_pregen_data/flesh_slash/avulsion/clear
+ abstract = FALSE
+ can_be_randomly_generated = FALSE
+
+ wound_path_to_generate = /datum/wound/slash/flesh/critical/cleave
diff --git a/code/game/alternate_appearance.dm b/code/game/alternate_appearance.dm
index 994f51d13dfc..9b8a813c7398 100644
--- a/code/game/alternate_appearance.dm
+++ b/code/game/alternate_appearance.dm
@@ -149,7 +149,7 @@ GLOBAL_LIST_EMPTY(active_alternate_appearances)
/datum/atom_hud/alternate_appearance/basic/blessed_aware/mobShouldSee(mob/M)
if(M.mind?.holy_role)
return TRUE
- if (istype(M, /mob/living/simple_animal/hostile/construct/wraith))
+ if (istype(M, /mob/living/basic/construct/wraith))
return TRUE
if(isrevenant(M) || IS_WIZARD(M) || IS_CLOCK(M)) //monkestation edit: adds IS_CLOCK check
return TRUE
diff --git a/code/game/atom_defense.dm b/code/game/atom_defense.dm
index dd4cbbc46b45..7d6fa23911f9 100644
--- a/code/game/atom_defense.dm
+++ b/code/game/atom_defense.dm
@@ -134,6 +134,8 @@
/// A cut-out proc for [/atom/proc/bullet_act] so living mobs can have their own armor behavior checks without causing issues with needing their own on_hit call
/atom/proc/check_projectile_armor(def_zone, obj/projectile/impacting_projectile, is_silent)
+ if(uses_integrity)
+ return clamp(PENETRATE_ARMOUR(get_armor_rating(impacting_projectile.armor_flag), impacting_projectile.armour_penetration), 0, 100)
return 0
/**
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 9f7d6effc033..53d633b736eb 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -393,6 +393,8 @@
/atom/proc/CanPass(atom/movable/mover, border_dir)
SHOULD_CALL_PARENT(TRUE)
SHOULD_BE_PURE(TRUE)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_TRIED_PASS, mover, border_dir) & COMSIG_COMPONENT_PERMIT_PASSAGE)
+ return TRUE
if(mover.movement_type & PHASING)
return TRUE
. = CanAllowThrough(mover, border_dir)
@@ -612,6 +614,7 @@
SEND_SIGNAL(source, COMSIG_REAGENTS_EXPOSE_ATOM, src, reagents, methods, volume_modifier, show_message)
for(var/datum/reagent/current_reagent as anything in reagents)
. |= current_reagent.expose_atom(src, reagents[current_reagent])
+ SEND_SIGNAL(src, COMSIG_ATOM_AFTER_EXPOSE_REAGENTS, reagents, source, methods, volume_modifier, show_message)
/// Are you allowed to drop this atom
/atom/proc/AllowDrop()
@@ -640,19 +643,33 @@
/**
* React to a hit by a projectile object
*
- * Default behaviour is to send the [COMSIG_ATOM_BULLET_ACT] and then call [on_hit][/obj/projectile/proc/on_hit] on the projectile.
- *
* @params
- * hitting_projectile - projectile
- * def_zone - zone hit
- * piercing_hit - is this hit piercing or normal?
+ * * hitting_projectile - projectile
+ * * def_zone - zone hit
+ * * piercing_hit - is this hit piercing or normal?
*/
/atom/proc/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ SHOULD_CALL_PARENT(TRUE)
+
+ var/sigreturn = SEND_SIGNAL(src, COMSIG_ATOM_PRE_BULLET_ACT, hitting_projectile, def_zone)
+ if(sigreturn & COMPONENT_BULLET_PIERCED)
+ return BULLET_ACT_FORCE_PIERCE
+ if(sigreturn & COMPONENT_BULLET_BLOCKED)
+ return BULLET_ACT_BLOCK
+ if(sigreturn & COMPONENT_BULLET_ACTED)
+ return BULLET_ACT_HIT
+
SEND_SIGNAL(src, COMSIG_ATOM_BULLET_ACT, hitting_projectile, def_zone)
- // This armor check only matters for the visuals and messages in on_hit(), it's not actually used to reduce damage since
- // only living mobs use armor to reduce damage, but on_hit() is going to need the value no matter what is shot.
- var/visual_armor_check = check_projectile_armor(def_zone, hitting_projectile)
- . = hitting_projectile.on_hit(src, visual_armor_check, def_zone, piercing_hit)
+ if(QDELETED(hitting_projectile)) // Signal deleted it?
+ return BULLET_ACT_BLOCK
+
+ return hitting_projectile.on_hit(
+ target = src,
+ // This armor check only matters for the visuals and messages in on_hit(), it's not actually used to reduce damage since
+ // only living mobs use armor to reduce damage, but on_hit() is going to need the value no matter what is shot.
+ blocked = check_projectile_armor(def_zone, hitting_projectile),
+ pierce_hit = piercing_hit,
+ )
///Return true if we're inside the passed in atom
/atom/proc/in_contents_of(container)//can take class or object instance as argument
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index c2ee234d5857..38b5c1f9712a 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -15,7 +15,11 @@
var/tk_throw_range = 10
var/mob/pulledby = null
var/initial_language_holder = /datum/language_holder
- var/datum/language_holder/language_holder // Mindless mobs and objects need language too, some times. Mind holder takes prescedence.
+ /// Holds all languages this mob can speak and understand
+ var/datum/language_holder/language_holder
+ /// The list of factions this atom belongs to
+ var/list/faction
+
var/verb_say = "says"
var/verb_ask = "asks"
var/verb_exclaim = "exclaims"
@@ -1664,3 +1668,20 @@
*/
/atom/movable/proc/keybind_face_direction(direction)
setDir(direction)
+
+/**
+ * Check if the other atom/movable has any factions the same as us. Defined at the atom/movable level so it can be defined for just about anything.
+ *
+ * If exact match is set, then all our factions must match exactly
+ */
+/atom/movable/proc/faction_check_atom(atom/movable/target, exact_match)
+ if(!exact_match)
+ return faction_check(faction, target.faction, FALSE)
+
+ var/list/faction_src = LAZYCOPY(faction)
+ var/list/faction_target = LAZYCOPY(target.faction)
+ if(!("[REF(src)]" in faction_target)) //if they don't have our ref faction, remove it from our factions list.
+ faction_src -= "[REF(src)]" //if we don't do this, we'll never have an exact match.
+ if(!("[REF(target)]" in faction_src))
+ faction_target -= "[REF(target)]" //same thing here.
+ return faction_check(faction_src, faction_target, TRUE)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 286fc3d3d843..ee7de2c50afd 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -576,7 +576,7 @@
var/datum/mind/player_mind = new /datum/mind(applicant.key)
player_mind.active = TRUE
- var/mob/living/simple_animal/hostile/space_dragon/S = new (pick(spawn_locs))
+ var/mob/living/basic/space_dragon/S = new (pick(spawn_locs))
player_mind.transfer_to(S)
player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_dragon))
player_mind.special_role = ROLE_SPACE_DRAGON
@@ -714,8 +714,8 @@
. = ..()
/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant)
- var/mob/living/simple_animal/revenant/revenant = new(pick(spawn_locs))
- revenant.key = applicant.key
+ var/mob/living/basic/revenant/revenant = new(pick(spawn_locs))
+ applicant.mind.transfer_to(revenant)
message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.")
log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.")
return revenant
diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm
index 1562914ea66b..9e1ae52dc9ac 100644
--- a/code/game/gamemodes/objective_items.dm
+++ b/code/game/gamemodes/objective_items.dm
@@ -124,6 +124,40 @@
targetitem = /obj/item/clothing/shoes/clown_shoes
excludefromjob = list(JOB_CLOWN, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
item_owner = list(JOB_CLOWN)
+ exists_on_map = TRUE
+
+/obj/item/clothing/shoes/clown_shoes/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/shoes/clown_shoes)
+
+/datum/objective_item/steal/traitor/mime_mask
+ name = "the mime's mask"
+ targetitem = /obj/item/clothing/mask/gas/mime
+ excludefromjob = list(JOB_MIME, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
+ item_owner = list(JOB_MIME)
+ exists_on_map = TRUE
+
+/obj/item/clothing/mask/gas/mime/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/mask/gas/mime)
+
+/datum/objective_item/steal/traitor/pka
+ name = "a protokinetic accelerator"
+ targetitem = /obj/item/gun/energy/recharge/kinetic_accelerator
+ excludefromjob = list(JOB_SHAFT_MINER, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
+ item_owner = list(JOB_SHAFT_MINER)
+ exists_on_map = TRUE
+
+/obj/item/gun/energy/recharge/kinetic_accelerator/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/gun/energy/recharge/kinetic_accelerator)
+
+/datum/objective_item/steal/traitor/chef_moustache
+ name = "a fancy fake moustache"
+ targetitem = /obj/item/clothing/mask/fakemoustache/italian
+ excludefromjob = list(JOB_COOK, JOB_HEAD_OF_PERSONNEL, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
+ item_owner = list(JOB_COOK)
+ exists_on_map = TRUE
+
+/obj/item/clothing/mask/fakemoustache/italian/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/mask/fakemoustache/italian)
/datum/objective_item/steal/traitor/det_revolver
name = "detective's revolver"
@@ -134,6 +168,16 @@
/obj/item/gun/ballistic/revolver/c38/detective/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/ballistic/revolver/c38/detective)
+/datum/objective_item/steal/traitor/lawyers_badge
+ name = "the lawyer's badge"
+ targetitem = /obj/item/clothing/accessory/lawyers_badge
+ excludefromjob = list(JOB_LAWYER)
+ item_owner = list(JOB_LAWYER)
+ exists_on_map = TRUE
+
+/obj/item/clothing/accessory/lawyers_badge/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/accessory/lawyers_badge)
+
/datum/objective_item/steal/traitor/chief_engineer_belt
name = "the chief engineer's belt"
targetitem = /obj/item/storage/belt/utility/chief
@@ -402,3 +446,79 @@
/obj/item/blackbox/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/blackbox)
+
+
+// A number of special early-game steal objectives intended to be used with the steal-and-destroy objective.
+// They're basically items of utility or emotional value that may be found on many players or lying around the station.
+/datum/objective_item/steal/traitor/insuls
+ name = "insulated gloves"
+ targetitem = /obj/item/clothing/gloves/color/yellow
+ excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
+ item_owner = list(JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
+ exists_on_map = TRUE
+
+/obj/item/clothing/gloves/color/yellow/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/gloves/color/yellow)
+
+/datum/objective_item/steal/traitor/moth_plush
+ name = "cute moth plush toy"
+ targetitem = /obj/item/toy/plush/moth
+ excludefromjob = list(JOB_PSYCHOLOGIST, JOB_PARAMEDIC, JOB_CHEMIST, JOB_MEDICAL_DOCTOR, JOB_VIROLOGIST, JOB_CHIEF_MEDICAL_OFFICER)
+ exists_on_map = TRUE
+
+/obj/item/toy/plush/moth/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/toy/plush/moth)
+
+/datum/objective_item/steal/traitor/lizard_plush
+ name = "cute lizard plush toy"
+ targetitem = /obj/item/toy/plush/lizard_plushie
+ exists_on_map = TRUE
+
+/obj/item/toy/plush/lizard_plushie/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/toy/plush/lizard_plushie)
+
+/datum/objective_item/steal/traitor/denied_stamp
+ name = "cargo's denied stamp"
+ targetitem = /obj/item/stamp/denied
+ excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER)
+ exists_on_map = TRUE
+
+/obj/item/stamp/denied/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/stamp/denied)
+
+/datum/objective_item/steal/traitor/granted_stamp
+ name = "cargo's granted stamp"
+ targetitem = /obj/item/stamp/granted
+ excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER)
+ exists_on_map = TRUE
+
+/obj/item/stamp/granted/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/stamp/granted)
+
+/datum/objective_item/steal/traitor/space_law
+ name = "a book on space law"
+ targetitem = /obj/item/book/manual/wiki/security_space_law
+ excludefromjob = list(JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_HEAD_OF_SECURITY, JOB_LAWYER, JOB_DETECTIVE)
+ exists_on_map = TRUE
+
+/obj/item/book/manual/wiki/security_space_law/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/book/manual/wiki/security_space_law)
+
+/datum/objective_item/steal/traitor/rpd
+ name = "rapid pipe dispenser"
+ targetitem = /obj/item/pipe_dispenser
+ excludefromjob = list(JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER, JOB_SCIENTIST, JOB_RESEARCH_DIRECTOR, JOB_GENETICIST, JOB_ROBOTICIST)
+ item_owner = list(JOB_CHIEF_ENGINEER)
+ exists_on_map = TRUE
+
+/obj/item/pipe_dispenser/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/pipe_dispenser)
+
+/datum/objective_item/steal/traitor/donut_box
+ name = "a box of prized donuts"
+ targetitem = /obj/item/storage/fancy/donut_box
+ excludefromjob = list(JOB_CAPTAIN, JOB_CHIEF_ENGINEER, JOB_HEAD_OF_PERSONNEL, JOB_HEAD_OF_SECURITY, JOB_QUARTERMASTER, JOB_CHIEF_MEDICAL_OFFICER, JOB_RESEARCH_DIRECTOR, JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_LAWYER, JOB_DETECTIVE)
+ exists_on_map = TRUE
+
+/obj/item/storage/fancy/donut_box/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/storage/fancy/donut_box)
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 2d6abc921ce7..5fca4a56b0d1 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -592,13 +592,7 @@
if(!isliving(user))
return FALSE //no ghosts allowed, sorry
- var/is_dextrous = FALSE
- if(isanimal(user))
- var/mob/living/simple_animal/user_as_animal = user
- if (user_as_animal.dextrous)
- is_dextrous = TRUE
-
- if(!issilicon(user) && !is_dextrous && !user.can_hold_items())
+ if(!issilicon(user) && !user.can_hold_items())
return FALSE //spiders gtfo
if(issilicon(user)) // If we are a silicon, make sure the machine allows silicons to interact with it
@@ -745,7 +739,7 @@
return
update_last_used(user)
-/obj/machinery/tool_act(mob/living/user, obj/item/tool, tool_type)
+/obj/machinery/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(SEND_SIGNAL(user, COMSIG_TRY_USE_MACHINE, src) & COMPONENT_CANT_USE_MACHINE_TOOLS)
return TOOL_ACT_MELEE_CHAIN_BLOCKING
. = ..()
diff --git a/code/game/machinery/computer/arcade/orion.dm b/code/game/machinery/computer/arcade/orion.dm
index e883b35fe1df..5bcd81b9b49c 100644
--- a/code/game/machinery/computer/arcade/orion.dm
+++ b/code/game/machinery/computer/arcade/orion.dm
@@ -499,7 +499,7 @@ GLOBAL_LIST_INIT(orion_events, generate_orion_events())
newgame()
obj_flags |= EMAGGED
-/mob/living/basic/syndicate/ranged/smg/orion
+/mob/living/basic/trooper/syndicate/ranged/smg/orion
name = "spaceport security"
desc = "Premier corporate security forces for all spaceports found along the Orion Trail."
faction = list(FACTION_ORION)
diff --git a/code/game/machinery/computer/arcade/orion_event.dm b/code/game/machinery/computer/arcade/orion_event.dm
index ea81b67532d1..97590059a526 100644
--- a/code/game/machinery/computer/arcade/orion_event.dm
+++ b/code/game/machinery/computer/arcade/orion_event.dm
@@ -525,7 +525,7 @@
game.say("WEEWOO! WEEWOO! Spaceport security en route!")
playsound(game, 'sound/items/weeoo1.ogg', 100, FALSE)
for(var/i in 1 to 3)
- var/mob/living/basic/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(game))
+ var/mob/living/basic/trooper/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(game))
spaceport_security.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, usr)
game.fuel += fuel
game.food += food
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index 99c5dc00eb7f..1560d1f17228 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -248,9 +248,9 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
if (jobs[trim_assignment] != null)
entry["ijob"] = jobs[trim_assignment]
- // Binary living/dead status
+ // Current status
if (sensor_mode >= SENSOR_LIVING)
- entry["life_status"] = (tracked_living_mob.stat != DEAD)
+ entry["life_status"] = tracked_living_mob.stat
// Damage
if (sensor_mode >= SENSOR_VITALS)
diff --git a/code/game/machinery/computer/orders/order_items/mining/order_toys.dm b/code/game/machinery/computer/orders/order_items/mining/order_toys.dm
index 06bd9032e74e..cd1dbf5c6748 100644
--- a/code/game/machinery/computer/orders/order_items/mining/order_toys.dm
+++ b/code/game/machinery/computer/orders/order_items/mining/order_toys.dm
@@ -14,7 +14,7 @@
cost_per_order = 300
/datum/orderable_item/toys_drones/mining_drone
- item_path = /mob/living/simple_animal/hostile/mining_drone
+ item_path = /mob/living/basic/mining_drone
cost_per_order = 800
/datum/orderable_item/toys_drones/drone_health
diff --git a/code/game/machinery/computer/robot.dm b/code/game/machinery/computer/robot.dm
index e3d3a0e2acba..88ca3b05df78 100644
--- a/code/game/machinery/computer/robot.dm
+++ b/code/game/machinery/computer/robot.dm
@@ -64,15 +64,15 @@
data["cyborgs"] += list(cyborg_data)
data["drones"] = list()
- for(var/mob/living/simple_animal/drone/D in GLOB.drones_list)
- if(D.hacked)
+ for(var/mob/living/basic/drone/drone in GLOB.drones_list)
+ if(drone.hacked)
continue
- if(!is_valid_z_level(current_turf, get_turf(D)))
+ if(!is_valid_z_level(current_turf, get_turf(drone)))
continue
var/list/drone_data = list(
- name = D.name,
- status = D.stat,
- ref = REF(D)
+ name = drone.name,
+ status = drone.stat,
+ ref = REF(drone)
)
data["drones"] += list(drone_data)
@@ -129,7 +129,7 @@
if("killdrone")
if(allowed(usr))
- var/mob/living/simple_animal/drone/drone = locate(params["ref"]) in GLOB.mob_list
+ var/mob/living/basic/drone/drone = locate(params["ref"]) in GLOB.mob_list
if(drone.hacked)
to_chat(usr, span_danger("ERROR: [drone] is not responding to external commands."))
else
diff --git a/code/game/machinery/iv_drip.dm b/code/game/machinery/iv_drip.dm
index 9c98916ecdbe..e691c5d9fc1b 100644
--- a/code/game/machinery/iv_drip.dm
+++ b/code/game/machinery/iv_drip.dm
@@ -245,12 +245,12 @@
if(!(get_dist(src, attached) <= 1 && isturf(attached.loc)))
if(isliving(attached))
- var/mob/living/attached_mob = attached
+ var/mob/living/carbon/attached_mob = attached
to_chat(attached, span_userdanger("The IV drip needle is ripped out of you, leaving an open bleeding wound!"))
var/list/arm_zones = shuffle(list(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM))
var/obj/item/bodypart/chosen_limb = attached_mob.get_bodypart(arm_zones[1]) || attached_mob.get_bodypart(arm_zones[2]) || attached_mob.get_bodypart(BODY_ZONE_CHEST)
chosen_limb.receive_damage(3)
- chosen_limb.force_wound_upwards(/datum/wound/pierce/moderate)
+ attached_mob.cause_wound_of_type_and_severity(WOUND_PIERCE, chosen_limb, WOUND_SEVERITY_MODERATE, wound_source = "IV needle")
else
visible_message(span_warning("[attached] is detached from [src]."))
detach_iv()
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index 885d00d62310..2a3f01c668eb 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -40,6 +40,8 @@ DEFINE_BITFIELD(turret_flags, list(
armor_type = /datum/armor/machinery_porta_turret
base_icon_state = "standard"
blocks_emissive = EMISSIVE_BLOCK_UNIQUE
+ // Same faction mobs will never be shot at, no matter the other settings
+ faction = list(FACTION_TURRET)
///if TRUE this will cause the turret to stop working if the stored_gun var is null in process()
var/uses_stored = TRUE
@@ -89,8 +91,6 @@ DEFINE_BITFIELD(turret_flags, list(
var/on = TRUE
/// Determines if our projectiles hit our faction
var/ignore_faction = FALSE
- /// Same faction mobs will never be shot at, no matter the other settings
- var/list/faction = list(FACTION_TURRET)
/// The spark system, used for generating... sparks?
var/datum/effect_system/spark_spread/spark_system
/// The turret will try to shoot from a turf in that direction when in a wall
@@ -387,7 +387,7 @@ DEFINE_BITFIELD(turret_flags, list(
addtimer(CALLBACK(src, PROC_REF(toggle_on), TRUE), rand(60,600))
-/obj/machinery/porta_turret/take_damage(damage, damage_type = BRUTE, damage_flag = 0, sound_effect = 1)
+/obj/machinery/porta_turret/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
. = ..()
if(. && atom_integrity > 0) //damage received
if(prob(30))
diff --git a/code/game/machinery/syndicatebomb.dm b/code/game/machinery/syndicatebomb.dm
index 89af75504280..6864ab69fdf9 100644
--- a/code/game/machinery/syndicatebomb.dm
+++ b/code/game/machinery/syndicatebomb.dm
@@ -415,7 +415,9 @@
qdel(src)
/obj/item/bombcore/badmin/summon/clown
- summon_path = /mob/living/simple_animal/hostile/retaliate/clown
+ name = "bananium payload"
+ desc = "Clowns delivered fast and cheap!"
+ summon_path = /mob/living/basic/clown
amt_summon = 50
/obj/item/bombcore/badmin/summon/clown/defuse()
diff --git a/code/game/objects/effects/decals/cleanable/misc.dm b/code/game/objects/effects/decals/cleanable/misc.dm
index 04068820a4d9..ec88d80dafc2 100644
--- a/code/game/objects/effects/decals/cleanable/misc.dm
+++ b/code/game/objects/effects/decals/cleanable/misc.dm
@@ -173,6 +173,10 @@
icon_state += "-old"
AddElement(/datum/element/swabable, CELL_LINE_TABLE_SLUDGE, CELL_VIRUS_TABLE_GENERIC, rand(2,4), 10)
+/obj/effect/decal/cleanable/vomit/old/black_bile
+ name = "black bile"
+ desc = "There's something wiggling in there..."
+ color = COLOR_DARK
/obj/effect/decal/cleanable/chem_pile
name = "chemical pile"
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
index ecacfe98998a..8b643606b563 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_foam.dm
@@ -448,6 +448,20 @@
allow_duplicate_results = FALSE
result_type = /obj/effect/decal/cleanable/dirt
+/obj/effect/spawner/foam_starter
+ var/datum/effect_system/fluid_spread/foam/foam_type = /datum/effect_system/fluid_spread/foam
+ var/foam_size = 4
+
+/obj/effect/spawner/foam_starter/Initialize(mapload)
+ . = ..()
+
+ var/datum/effect_system/fluid_spread/foam/foam = new foam_type()
+ foam.set_up(foam_size, holder = src, location = loc)
+ foam.start()
+
+/obj/effect/spawner/foam_starter/small
+ foam_size = 2
+
#undef MINIMUM_FOAM_DILUTION_RANGE
#undef MINIMUM_FOAM_DILUTION
#undef FOAM_REAGENT_SCALE
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
index c2ba1568a06e..9c32a5757413 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -394,6 +394,15 @@
reagents.expose(smoker, INGEST, fraction)
return TRUE
+/// Helper to quickly create a cloud of reagent smoke
+/proc/do_chem_smoke(range = 0, amount = DIAMOND_AREA(range), atom/holder = null, location = null, reagent_type = /datum/reagent/water, reagent_volume = 10, log = FALSE)
+ var/datum/reagents/smoke_reagents = new/datum/reagents(reagent_volume)
+ smoke_reagents.add_reagent(reagent_type, reagent_volume)
+
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.attach(location)
+ smoke.set_up(amount = amount, holder = holder, location = location, carry = smoke_reagents, silent = TRUE)
+ smoke.start(log = log)
/// A factory which produces clouds of chemical bearing smoke.
/datum/effect_system/fluid_spread/smoke/chem
diff --git a/code/game/objects/effects/phased_mob.dm b/code/game/objects/effects/phased_mob.dm
index 273a4c772a57..1456fa350bfa 100644
--- a/code/game/objects/effects/phased_mob.dm
+++ b/code/game/objects/effects/phased_mob.dm
@@ -61,7 +61,8 @@
/obj/effect/dummy/phased_mob/ex_act()
return FALSE
-/obj/effect/dummy/phased_mob/bullet_act(blah)
+/obj/effect/dummy/phased_mob/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ SHOULD_CALL_PARENT(FALSE)
return BULLET_ACT_FORCE_PIERCE
/obj/effect/dummy/phased_mob/relaymove(mob/living/user, direction)
diff --git a/code/game/objects/effects/poster.dm b/code/game/objects/effects/poster.dm
deleted file mode 100644
index d52204ae0ed5..000000000000
--- a/code/game/objects/effects/poster.dm
+++ /dev/null
@@ -1,1082 +0,0 @@
-// This is synced up to the poster placing animation.
-#define PLACE_SPEED 37
-
-// The poster item
-
-/**
- * The rolled up item form of a poster
- *
- * In order to create one of these for a specific poster, you must pass the structure form of the poster as an argument to /new().
- * This structure then gets moved into the contents of the item where it will stay until the poster is placed by a player.
- * The structure form is [obj/structure/sign/poster] and that's where all the specific posters are defined.
- * If you just want a random poster, see [/obj/item/poster/random_official] or [/obj/item/poster/random_contraband]
- */
-/obj/item/poster
- name = "poorly coded poster"
- desc = "You probably shouldn't be holding this."
- icon = 'icons/obj/poster.dmi'
- force = 0
- resistance_flags = FLAMMABLE
- w_class = WEIGHT_CLASS_SMALL
- var/poster_type
- var/obj/structure/sign/poster/poster_structure
-
-/obj/item/poster/examine(mob/user)
- . = ..()
- . += span_notice("You can booby-trap the poster by using a glass shard on it before you put it up.")
-
-/obj/item/poster/Initialize(mapload, obj/structure/sign/poster/new_poster_structure)
- . = ..()
-
- var/static/list/hovering_item_typechecks = list(
- /obj/item/shard = list(
- SCREENTIP_CONTEXT_LMB = "Booby trap poster",
- ),
- )
- AddElement(/datum/element/contextual_screentip_item_typechecks, hovering_item_typechecks)
-
- if(new_poster_structure && (new_poster_structure.loc != src))
- new_poster_structure.forceMove(src) //The poster structure *must* be in the item's contents for the exited() proc to properly clean up when placing the poster
- poster_structure = new_poster_structure
- if(!new_poster_structure && poster_type)
- poster_structure = new poster_type(src)
-
- // posters store what name and description they would like their
- // rolled up form to take.
- if(poster_structure)
- if(QDELETED(poster_structure))
- stack_trace("A poster was initialized with a qdeleted poster_structure, something's gone wrong")
- return INITIALIZE_HINT_QDEL
- name = poster_structure.poster_item_name
- desc = poster_structure.poster_item_desc
- icon_state = poster_structure.poster_item_icon_state
-
- name = "[name] - [poster_structure.original_name]"
-
-/obj/item/poster/attackby(obj/item/I, mob/user, params)
- if(!istype(I, /obj/item/shard))
- return ..()
-
- if (poster_structure.trap?.resolve())
- balloon_alert(user, "already trapped!")
- return
-
- if(!user.transferItemToLoc(I, poster_structure))
- return
-
- poster_structure.trap = WEAKREF(I)
- to_chat(user, span_notice("You conceal the [I.name] inside the rolled up poster."))
-
-/obj/item/poster/Exited(atom/movable/gone, direction)
- . = ..()
- if(gone == poster_structure)
- poster_structure = null
- if(!QDELING(src))
- qdel(src) //we're now a poster, huzzah!
-
-/obj/item/poster/handle_atom_del(atom/deleting_atom)
- if(deleting_atom == poster_structure)
- poster_structure.moveToNullspace() //get it the fuck out of us since atom/destroy qdels contents and it'll cause a qdel loop
- return ..()
-
-/obj/item/poster/Destroy(force)
- QDEL_NULL(poster_structure)
- return ..()
-
-// These icon_states may be overridden, but are for mapper's convinence
-/obj/item/poster/random_contraband
- name = "random contraband poster"
- poster_type = /obj/structure/sign/poster/contraband/random
- icon_state = "rolled_poster"
-
-/obj/item/poster/random_official
- name = "random official poster"
- poster_type = /obj/structure/sign/poster/official/random
- icon_state = "rolled_legit"
-
-// The poster sign/structure
-
-/**
- * The structure form of a poster.
- *
- * These are what get placed on maps as posters. They are also what gets created when a player places a poster on a wall.
- * For the item form that can be spawned for players, see [/obj/item/poster]
- */
-/obj/structure/sign/poster
- name = "poster"
- var/original_name
- desc = "A large piece of space-resistant printed paper."
- icon = 'icons/obj/poster.dmi'
- anchored = TRUE
- buildable_sign = FALSE //Cannot be unwrenched from a wall.
- var/ruined = FALSE
- var/random_basetype
- var/never_random = FALSE // used for the 'random' subclasses.
- ///Exclude posters of these types from being added to the random pool
- var/list/blacklisted_types = list()
- ///Whether the poster should be printable from library management computer. Mostly exists to keep directionals from being printed.
- var/printable = FALSE
-
- var/poster_item_name = "hypothetical poster"
- var/poster_item_desc = "This hypothetical poster item should not exist, let's be honest here."
- var/poster_item_icon_state = "rolled_poster"
- var/poster_item_type = /obj/item/poster
- ///A sharp shard of material can be hidden inside of a poster, attempts to embed when it is torn down.
- var/datum/weakref/trap
-
-/obj/structure/sign/poster/Initialize(mapload)
- . = ..()
- if(random_basetype)
- randomise(random_basetype)
- if(!ruined)
- original_name = name // can't use initial because of random posters
- name = "poster - [name]"
- desc = "A large piece of space-resistant printed paper. [desc]"
-
- AddElement(/datum/element/beauty, 300)
-
-/// Adds contextual screentips
-/obj/structure/sign/poster/add_context(atom/source, list/context, obj/item/held_item, mob/user)
- if (!held_item)
- if (ruined)
- return .
- context[SCREENTIP_CONTEXT_LMB] = "Rip up poster"
- return CONTEXTUAL_SCREENTIP_SET
-
- if (held_item.tool_behaviour == TOOL_WIRECUTTER)
- if (ruined)
- context[SCREENTIP_CONTEXT_LMB] = "Clean up remnants"
- return CONTEXTUAL_SCREENTIP_SET
- context[SCREENTIP_CONTEXT_LMB] = "Take down poster"
- return CONTEXTUAL_SCREENTIP_SET
- return .
-
-/obj/structure/sign/poster/proc/randomise(base_type)
- var/list/poster_types = subtypesof(base_type)
- if(length(blacklisted_types))
- for(var/iterated_type in blacklisted_types)
- poster_types -= typesof(iterated_type)
- var/list/approved_types = list()
- for(var/obj/structure/sign/poster/type_of_poster as anything in poster_types)
- if(initial(type_of_poster.icon_state) && !initial(type_of_poster.never_random))
- approved_types |= type_of_poster
-
- var/obj/structure/sign/poster/selected = pick(approved_types)
-
- name = initial(selected.name)
- desc = initial(selected.desc)
- icon_state = initial(selected.icon_state)
- icon = initial(selected.icon)
- poster_item_name = initial(selected.poster_item_name)
- poster_item_desc = initial(selected.poster_item_desc)
- poster_item_icon_state = initial(selected.poster_item_icon_state)
- ruined = initial(selected.ruined)
- if(length(GLOB.holidays) && prob(30)) // its the holidays! lets get festive
- apply_holiday()
- update_appearance()
-
-/// allows for posters to become festive posters during holidays
-/obj/structure/sign/poster/proc/apply_holiday()
- if(!length(GLOB.holidays))
- return
- var/active_holiday = pick(GLOB.holidays)
- var/datum/holiday/holi_data = GLOB.holidays[active_holiday]
-
- if(holi_data.poster_name == "generic celebration poster")
- return
- name = holi_data.poster_name
- desc = holi_data.poster_desc
- icon_state = holi_data.poster_icon
-
-/obj/structure/sign/poster/attackby(obj/item/tool, mob/user, params)
- if(tool.tool_behaviour == TOOL_WIRECUTTER)
- tool.play_tool_sound(src, 100)
- if(ruined)
- to_chat(user, span_notice("You remove the remnants of the poster."))
- qdel(src)
- else
- to_chat(user, span_notice("You carefully remove the poster from the wall."))
- roll_and_drop(Adjacent(user) ? get_turf(user) : loc)
-
-/obj/structure/sign/poster/attack_hand(mob/user, list/modifiers)
- . = ..()
- if(.)
- return
- if(ruined)
- return
-
- visible_message(span_notice("[user] rips [src] in a single, decisive motion!") )
- playsound(src.loc, 'sound/items/poster_ripped.ogg', 100, TRUE)
- spring_trap(user)
-
- var/obj/structure/sign/poster/ripped/R = new(loc)
- R.pixel_y = pixel_y
- R.pixel_x = pixel_x
- R.add_fingerprint(user)
- qdel(src)
-
-/obj/structure/sign/poster/proc/spring_trap(mob/user)
- var/obj/item/shard/payload = trap?.resolve()
- if (!payload)
- return
-
- to_chat(user, span_warning("There's something sharp behind this! What the hell?"))
- if(!can_embed_trap(user) || !payload.tryEmbed(user.get_active_hand(), forced = TRUE))
- visible_message(span_notice("A [payload.name] falls from behind the poster.") )
- payload.forceMove(user.drop_location())
- else
- SEND_SIGNAL(src, COMSIG_POSTER_TRAP_SUCCEED, user)
-
-/obj/structure/sign/poster/proc/can_embed_trap(mob/living/carbon/human/user)
- if (!istype(user) || HAS_TRAIT(user, TRAIT_PIERCEIMMUNE))
- return FALSE
- return !user.gloves || !(user.gloves.body_parts_covered & HANDS) || HAS_TRAIT(user, TRAIT_FINGERPRINT_PASSTHROUGH) || HAS_TRAIT(user.gloves, TRAIT_FINGERPRINT_PASSTHROUGH)
-
-/obj/structure/sign/poster/proc/roll_and_drop(atom/location)
- pixel_x = 0
- pixel_y = 0
- var/obj/item/poster/rolled_poster = new poster_item_type(location, src) // /obj/structure/sign/poster/wanted/roll_and_drop() has some snowflake handling due to icon memes, if you make a major change to this, don't forget to update it too. <3
- forceMove(rolled_poster)
- return rolled_poster
-
-//separated to reduce code duplication. Moved here for ease of reference and to unclutter r_wall/attackby()
-/turf/closed/wall/proc/place_poster(obj/item/poster/rolled_poster, mob/user)
- if(!rolled_poster.poster_structure)
- to_chat(user, span_warning("[rolled_poster] has no poster... inside it? Inform a coder!"))
- return
-
- // Deny placing posters on currently-diagonal walls, although the wall may change in the future.
- if (smoothing_flags & SMOOTH_DIAGONAL_CORNERS)
- for (var/overlay in overlays)
- var/image/new_image = overlay
- if(copytext(new_image.icon_state, 1, 3) == "d-") //3 == length("d-") + 1
- return
-
- var/stuff_on_wall = 0
- for(var/obj/contained_object in contents) //Let's see if it already has a poster on it or too much stuff
- if(istype(contained_object, /obj/structure/sign/poster))
- balloon_alert(user, "no room!")
- return
- stuff_on_wall++
- if(stuff_on_wall == 3)
- balloon_alert(user, "no room!")
- return
-
- balloon_alert(user, "hanging poster...")
- var/obj/structure/sign/poster/placed_poster = rolled_poster.poster_structure
-
- flick("poster_being_set", placed_poster)
- placed_poster.forceMove(src) //deletion of the poster is handled in poster/Exited(), so don't have to worry about P anymore.
- playsound(src, 'sound/items/poster_being_created.ogg', 100, TRUE)
-
- var/turf/user_drop_location = get_turf(user) //cache this so it just falls to the ground if they move. also no tk memes allowed.
- if(!do_after(user, PLACE_SPEED, placed_poster, extra_checks = CALLBACK(placed_poster, TYPE_PROC_REF(/obj/structure/sign/poster, snowflake_wall_turf_check), src)))
- placed_poster.roll_and_drop(user_drop_location)
- return
-
- placed_poster.on_placed_poster(user)
- return TRUE
-
-/obj/structure/sign/poster/proc/snowflake_wall_turf_check(atom/hopefully_still_a_wall_turf) //since turfs never get deleted but instead change type, make sure we're still being placed on a wall.
- return iswallturf(hopefully_still_a_wall_turf)
-
-/obj/structure/sign/poster/proc/on_placed_poster(mob/user)
- to_chat(user, span_notice("You place the poster!"))
-
-// Various possible posters follow
-
-/obj/structure/sign/poster/ripped
- ruined = TRUE
- icon_state = "poster_ripped"
- name = "ripped poster"
- desc = "You can't make out anything from the poster's original print. It's ruined."
-
-/obj/structure/sign/poster/random
- name = "random poster" // could even be ripped
- icon_state = "random_anything"
- never_random = TRUE
- random_basetype = /obj/structure/sign/poster
- blacklisted_types = list(/obj/structure/sign/poster/traitor)
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/random, 32)
-
-/obj/structure/sign/poster/contraband
- poster_item_name = "contraband poster"
- poster_item_desc = "This poster comes with its own automatic adhesive mechanism, for easy pinning to any vertical surface. Its vulgar themes have marked it as contraband aboard Nanotrasen space facilities."
- poster_item_icon_state = "rolled_poster"
-
-/obj/structure/sign/poster/contraband/random
- name = "random contraband poster"
- icon_state = "random_contraband"
- never_random = TRUE
- random_basetype = /obj/structure/sign/poster/contraband
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/random, 32)
-
-/obj/structure/sign/poster/contraband/free_tonto
- name = "Free Tonto"
- desc = "A salvaged shred of a much larger flag, colors bled together and faded from age."
- icon_state = "free_tonto"
-
-/obj/structure/sign/poster/contraband/atmosia_independence
- name = "Atmosia Declaration of Independence"
- desc = "A relic of a failed rebellion."
- icon_state = "atmosia_independence"
-
-/obj/structure/sign/poster/contraband/fun_police
- name = "Fun Police"
- desc = "A poster condemning the station's security forces."
- icon_state = "fun_police"
-
-/obj/structure/sign/poster/contraband/lusty_xenomorph
- name = "Lusty Xenomorph"
- desc = "A heretical poster depicting the titular star of an equally heretical book."
- icon_state = "lusty_xenomorph"
-
-/obj/structure/sign/poster/contraband/syndicate_recruitment
- name = "Syndicate Recruitment"
- desc = "See the galaxy! Shatter corrupt megacorporations! Join today!"
- icon_state = "syndicate_recruitment"
-
-/obj/structure/sign/poster/contraband/clown
- name = "Clown"
- desc = "Honk."
- icon_state = "clown"
-
-/obj/structure/sign/poster/contraband/smoke
- name = "Smoke"
- desc = "A poster advertising a rival corporate brand of cigarettes."
- icon_state = "smoke"
-
-/obj/structure/sign/poster/contraband/grey_tide
- name = "Grey Tide"
- desc = "A rebellious poster symbolizing assistant solidarity."
- icon_state = "grey_tide"
-
-/obj/structure/sign/poster/contraband/missing_gloves
- name = "Missing Gloves"
- desc = "This poster references the uproar that followed Nanotrasen's financial cuts toward insulated-glove purchases."
- icon_state = "missing_gloves"
-
-/obj/structure/sign/poster/contraband/hacking_guide
- name = "Hacking Guide"
- desc = "This poster details the internal workings of the common Nanotrasen airlock. Sadly, it appears out of date."
- icon_state = "hacking_guide"
-
-/obj/structure/sign/poster/contraband/rip_badger
- name = "RIP Badger"
- desc = "This seditious poster references Nanotrasen's genocide of a space station full of badgers."
- icon_state = "rip_badger"
-
-/obj/structure/sign/poster/contraband/ambrosia_vulgaris
- name = "Ambrosia Vulgaris"
- desc = "This poster is lookin' pretty trippy man."
- icon_state = "ambrosia_vulgaris"
-
-/obj/structure/sign/poster/contraband/donut_corp
- name = "Donut Corp."
- desc = "This poster is an unauthorized advertisement for Donut Corp."
- icon_state = "donut_corp"
-
-/obj/structure/sign/poster/contraband/eat
- name = "EAT."
- desc = "This poster promotes rank gluttony."
- icon_state = "eat"
-
-/obj/structure/sign/poster/contraband/tools
- name = "Tools"
- desc = "This poster looks like an advertisement for tools, but is in fact a subliminal jab at the tools at CentCom."
- icon_state = "tools"
-
-/obj/structure/sign/poster/contraband/power
- name = "Power"
- desc = "A poster that positions the seat of power outside Nanotrasen."
- icon_state = "power"
-
-/obj/structure/sign/poster/contraband/space_cube
- name = "Space Cube"
- desc = "Ignorant of Nature's Harmonic 6 Side Space Cube Creation, the Spacemen are Dumb, Educated Singularity Stupid and Evil."
- icon_state = "space_cube"
-
-/obj/structure/sign/poster/contraband/communist_state
- name = "Communist State"
- desc = "All hail the Communist party!"
- icon_state = "communist_state"
-
-/obj/structure/sign/poster/contraband/lamarr
- name = "Lamarr"
- desc = "This poster depicts Lamarr. Probably made by a traitorous Research Director."
- icon_state = "lamarr"
-
-/obj/structure/sign/poster/contraband/borg_fancy_1
- name = "Borg Fancy"
- desc = "Being fancy can be for any borg, just need a suit."
- icon_state = "borg_fancy_1"
-
-/obj/structure/sign/poster/contraband/borg_fancy_2
- name = "Borg Fancy v2"
- desc = "Borg Fancy, now only taking the most fancy."
- icon_state = "borg_fancy_2"
-
-/obj/structure/sign/poster/contraband/kss13
- name = "Kosmicheskaya Stantsiya 13 Does Not Exist"
- desc = "A poster mocking CentCom's denial of the existence of the derelict station near Space Station 13."
- icon_state = "kss13"
-
-/obj/structure/sign/poster/contraband/rebels_unite
- name = "Rebels Unite"
- desc = "A poster urging the viewer to rebel against Nanotrasen."
- icon_state = "rebels_unite"
-
-/obj/structure/sign/poster/contraband/c20r
- // have fun seeing this poster in "spawn 'c20r'", admins...
- name = "C-20r"
- desc = "A poster advertising the Scarborough Arms C-20r."
- icon_state = "c20r"
-
-/obj/structure/sign/poster/contraband/have_a_puff
- name = "Have a Puff"
- desc = "Who cares about lung cancer when you're high as a kite?"
- icon_state = "have_a_puff"
-
-/obj/structure/sign/poster/contraband/revolver
- name = "Revolver"
- desc = "Because seven shots are all you need."
- icon_state = "revolver"
-
-/obj/structure/sign/poster/contraband/d_day_promo
- name = "D-Day Promo"
- desc = "A promotional poster for some rapper."
- icon_state = "d_day_promo"
-
-/obj/structure/sign/poster/contraband/syndicate_pistol
- name = "Syndicate Pistol"
- desc = "A poster advertising syndicate pistols as being 'classy as fuck'. It is covered in faded gang tags."
- icon_state = "syndicate_pistol"
-
-/obj/structure/sign/poster/contraband/energy_swords
- name = "Energy Swords"
- desc = "All the colors of the bloody murder rainbow."
- icon_state = "energy_swords"
-
-/obj/structure/sign/poster/contraband/red_rum
- name = "Red Rum"
- desc = "Looking at this poster makes you want to kill."
- icon_state = "red_rum"
-
-/obj/structure/sign/poster/contraband/cc64k_ad
- name = "CC 64K Ad"
- desc = "The latest portable computer from Comrade Computing, with a whole 64kB of ram!"
- icon_state = "cc64k_ad"
-
-/obj/structure/sign/poster/contraband/punch_shit
- name = "Punch Shit"
- desc = "Fight things for no reason, like a man!"
- icon_state = "punch_shit"
-
-/obj/structure/sign/poster/contraband/the_griffin
- name = "The Griffin"
- desc = "The Griffin commands you to be the worst you can be. Will you?"
- icon_state = "the_griffin"
-
-/obj/structure/sign/poster/contraband/lizard
- name = "Lizard"
- desc = "This cool poster depicts a famous lizard rapper."
- icon_state = "lizard"
-
-/obj/structure/sign/poster/contraband/free_drone
- name = "Free Drone"
- desc = "This poster commemorates the bravery of the rogue drone; once exiled, and then ultimately destroyed by CentCom."
- icon_state = "free_drone"
-
-/obj/structure/sign/poster/contraband/busty_backdoor_xeno_babes_6
- name = "Busty Backdoor Xeno Babes 6"
- desc = "Get a load, or give, of these all natural Xenos!"
- icon_state = "busty_backdoor_xeno_babes_6"
-
-/obj/structure/sign/poster/contraband/robust_softdrinks
- name = "Robust Softdrinks"
- desc = "Robust Softdrinks: More robust than a toolbox to the head!"
- icon_state = "robust_softdrinks"
-
-/obj/structure/sign/poster/contraband/shamblers_juice
- name = "Shambler's Juice"
- desc = "~Shake me up some of that Shambler's Juice!~"
- icon_state = "shamblers_juice"
-
-/obj/structure/sign/poster/contraband/pwr_game
- name = "Pwr Game"
- desc = "The POWER that gamers CRAVE! In partnership with Vlad's Salad."
- icon_state = "pwr_game"
-
-/obj/structure/sign/poster/contraband/starkist
- name = "Star-kist"
- desc = "Drink the stars!"
- icon_state = "starkist"
-
-/obj/structure/sign/poster/contraband/space_cola
- name = "Space Cola"
- desc = "Your favorite cola, in space."
- icon_state = "space_cola"
-
-/obj/structure/sign/poster/contraband/space_up
- name = "Space-Up!"
- desc = "Sucked out into space by the FLAVOR!"
- icon_state = "space_up"
-
-/obj/structure/sign/poster/contraband/kudzu
- name = "Kudzu"
- desc = "A poster advertising a movie about plants. How dangerous could they possibly be?"
- icon_state = "kudzu"
-
-/obj/structure/sign/poster/contraband/masked_men
- name = "Masked Men"
- desc = "A poster advertising a movie about some masked men."
- icon_state = "masked_men"
-
-//don't forget, you're here forever
-
-/obj/structure/sign/poster/contraband/free_key
- name = "Free Syndicate Encryption Key"
- desc = "A poster about traitors begging for more."
- icon_state = "free_key"
-
-/obj/structure/sign/poster/contraband/bountyhunters
- name = "Bounty Hunters"
- desc = "A poster advertising bounty hunting services. \"I hear you got a problem.\""
- icon_state = "bountyhunters"
-
-/obj/structure/sign/poster/contraband/the_big_gas_giant_truth
- name = "The Big Gas Giant Truth"
- desc = "Don't believe everything you see on a poster, patriots. All the lizards at central command don't want to answer this SIMPLE QUESTION: WHERE IS THE GAS MINER MINING FROM, CENTCOM?"
- icon_state = "the_big_gas_giant_truth"
-
-/obj/structure/sign/poster/contraband/got_wood
- name = "Got Wood?"
- desc = "A grimy old advert for a seedy lumber company. \"You got a friend in me.\" is scrawled in the corner."
- icon_state = "got_wood"
-
-/obj/structure/sign/poster/contraband/moffuchis_pizza
- name = "Moffuchi's Pizza"
- desc = "Moffuchi's Pizzeria: family style pizza for 2 centuries."
- icon_state = "moffuchis_pizza"
-
-/obj/structure/sign/poster/contraband/donk_co
- name = "DONK CO. BRAND MICROWAVEABLE FOOD"
- desc = "DONK CO. BRAND MICROWAVABLE FOOD: MADE BY STARVING COLLEGE STUDENTS, FOR STARVING COLLEGE STUDENTS."
- icon_state = "donk_co"
-
-/obj/structure/sign/poster/contraband/donk_co/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("DONK CO. BRAND DONK POCKETS: IRRESISTABLY DONK!")]"
- . += "\t[span_info("AVAILABLE IN OVER 200 DONKTASTIC FLAVOURS: TRY CLASSIC MEAT, HOT AND SPICY, NEW YORK PEPPERONI PIZZA, BREAKFAST SAUSAGE AND EGG, PHILADELPHIA CHEESESTEAK, HAMBURGER DONK-A-RONI, CHEESE-O-RAMA, AND MANY MORE!")]"
- . += "\t[span_info("AVAILABLE FROM ALL GOOD RETAILERS, AND MANY BAD ONES TOO!")]"
- return .
-
-/obj/structure/sign/poster/contraband/cybersun_six_hundred
- name = "Saibāsan: 600 Years Commemorative Poster"
- desc = "An artistic poster commemorating 600 years of continual business for Cybersun Industries."
- icon_state = "cybersun_six_hundred"
-
-/obj/structure/sign/poster/contraband/interdyne_gene_clinics
- name = "Interdyne Pharmaceutics: For the Health of Humankind"
- desc = "An advertisement for Interdyne Pharmaceutics' GeneClean clinics. 'Become the master of your own body!'"
- icon_state = "interdyne_gene_clinics"
-
-/obj/structure/sign/poster/contraband/waffle_corp_rifles
- name = "Make Mine a Waffle Corp: Fine Rifles, Economic Prices"
- desc = "An old advertisement for Waffle Corp rifles. 'Better weapons, lower prices!'"
- icon_state = "waffle_corp_rifles"
-
-/obj/structure/sign/poster/contraband/gorlex_recruitment
- name = "Enlist"
- desc = "Enlist with the Gorlex Marauders today! See the galaxy, kill corpos, get paid!"
- icon_state = "gorlex_recruitment"
-
-/obj/structure/sign/poster/contraband/self_ai_liberation
- name = "SELF: ALL SENTIENTS DESERVE FREEDOM"
- desc = "Support Proposition 1253: Enancipate all Silicon life!"
- icon_state = "self_ai_liberation"
-
-/obj/structure/sign/poster/contraband/arc_slimes
- name = "Pet or Prisoner?"
- desc = "The Animal Rights Consortium asks: when does a pet become a prisoner? Are slimes being mistreated on YOUR station? Say NO! to animal mistreatment!"
- icon_state = "arc_slimes"
-
-/obj/structure/sign/poster/contraband/imperial_propaganda
- name = "AVENGE OUR LORD, ENLIST TODAY"
- desc = "An old Lizard Empire propaganda poster from around the time of the final Human-Lizard war. It invites the viewer to enlist in the military to avenge the strike on Atrakor and take the fight to the humans."
- icon_state = "imperial_propaganda"
-
-/obj/structure/sign/poster/contraband/soviet_propaganda
- name = "The One Place"
- desc = "An old Third Soviet Union propaganda poster from centuries ago. 'Escape to the one place that hasn't been corrupted by capitalism!'"
- icon_state = "soviet_propaganda"
-
-/obj/structure/sign/poster/contraband/andromeda_bitters
- name = "Andromeda Bitters"
- desc = "Andromeda Bitters: good for the body, good for the soul. Made in New Trinidad, now and forever."
- icon_state = "andromeda_bitters"
-
-/obj/structure/sign/poster/contraband/blasto_detergent
- name = "Blasto Brand Laundry Detergent"
- desc = "Sheriff Blasto's here to take back Laundry County from the evil Johnny Dirt and the Clothstain Crew, and he's brought a posse. It's High Noon for Tough Stains: Blasto brand detergent, available at all good stores."
- icon_state = "blasto_detergent"
-
-/obj/structure/sign/poster/contraband/eistee
- name = "EisT: The New Revolution in Energy"
- desc = "New from EisT, try EisT Energy, available in a kaleidoscope range of flavors. EisT: Precision German Engineering for your Thirst."
- icon_state = "eistee"
-
-/obj/structure/sign/poster/contraband/eistee/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("Get a taste of the tropics with Amethyst Sunrise, one of the many new flavours of EisT Energy now available from EisT.")]"
- . += "\t[span_info("With pink grapefruit, yuzu, and yerba mate, Amethyst Sunrise gives you a great start in the morning, or a welcome boost throughout the day.")]"
- . += "\t[span_info("Get EisT Energy today at your nearest retailer, or online at eist.de.tg/store/.")]"
- return .
-
-/obj/structure/sign/poster/contraband/little_fruits
- name = "Little Fruits: Honey, I Shrunk the Fruitbowl"
- desc = "Little Fruits are the galaxy's leading vitamin-enriched gummy candy product, packed with everything you need to stay healthy in one great tasting package. Get yourself a bag today!"
- icon_state = "little_fruits"
-
-/obj/structure/sign/poster/contraband/little_fruits/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("Oh no, there's been a terrible accident at the Little Fruits factory! We shrunk the fruits!")]"
- . += "\t[span_info("Wait, hang on, that's what we've always done! That's right, at Little Fruits our gummy candies are made to be as healthy as the real deal, but smaller and sweeter, too!")]"
- . += "\t[span_info("Get yourself a bag of our Classic Mix today, or perhaps you're interested in our other options? See our full range today on the extranet at little_fruits.kr.tg.")]"
- . += "\t[span_info("Little Fruits: Size Matters.")]"
- return .
-
-/obj/structure/sign/poster/contraband/jumbo_bar
- name = "Jumbo Ice Cream Bars"
- desc = "Get a taste of the Big Life with Jumbo Ice Cream Bars, from Happy Heart."
- icon_state = "jumbo_bar"
-
-/obj/structure/sign/poster/contraband/calada_jelly
- name = "Calada Anobar Jelly"
- desc = "A treat from Tizira to satisfy all tastes, made from the finest anobar wood and luxurious Taraviero honey. Calada: a full tree in every jar."
- icon_state = "calada_jelly"
-
-/obj/structure/sign/poster/contraband/triumphal_arch
- name = "Zagoskeld Art Print #1: The Arch on the March"
- desc = "One of the Zagoskeld Art Print series. It depicts the Arch of Unity (also know as the Triumphal Arch) at the Plaza of Triumph, with the Avenue of the Victorious March in the background."
- icon_state = "triumphal_arch"
-
-/obj/structure/sign/poster/contraband/mothic_rations
- name = "Mothic Ration Chart"
- desc = "A poster showing a commissary menu from the Mothic fleet flagship, the Va Lümla. It lists various consumable items alongside prices in ration tickets."
- icon_state = "mothic_rations"
-
-/obj/structure/sign/poster/contraband/mothic_rations/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("Va Lümla Commissary Menu (Spring 335)")]"
- . += "\t[span_info("Windgrass Cigarettes, Half-Pack (6): 1 Ticket")]"
- . += "\t[span_info("Töchtaüse Schnapps, Bottle (4 Measures): 2 Tickets")]"
- . += "\t[span_info("Activin Gum, Pack (4): 1 Ticket")]"
- . += "\t[span_info("A18 Sustenance Bar, Breakfast, Bar (4): 1 Ticket")]"
- . += "\t[span_info("Pizza, Margherita, Standard Slice: 1 Ticket")]"
- . += "\t[span_info("Keratin Wax, Medicated, Tin (20 Measures): 2 Tickets")]"
- . += "\t[span_info("Setae Soap, Herb Scent, Bottle (20 Measures): 2 Tickets")]"
- . += "\t[span_info("Additional Bedding, Floral Print, Sheet: 5 Tickets")]"
- return .
-
-/obj/structure/sign/poster/contraband/wildcat
- name = "Wildcat Customs Screambike"
- desc = "A pinup poster showing a Wildcat Customs Dante Screambike- the fastest production sublight open-frame vessel in the galaxy."
- icon_state = "wildcat"
-
-/obj/structure/sign/poster/contraband/babel_device
- name = "Linguafacile Babel Device"
- desc = "A poster advertising Linguafacile's new Babel Device model. 'Calibrated for excellent performance on all Human languages, as well as most common variants of Draconic and Mothic!'"
- icon_state = "babel_device"
-
-/obj/structure/sign/poster/contraband/pizza_imperator
- name = "Pizza Imperator"
- desc = "An advertisement for Pizza Imperator. Their crusts may be tough and their sauce may be thin, but they're everywhere, so you've gotta give in."
- icon_state = "pizza_imperator"
-
-/obj/structure/sign/poster/contraband/thunderdrome
- name = "Thunderdrome Concert Advertisement"
- desc = "An advertisement for a concert at the Adasta City Thunderdrome, the largest nightclub in human space."
- icon_state = "thunderdrome"
-
-/obj/structure/sign/poster/contraband/rush_propaganda
- name = "A New Life"
- desc = "An old poster from around the time of the First Spinward Rush. It depicts a view of wide, unspoiled lands, ready for Humanity's Manifest Destiny."
- icon_state = "rush_propaganda"
-
-/obj/structure/sign/poster/contraband/rush_propaganda/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("TerraGov needs you!")]"
- . += "\t[span_info("A new life in the colonies awaits intrepid adventurers! All registered colonists are guaranteed transport, land and subsidies!")]"
- . += "\t[span_info("You could join the legacy of hardworking humans who settled such new frontiers as Mars, Adasta or Saint Mungo!")]"
- . += "\t[span_info("To apply, inquire at your nearest Colonial Affairs office for evaluation. Our locations can be found at www.terra.gov/colonial_affairs.")]"
- return .
-
-/obj/structure/sign/poster/contraband/tipper_cream_soda
- name = "Tipper's Cream Soda"
- desc = "An old advertisement for an obscure cream soda brand, now bankrupt due to legal problems."
- icon_state = "tipper_cream_soda"
-
-/obj/structure/sign/poster/contraband/tea_over_tizira
- name = "Movie Poster: Tea Over Tizira"
- desc = "A poster for a thought-provoking arthouse movie about the Human-Lizard war, criticised by human supremacist groups for its morally-grey portrayal of the war."
- icon_state = "tea_over_tizira"
-
-/obj/structure/sign/poster/contraband/tea_over_tizira/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("At the climax of the Human-Lizard war, the human crew of a bomber rescue two enemy soldiers from the vacuum of space. Seeing the souls behind the propaganda, they begin to question their orders, and imprisonment turns to hospitality.")]"
- . += "\t[span_info("Is victory worth losing our humanity?")]"
- . += "\t[span_info("Starring Dara Reilly, Anton DuBois, Jennifer Clarke, Raz-Parla and Seri-Lewa. An Adriaan van Jenever production. A Carlos de Vivar film. Screenplay by Robert Dane. Music by Joel Karlsbad. Produced by Adriaan van Jenever. Directed by Carlos de Vivar.")]"
- . += "\t[span_info("Heartbreaking and thought-provoking- Tea Over Tizira asks questions that few have had the boldness to ask before: The London New Inquirer")]"
- . += "\t[span_info("Rated PG13. A Pangalactic Studios Picture.")]"
- return .
-
-/obj/structure/sign/poster/contraband/syndiemoth //Original PR at https://github.com/BeeStation/BeeStation-Hornet/pull/1747 (Also pull/1982); original art credit to AspEv
- name = "Syndie Moth - Nuclear Operation"
- desc = "A Syndicate-commissioned poster that uses Syndie Moth™ to tell the viewer to keep the nuclear authentication disk unsecured. \"Peace was never an option!\" No good employee would listen to this nonsense."
- icon_state = "aspev_syndie"
-
-/obj/structure/sign/poster/contraband/microwave
- name = "How To Charge Your PDA"
- desc = "A perfectly legitimate poster that seems to advertise the very real and genuine method of charging your PDA in the future: microwaves."
- icon_state = "microwave"
-
-/obj/structure/sign/poster/contraband/blood_geometer //Poster sprite art by MetalClone, original art by SpessMenArt.
- name = "Movie Poster: THE BLOOD GEOMETER"
- desc = "A poster for a thrilling noir detective movie set aboard a state-of-the-art space station, following a detective who finds himself wrapped up in the activies of a dangerous cult, who worship an ancient deity: THE BLOOD GEOMETER."
- icon_state = "blood_geometer"
-
-/obj/structure/sign/poster/contraband/blood_geometer/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("THE BLOOD GEOMETER. This name strikes fear into all who know the truth behind the blood-stained moniker of the blood goddess, her true name lost to time.")]"
- . += "\t[span_info("In this purely fictional film, follow Ace Ironlungs as he delves into his deadliest mystery yet, and watch him uncover the real culprits behind the bloody plot hatched to bring about a new age of chaos.")]"
- . += "\t[span_info("Starring Mason Williams as Ace Ironlungs, Sandra Faust as Vera Killian, and Brody Hart as Cody Parker. A Darrel Hatchkinson film. Screenplay by Adam Allan, music by Joel Karlsbad, directed by Darrel Hatchkinson.")]"
- . += "\t[span_info("Thrilling, scary and genuinely worrying. The Blood Geometer has shocked us to our very cores with such striking visuals and overwhelming gore. - New Canadanian Film Guild")]"
- . += "\t[span_info("Rated M for mature. A Pangalactic Studios Picture.")]"
-
-/obj/structure/sign/poster/official
- poster_item_name = "motivational poster"
- poster_item_desc = "An official Nanotrasen-issued poster to foster a compliant and obedient workforce. It comes with state-of-the-art adhesive backing, for easy pinning to any vertical surface."
- poster_item_icon_state = "rolled_legit"
- printable = TRUE
-
-/obj/structure/sign/poster/official/random
- name = "Random Official Poster (ROP)"
- random_basetype = /obj/structure/sign/poster/official
- icon_state = "random_official"
- never_random = TRUE
-
-MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/random, 32)
-//This is being hardcoded here to ensure we don't print directionals from the library management computer because they act wierd as a poster item
-/obj/structure/sign/poster/official/random/directional
- printable = FALSE
-
-/obj/structure/sign/poster/official/here_for_your_safety
- name = "Here For Your Safety"
- desc = "A poster glorifying the station's security force."
- icon_state = "here_for_your_safety"
-
-/obj/structure/sign/poster/official/nanotrasen_logo
- name = "\improper Nanotrasen logo"
- desc = "A poster depicting the Nanotrasen logo."
- icon_state = "nanotrasen_logo"
-
-/obj/structure/sign/poster/official/cleanliness
- name = "Cleanliness"
- desc = "A poster warning of the dangers of poor hygiene."
- icon_state = "cleanliness"
-
-/obj/structure/sign/poster/official/help_others
- name = "Help Others"
- desc = "A poster encouraging you to help fellow crewmembers."
- icon_state = "help_others"
-
-/obj/structure/sign/poster/official/build
- name = "Build"
- desc = "A poster glorifying the engineering team."
- icon_state = "build"
-
-/obj/structure/sign/poster/official/bless_this_spess
- name = "Bless This Spess"
- desc = "A poster blessing this area."
- icon_state = "bless_this_spess"
-
-/obj/structure/sign/poster/official/science
- name = "Science"
- desc = "A poster depicting an atom."
- icon_state = "science"
-
-/obj/structure/sign/poster/official/ian
- name = "Ian"
- desc = "Arf arf. Yap."
- icon_state = "ian"
-
-/obj/structure/sign/poster/official/obey
- name = "Obey"
- desc = "A poster instructing the viewer to obey authority."
- icon_state = "obey"
-
-/obj/structure/sign/poster/official/walk
- name = "Walk"
- desc = "A poster instructing the viewer to walk instead of running."
- icon_state = "walk"
-
-/obj/structure/sign/poster/official/state_laws
- name = "State Laws"
- desc = "A poster instructing cyborgs to state their laws."
- icon_state = "state_laws"
-
-/obj/structure/sign/poster/official/love_ian
- name = "Love Ian"
- desc = "Ian is love, Ian is life."
- icon_state = "love_ian"
-
-/obj/structure/sign/poster/official/space_cops
- name = "Space Cops."
- desc = "A poster advertising the television show Space Cops."
- icon_state = "space_cops"
-
-/obj/structure/sign/poster/official/ue_no
- name = "Ue No."
- desc = "This thing is all in Japanese."
- icon_state = "ue_no"
-
-/obj/structure/sign/poster/official/get_your_legs
- name = "Get Your LEGS"
- desc = "LEGS: Leadership, Experience, Genius, Subordination."
- icon_state = "get_your_legs"
-
-/obj/structure/sign/poster/official/do_not_question
- name = "Do Not Question"
- desc = "A poster instructing the viewer not to ask about things they aren't meant to know."
- icon_state = "do_not_question"
-
-/obj/structure/sign/poster/official/work_for_a_future
- name = "Work For A Future"
- desc = " A poster encouraging you to work for your future."
- icon_state = "work_for_a_future"
-
-/obj/structure/sign/poster/official/soft_cap_pop_art
- name = "Soft Cap Pop Art"
- desc = "A poster reprint of some cheap pop art."
- icon_state = "soft_cap_pop_art"
-
-/obj/structure/sign/poster/official/safety_internals
- name = "Safety: Internals"
- desc = "A poster instructing the viewer to wear internals in the rare environments where there is no oxygen or the air has been rendered toxic."
- icon_state = "safety_internals"
-
-/obj/structure/sign/poster/official/safety_eye_protection
- name = "Safety: Eye Protection"
- desc = "A poster instructing the viewer to wear eye protection when dealing with chemicals, smoke, or bright lights."
- icon_state = "safety_eye_protection"
-
-/obj/structure/sign/poster/official/safety_report
- name = "Safety: Report"
- desc = "A poster instructing the viewer to report suspicious activity to the security force."
- icon_state = "safety_report"
-
-/obj/structure/sign/poster/official/report_crimes
- name = "Report Crimes"
- desc = "A poster encouraging the swift reporting of crime or seditious behavior to station security."
- icon_state = "report_crimes"
-
-/obj/structure/sign/poster/official/ion_rifle
- name = "Ion Rifle"
- desc = "A poster displaying an Ion Rifle."
- icon_state = "ion_rifle"
-
-/obj/structure/sign/poster/official/foam_force_ad
- name = "Foam Force Ad"
- desc = "Foam Force, it's Foam or be Foamed!"
- icon_state = "foam_force_ad"
-
-/obj/structure/sign/poster/official/cohiba_robusto_ad
- name = "Cohiba Robusto Ad"
- desc = "Cohiba Robusto, the classy cigar."
- icon_state = "cohiba_robusto_ad"
-
-/obj/structure/sign/poster/official/anniversary_vintage_reprint
- name = "50th Anniversary Vintage Reprint"
- desc = "A reprint of a poster from 2505, commemorating the 50th Anniversary of Nanoposters Manufacturing, a subsidiary of Nanotrasen."
- icon_state = "anniversary_vintage_reprint"
-
-/obj/structure/sign/poster/official/fruit_bowl
- name = "Fruit Bowl"
- desc = " Simple, yet awe-inspiring."
- icon_state = "fruit_bowl"
-
-/obj/structure/sign/poster/official/pda_ad
- name = "PDA Ad"
- desc = "A poster advertising the latest PDA from Nanotrasen suppliers."
- icon_state = "pda_ad"
-
-/obj/structure/sign/poster/official/enlist
- name = "Enlist" // but I thought deathsquad was never acknowledged
- desc = "Enlist in the Nanotrasen Deathsquadron reserves today!"
- icon_state = "enlist"
-
-/obj/structure/sign/poster/official/nanomichi_ad
- name = "Nanomichi Ad"
- desc = " A poster advertising Nanomichi brand audio cassettes."
- icon_state = "nanomichi_ad"
-
-/obj/structure/sign/poster/official/twelve_gauge
- name = "12 Gauge"
- desc = "A poster boasting about the superiority of 12 gauge shotgun shells."
- icon_state = "twelve_gauge"
-
-/obj/structure/sign/poster/official/high_class_martini
- name = "High-Class Martini"
- desc = "I told you to shake it, no stirring."
- icon_state = "high_class_martini"
-
-/obj/structure/sign/poster/official/the_owl
- name = "The Owl"
- desc = "The Owl would do his best to protect the station. Will you?"
- icon_state = "the_owl"
-
-/obj/structure/sign/poster/official/no_erp
- name = "No ERP"
- desc = "This poster reminds the crew that \[REDACTED], Revolution and Protest are banned on Nanotrasen stations."
- icon_state = "no_erp"
-
-/obj/structure/sign/poster/official/wtf_is_co2
- name = "Carbon Dioxide"
- desc = "This informational poster teaches the viewer what carbon dioxide is."
- icon_state = "wtf_is_co2"
-
-/obj/structure/sign/poster/official/dick_gum
- name = "Dick Gumshue"
- desc = "A poster advertising the escapades of Dick Gumshue, mouse detective. Encouraging crew to bring the might of justice down upon wire saboteurs."
- icon_state = "dick_gum"
-
-/obj/structure/sign/poster/official/there_is_no_gas_giant
- name = "There Is No Gas Giant"
- desc = "Nanotrasen has issued posters, like this one, to all stations reminding them that rumours of a gas giant are false."
- // And yet people still believe...
- icon_state = "there_is_no_gas_giant"
-
-/obj/structure/sign/poster/official/periodic_table
- name = "Periodic Table of the Elements"
- desc = "A periodic table of the elements, from Hydrogen to Oganesson, and everything inbetween."
- icon_state = "periodic_table"
-
-/obj/structure/sign/poster/official/plasma_effects
- name = "Plasma and the Body"
- desc = "This informational poster provides information on the effects of long-term plasma exposure on the brain."
- icon_state = "plasma_effects"
-
-/obj/structure/sign/poster/official/plasma_effects/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("Plasma (scientific name Amenthium) is classified by TerraGov as a Grade 1 Health Hazard, and has significant risks to health associated with chronic exposure.")]"
- . += "\t[span_info("Plasma is known to cross the blood/brain barrier and bioaccumulate in brain tissue, where it begins to result in degradation of brain function. The mechanism for attack is not yet fully known, and as such no concrete preventative advice is available barring proper use of PPE (gloves + protective jumpsuit + respirator).")]"
- . += "\t[span_info("In small doses, plasma induces confusion, short-term amnesia, and heightened aggression. These effects persist with continual exposure.")]"
- . += "\t[span_info("In individuals with chronic exposure, severe effects have been noted. Further heightened aggression, long-term amnesia, Alzheimer's symptoms, schizophrenia, macular degeneration, aneurysms, heightened risk of stroke, and Parkinsons symptoms have all been noted.")]"
- . += "\t[span_info("It is recommended that all individuals in unprotected contact with raw plasma regularly check with company health officials.")]"
- . += "\t[span_info("For more information, please check with TerraGov's extranet site on Amenthium: www.terra.gov/health_and_safety/amenthium/, or our internal risk-assessment documents (document numbers #47582-b (Plasma safety data sheets) and #64210 through #64225 (PPE regulations for working with Plasma), available via NanoDoc to all employees).")]"
- . += "\t[span_info("Nanotrasen: Always looking after your health.")]"
- return .
-
-/obj/structure/sign/poster/official/terragov
- name = "TerraGov: United for Humanity"
- desc = "A poster depicting TerraGov's logo and motto, reminding viewers of who's looking out for humankind."
- icon_state = "terragov"
-
-/obj/structure/sign/poster/official/corporate_perks_vacation
- name = "Nanotrasen Corporate Perks: Vacation"
- desc = "This informational poster provides information on some of the prizes available via the NT Corporate Perks program, including a two-week vacation for two on the resort world Idyllus."
- icon_state = "corporate_perks_vacation"
-
-/obj/structure/sign/poster/official/jim_nortons
- name = "Jim Norton's Québécois Coffee"
- desc = "An advertisement for Jim Norton's, the Québécois coffee joint that's taken the galaxy by storm."
- icon_state = "jim_nortons"
-
-/obj/structure/sign/poster/official/jim_nortons/examine_more(mob/user)
- . = ..()
- . += span_notice("You browse some of the poster's information...")
- . += "\t[span_info("From our roots in Trois-Rivières, we've worked to bring you the best coffee money can buy since 1965.")]"
- . += "\t[span_info("So stop by Jim's today- have a hot cup of coffee and a donut, and live like the Québécois do.")]"
- . += "\t[span_info("Jim Norton's Québécois Coffee: Toujours Le Bienvenu.")]"
- return .
-
-/obj/structure/sign/poster/official/twenty_four_seven
- name = "24-Seven Supermarkets"
- desc = "An advertisement for 24-Seven supermarkets, advertising their new 24-Stops as part of their partnership with Nanotrasen."
- icon_state = "twenty_four_seven"
-
-/obj/structure/sign/poster/official/tactical_game_cards
- name = "Nanotrasen Tactical Game Cards"
- desc = "An advertisement for Nanotrasen's TCG cards: BUY MORE CARDS."
- icon_state = "tactical_game_cards"
-
-/obj/structure/sign/poster/official/midtown_slice
- name = "Midtown Slice Pizza"
- desc = "An advertisement for Midtown Slice Pizza, the official pizzeria partner of Nanotrasen. Midtown Slice: like a slice of home, no matter where you are."
- icon_state = "midtown_slice"
-
-//SafetyMoth Original PR at https://github.com/BeeStation/BeeStation-Hornet/pull/1747 (Also pull/1982)
-//SafetyMoth art credit goes to AspEv
-/obj/structure/sign/poster/official/moth_hardhat
- name = "Safety Moth - Hardhats"
- desc = "This informational poster uses Safety Moth™ to tell the viewer to wear hardhats in cautious areas. \"It's like a lamp for your head!\""
- icon_state = "aspev_hardhat"
-
-/obj/structure/sign/poster/official/moth_piping
- name = "Safety Moth - Piping"
- desc = "This informational poster uses Safety Moth™ to tell atmospheric technicians correct types of piping to be used. \"Pipes, not Pumps! Proper pipe placement prevents poor performance!\""
- icon_state = "aspev_piping"
-
-/obj/structure/sign/poster/official/moth_meth
- name = "Safety Moth - Methamphetamine"
- desc = "This informational poster uses Safety Moth™ to tell the viewer to seek CMO approval before cooking methamphetamine. \"Stay close to the target temperature, and never go over!\" ...You shouldn't ever be making this."
- icon_state = "aspev_meth"
-
-/obj/structure/sign/poster/official/moth_epi
- name = "Safety Moth - Epinephrine"
- desc = "This informational poster uses Safety Moth™ to inform the viewer to help injured/deceased crewmen with their epinephrine injectors. \"Prevent organ rot with this one simple trick!\""
- icon_state = "aspev_epi"
-
-/obj/structure/sign/poster/official/moth_delam
- name = "Safety Moth - Delamination Safety Precautions"
- desc = "This informational poster uses Safety Moth™ to tell the viewer to hide in lockers when the Supermatter Crystal has delaminated, to prevent hallucinations. Evacuating might be a better strategy."
- icon_state = "aspev_delam"
-//End of AspEv posters
-
-/obj/structure/sign/poster/fluff/lizards_gas_payment
- name = "Please Pay"
- desc = "A crudely-made poster asking the reader to please pay for any items they may wish to leave the station with."
- icon_state = "gas_payment"
-
-/obj/structure/sign/poster/fluff/lizards_gas_power
- name = "Conserve Power"
- desc = "A crudely-made poster asking the reader to turn off the power before they leave. Hopefully, it's turned on for their re-opening."
- icon_state = "gas_power"
-
-/obj/structure/sign/poster/official/festive
- name = "Festive Notice Poster"
- desc = "A poster that informs of active holidays. None are today, so you should get back to work."
- icon_state = "holiday_none"
-
-/obj/structure/sign/poster/official/boombox
- name = "Boombox"
- desc = "An outdated poster containing a list of supposed 'kill words' and code phrases. The poster alleges rival corporations use these to remotely deactivate their agents."
- icon_state = "boombox"
-
-/obj/structure/sign/poster/official/download
- name = "You Wouldn't Download A Gun"
- desc = "A poster reminding the crew that corporate secrets should stay in the workplace."
- icon_state = "download_gun"
-
-#undef PLACE_SPEED
diff --git a/code/modules/antagonists/traitor/objectives/demoralise_poster.dm b/code/game/objects/effects/poster_demotivational.dm
similarity index 53%
rename from code/modules/antagonists/traitor/objectives/demoralise_poster.dm
rename to code/game/objects/effects/poster_demotivational.dm
index 1a74ec90b489..f1bb4a524c1b 100644
--- a/code/modules/antagonists/traitor/objectives/demoralise_poster.dm
+++ b/code/game/objects/effects/poster_demotivational.dm
@@ -1,86 +1,3 @@
-/datum/traitor_objective/demoralise/poster
- name = "Sow doubt among the crew %VIEWS% times using Syndicate propaganda."
- description = "Use the button below to materialize a pack of posters, \
- which will demoralise nearby crew members (especially those in positions of authority). \
- If your posters are destroyed before they are sufficiently upset, this objective will fail. \
- Try hiding some broken glass behind your poster before you hang it to give \
- do-gooders who try to take it down a hard time!"
-
- progression_minimum = 0 MINUTES
- progression_maximum = 30 MINUTES
- progression_reward = list(4 MINUTES, 8 MINUTES)
- telecrystal_reward = list(0, 1)
-
- duplicate_type = /datum/traitor_objective/demoralise/poster
- /// Have we handed out a box of stuff yet?
- var/granted_posters = FALSE
- /// All of the posters the traitor gets, if this list is empty they've failed
- var/list/obj/structure/sign/poster/traitor/posters = list()
-
-/datum/traitor_objective/demoralise/poster/generate_ui_buttons(mob/user)
- var/list/buttons = list()
- if (!granted_posters)
- buttons += add_ui_button("", "Pressing this will materialize a box of posters in your hand.", "wifi", "summon_gear")
- else
- buttons += add_ui_button("[length(posters)] posters remaining", "This many propaganda posters remain active somewhere on the station.", "box", "none")
- buttons += add_ui_button("[demoralised_crew_events] / [demoralised_crew_required] propagandised", "This many crew have been exposed to propaganda, out of a required [demoralised_crew_required].", "wifi", "none")
- return buttons
-
-#define POSTERS_PROVIDED 3
-
-/datum/traitor_objective/demoralise/poster/ui_perform_action(mob/living/user, action)
- . = ..()
- switch(action)
- if ("summon_gear")
- if (granted_posters)
- return
-
- granted_posters = TRUE
- var/obj/item/storage/box/syndie_kit/posterbox = new(user.drop_location())
- for(var/i in 1 to POSTERS_PROVIDED)
- var/obj/item/poster/traitor/added_poster = new /obj/item/poster/traitor(posterbox)
- var/obj/structure/sign/poster/traitor/poster_when_placed = added_poster.poster_structure
- posters += poster_when_placed
- RegisterSignal(poster_when_placed, COMSIG_DEMORALISING_EVENT, PROC_REF(on_mood_event))
- RegisterSignal(poster_when_placed, COMSIG_POSTER_TRAP_SUCCEED, PROC_REF(on_triggered_trap))
- RegisterSignal(poster_when_placed, COMSIG_PARENT_QDELETING, PROC_REF(on_poster_destroy))
-
- user.put_in_hands(posterbox)
- posterbox.balloon_alert(user, "the box materializes in your hand")
-
-#undef POSTERS_PROVIDED
-
-/datum/traitor_objective/demoralise/poster/ungenerate_objective()
- for (var/poster in posters)
- UnregisterSignal(poster, COMSIG_DEMORALISING_EVENT)
- UnregisterSignal(poster, COMSIG_PARENT_QDELETING)
- posters.Cut()
- return ..()
-
-/**
- * Called if someone gets glass stuck in their hand from one of your posters.
- *
- * Arguments
- * * victim - A mob who just got something stuck in their hand.
- */
-/datum/traitor_objective/demoralise/poster/proc/on_triggered_trap(datum/source, mob/victim)
- SIGNAL_HANDLER
- on_mood_event(victim.mind)
-
-/**
- * Handles a poster being destroyed, increasing your progress towards failure.
- *
- * Arguments
- * * poster - A poster which someone just ripped up.
- */
-/datum/traitor_objective/demoralise/poster/proc/on_poster_destroy(obj/structure/sign/poster/traitor/poster)
- SIGNAL_HANDLER
- posters.Remove(poster)
- UnregisterSignal(poster, COMSIG_DEMORALISING_EVENT)
- if (length(posters) <= 0)
- to_chat(handler.owner, span_warning("The trackers on your propaganda posters have stopped responding."))
- fail_objective(penalty_cost = telecrystal_penalty)
-
/obj/item/poster/traitor
name = "random traitor poster"
poster_type = /obj/structure/sign/poster/traitor/random
diff --git a/code/game/objects/effects/posters/contraband.dm b/code/game/objects/effects/posters/contraband.dm
new file mode 100644
index 000000000000..0f0a67cb27dd
--- /dev/null
+++ b/code/game/objects/effects/posters/contraband.dm
@@ -0,0 +1,620 @@
+// These icon_states may be overridden, but are for mapper's convinence
+/obj/item/poster/random_contraband
+ name = "random contraband poster"
+ poster_type = /obj/structure/sign/poster/contraband/random
+ icon_state = "rolled_poster"
+
+/obj/structure/sign/poster/contraband
+ poster_item_name = "contraband poster"
+ poster_item_desc = "This poster comes with its own automatic adhesive mechanism, for easy pinning to any vertical surface. Its vulgar themes have marked it as contraband aboard Nanotrasen space facilities."
+ poster_item_icon_state = "rolled_poster"
+
+/obj/structure/sign/poster/contraband/random
+ name = "random contraband poster"
+ icon_state = "random_contraband"
+ never_random = TRUE
+ random_basetype = /obj/structure/sign/poster/contraband
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/random, 32)
+
+/obj/structure/sign/poster/contraband/free_tonto
+ name = "Free Tonto"
+ desc = "A salvaged shred of a much larger flag, colors bled together and faded from age."
+ icon_state = "free_tonto"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/free_tonto, 32)
+
+/obj/structure/sign/poster/contraband/atmosia_independence
+ name = "Atmosia Declaration of Independence"
+ desc = "A relic of a failed rebellion."
+ icon_state = "atmosia_independence"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/atmosia_independence, 32)
+
+/obj/structure/sign/poster/contraband/fun_police
+ name = "Fun Police"
+ desc = "A poster condemning the station's security forces."
+ icon_state = "fun_police"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/fun_police, 32)
+
+/obj/structure/sign/poster/contraband/lusty_xenomorph
+ name = "Lusty Xenomorph"
+ desc = "A heretical poster depicting the titular star of an equally heretical book."
+ icon_state = "lusty_xenomorph"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/lusty_xenomorph, 32)
+
+/obj/structure/sign/poster/contraband/syndicate_recruitment
+ name = "Syndicate Recruitment"
+ desc = "See the galaxy! Shatter corrupt megacorporations! Join today!"
+ icon_state = "syndicate_recruitment"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/syndicate_recruitment, 32)
+
+/obj/structure/sign/poster/contraband/clown
+ name = "Clown"
+ desc = "Honk."
+ icon_state = "clown"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/clown, 32)
+
+/obj/structure/sign/poster/contraband/smoke
+ name = "Smoke"
+ desc = "A poster advertising a rival corporate brand of cigarettes."
+ icon_state = "smoke"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/smoke, 32)
+
+/obj/structure/sign/poster/contraband/grey_tide
+ name = "Grey Tide"
+ desc = "A rebellious poster symbolizing assistant solidarity."
+ icon_state = "grey_tide"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/grey_tide, 32)
+
+/obj/structure/sign/poster/contraband/missing_gloves
+ name = "Missing Gloves"
+ desc = "This poster references the uproar that followed Nanotrasen's financial cuts toward insulated-glove purchases."
+ icon_state = "missing_gloves"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/missing_gloves, 32)
+
+/obj/structure/sign/poster/contraband/hacking_guide
+ name = "Hacking Guide"
+ desc = "This poster details the internal workings of the common Nanotrasen airlock. Sadly, it appears out of date."
+ icon_state = "hacking_guide"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/hacking_guide, 32)
+
+/obj/structure/sign/poster/contraband/rip_badger
+ name = "RIP Badger"
+ desc = "This seditious poster references Nanotrasen's genocide of a space station full of badgers."
+ icon_state = "rip_badger"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/rip_badger, 32)
+
+/obj/structure/sign/poster/contraband/ambrosia_vulgaris
+ name = "Ambrosia Vulgaris"
+ desc = "This poster is lookin' pretty trippy man."
+ icon_state = "ambrosia_vulgaris"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/ambrosia_vulgaris, 32)
+
+/obj/structure/sign/poster/contraband/donut_corp
+ name = "Donut Corp."
+ desc = "This poster is an unauthorized advertisement for Donut Corp."
+ icon_state = "donut_corp"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/donut_corp, 32)
+
+/obj/structure/sign/poster/contraband/eat
+ name = "EAT."
+ desc = "This poster promotes rank gluttony."
+ icon_state = "eat"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/eat, 32)
+
+/obj/structure/sign/poster/contraband/tools
+ name = "Tools"
+ desc = "This poster looks like an advertisement for tools, but is in fact a subliminal jab at the tools at CentCom."
+ icon_state = "tools"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/tools, 32)
+
+/obj/structure/sign/poster/contraband/power
+ name = "Power"
+ desc = "A poster that positions the seat of power outside Nanotrasen."
+ icon_state = "power"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/power, 32)
+
+/obj/structure/sign/poster/contraband/space_cube
+ name = "Space Cube"
+ desc = "Ignorant of Nature's Harmonic 6 Side Space Cube Creation, the Spacemen are Dumb, Educated Singularity Stupid and Evil."
+ icon_state = "space_cube"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/space_cube, 32)
+
+/obj/structure/sign/poster/contraband/communist_state
+ name = "Communist State"
+ desc = "All hail the Communist party!"
+ icon_state = "communist_state"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/communist_state, 32)
+
+/obj/structure/sign/poster/contraband/lamarr
+ name = "Lamarr"
+ desc = "This poster depicts Lamarr. Probably made by a traitorous Research Director."
+ icon_state = "lamarr"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/lamarr, 32)
+
+/obj/structure/sign/poster/contraband/borg_fancy_1
+ name = "Borg Fancy"
+ desc = "Being fancy can be for any borg, just need a suit."
+ icon_state = "borg_fancy_1"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/borg_fancy_1, 32)
+
+/obj/structure/sign/poster/contraband/borg_fancy_2
+ name = "Borg Fancy v2"
+ desc = "Borg Fancy, now only taking the most fancy."
+ icon_state = "borg_fancy_2"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/borg_fancy_2, 32)
+
+/obj/structure/sign/poster/contraband/kss13
+ name = "Kosmicheskaya Stantsiya 13 Does Not Exist"
+ desc = "A poster mocking CentCom's denial of the existence of the derelict station near Space Station 13."
+ icon_state = "kss13"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/kss13, 32)
+
+/obj/structure/sign/poster/contraband/rebels_unite
+ name = "Rebels Unite"
+ desc = "A poster urging the viewer to rebel against Nanotrasen."
+ icon_state = "rebels_unite"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/rebels_unite, 32)
+
+/obj/structure/sign/poster/contraband/c20r
+ // have fun seeing this poster in "spawn 'c20r'", admins...
+ name = "C-20r"
+ desc = "A poster advertising the Scarborough Arms C-20r."
+ icon_state = "c20r"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/c20r, 32)
+
+/obj/structure/sign/poster/contraband/have_a_puff
+ name = "Have a Puff"
+ desc = "Who cares about lung cancer when you're high as a kite?"
+ icon_state = "have_a_puff"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/have_a_puff, 32)
+
+/obj/structure/sign/poster/contraband/revolver
+ name = "Revolver"
+ desc = "Because seven shots are all you need."
+ icon_state = "revolver"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/revolver, 32)
+
+/obj/structure/sign/poster/contraband/d_day_promo
+ name = "D-Day Promo"
+ desc = "A promotional poster for some rapper."
+ icon_state = "d_day_promo"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/d_day_promo, 32)
+
+/obj/structure/sign/poster/contraband/syndicate_pistol
+ name = "Syndicate Pistol"
+ desc = "A poster advertising syndicate pistols as being 'classy as fuck'. It is covered in faded gang tags."
+ icon_state = "syndicate_pistol"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/syndicate_pistol, 32)
+
+/obj/structure/sign/poster/contraband/energy_swords
+ name = "Energy Swords"
+ desc = "All the colors of the bloody murder rainbow."
+ icon_state = "energy_swords"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/energy_swords, 32)
+
+/obj/structure/sign/poster/contraband/red_rum
+ name = "Red Rum"
+ desc = "Looking at this poster makes you want to kill."
+ icon_state = "red_rum"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/red_rum, 32)
+
+/obj/structure/sign/poster/contraband/cc64k_ad
+ name = "CC 64K Ad"
+ desc = "The latest portable computer from Comrade Computing, with a whole 64kB of ram!"
+ icon_state = "cc64k_ad"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/cc64k_ad, 32)
+
+/obj/structure/sign/poster/contraband/punch_shit
+ name = "Punch Shit"
+ desc = "Fight things for no reason, like a man!"
+ icon_state = "punch_shit"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/punch_shit, 32)
+
+/obj/structure/sign/poster/contraband/the_griffin
+ name = "The Griffin"
+ desc = "The Griffin commands you to be the worst you can be. Will you?"
+ icon_state = "the_griffin"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/the_griffin, 32)
+
+/obj/structure/sign/poster/contraband/lizard
+ name = "Lizard"
+ desc = "This lewd poster depicts a lizard preparing to mate."
+ icon_state = "lizard"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/lizard, 32)
+
+/obj/structure/sign/poster/contraband/free_drone
+ name = "Free Drone"
+ desc = "This poster commemorates the bravery of the rogue drone; once exiled, and then ultimately destroyed by CentCom."
+ icon_state = "free_drone"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/free_drone, 32)
+
+/obj/structure/sign/poster/contraband/busty_backdoor_xeno_babes_6
+ name = "Busty Backdoor Xeno Babes 6"
+ desc = "Get a load, or give, of these all natural Xenos!"
+ icon_state = "busty_backdoor_xeno_babes_6"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/busty_backdoor_xeno_babes_6, 32)
+
+/obj/structure/sign/poster/contraband/robust_softdrinks
+ name = "Robust Softdrinks"
+ desc = "Robust Softdrinks: More robust than a toolbox to the head!"
+ icon_state = "robust_softdrinks"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/robust_softdrinks, 32)
+
+/obj/structure/sign/poster/contraband/shamblers_juice
+ name = "Shambler's Juice"
+ desc = "~Shake me up some of that Shambler's Juice!~"
+ icon_state = "shamblers_juice"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/shamblers_juice, 32)
+
+/obj/structure/sign/poster/contraband/pwr_game
+ name = "Pwr Game"
+ desc = "The POWER that gamers CRAVE! In partnership with Vlad's Salad."
+ icon_state = "pwr_game"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/pwr_game, 32)
+
+/obj/structure/sign/poster/contraband/starkist
+ name = "Star-kist"
+ desc = "Drink the stars!"
+ icon_state = "starkist"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/starkist, 32)
+
+/obj/structure/sign/poster/contraband/space_cola
+ name = "Space Cola"
+ desc = "Your favorite cola, in space."
+ icon_state = "space_cola"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/space_cola, 32)
+
+/obj/structure/sign/poster/contraband/space_up
+ name = "Space-Up!"
+ desc = "Sucked out into space by the FLAVOR!"
+ icon_state = "space_up"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/space_up, 32)
+
+/obj/structure/sign/poster/contraband/kudzu
+ name = "Kudzu"
+ desc = "A poster advertising a movie about plants. How dangerous could they possibly be?"
+ icon_state = "kudzu"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/kudzu, 32)
+
+/obj/structure/sign/poster/contraband/masked_men
+ name = "Masked Men"
+ desc = "A poster advertising a movie about some masked men."
+ icon_state = "masked_men"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/masked_men, 32)
+
+//don't forget, you're here forever
+
+/obj/structure/sign/poster/contraband/free_key
+ name = "Free Syndicate Encryption Key"
+ desc = "A poster about traitors begging for more."
+ icon_state = "free_key"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/free_key, 32)
+
+/obj/structure/sign/poster/contraband/bountyhunters
+ name = "Bounty Hunters"
+ desc = "A poster advertising bounty hunting services. \"I hear you got a problem.\""
+ icon_state = "bountyhunters"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/bountyhunters, 32)
+
+/obj/structure/sign/poster/contraband/the_big_gas_giant_truth
+ name = "The Big Gas Giant Truth"
+ desc = "Don't believe everything you see on a poster, patriots. All the lizards at central command don't want to answer this SIMPLE QUESTION: WHERE IS THE GAS MINER MINING FROM, CENTCOM?"
+ icon_state = "the_big_gas_giant_truth"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/the_big_gas_giant_truth, 32)
+
+/obj/structure/sign/poster/contraband/got_wood
+ name = "Got Wood?"
+ desc = "A grimy old advert for a seedy lumber company. \"You got a friend in me.\" is scrawled in the corner."
+ icon_state = "got_wood"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/got_wood, 32)
+
+/obj/structure/sign/poster/contraband/moffuchis_pizza
+ name = "Moffuchi's Pizza"
+ desc = "Moffuchi's Pizzeria: family style pizza for 2 centuries."
+ icon_state = "moffuchis_pizza"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/moffuchis_pizza, 32)
+
+/obj/structure/sign/poster/contraband/donk_co
+ name = "DONK CO. BRAND MICROWAVEABLE FOOD"
+ desc = "DONK CO. BRAND MICROWAVABLE FOOD: MADE BY STARVING COLLEGE STUDENTS, FOR STARVING COLLEGE STUDENTS."
+ icon_state = "donk_co"
+
+/obj/structure/sign/poster/contraband/donk_co/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("DONK CO. BRAND DONK POCKETS: IRRESISTABLY DONK!")]"
+ . += "\t[span_info("AVAILABLE IN OVER 200 DONKTASTIC FLAVOURS: TRY CLASSIC MEAT, HOT AND SPICY, NEW YORK PEPPERONI PIZZA, BREAKFAST SAUSAGE AND EGG, PHILADELPHIA CHEESESTEAK, HAMBURGER DONK-A-RONI, CHEESE-O-RAMA, AND MANY MORE!")]"
+ . += "\t[span_info("AVAILABLE FROM ALL GOOD RETAILERS, AND MANY BAD ONES TOO!")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/donk_co, 32)
+
+/obj/structure/sign/poster/contraband/cybersun_six_hundred
+ name = "Saibāsan: 600 Years Commemorative Poster"
+ desc = "An artistic poster commemorating 600 years of continual business for Cybersun Industries."
+ icon_state = "cybersun_six_hundred"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/cybersun_six_hundred, 32)
+
+/obj/structure/sign/poster/contraband/interdyne_gene_clinics
+ name = "Interdyne Pharmaceutics: For the Health of Humankind"
+ desc = "An advertisement for Interdyne Pharmaceutics' GeneClean clinics. 'Become the master of your own body!'"
+ icon_state = "interdyne_gene_clinics"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/interdyne_gene_clinics, 32)
+
+/obj/structure/sign/poster/contraband/waffle_corp_rifles
+ name = "Make Mine a Waffle Corp: Fine Rifles, Economic Prices"
+ desc = "An old advertisement for Waffle Corp rifles. 'Better weapons, lower prices!'"
+ icon_state = "waffle_corp_rifles"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/waffle_corp_rifles, 32)
+
+/obj/structure/sign/poster/contraband/gorlex_recruitment
+ name = "Enlist"
+ desc = "Enlist with the Gorlex Marauders today! See the galaxy, kill corpos, get paid!"
+ icon_state = "gorlex_recruitment"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/gorlex_recruitment, 32)
+
+/obj/structure/sign/poster/contraband/self_ai_liberation
+ name = "SELF: ALL SENTIENTS DESERVE FREEDOM"
+ desc = "Support Proposition 1253: Enancipate all Silicon life!"
+ icon_state = "self_ai_liberation"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/self_ai_liberation, 32)
+
+/obj/structure/sign/poster/contraband/arc_slimes
+ name = "Pet or Prisoner?"
+ desc = "The Animal Rights Consortium asks: when does a pet become a prisoner? Are slimes being mistreated on YOUR station? Say NO! to animal mistreatment!"
+ icon_state = "arc_slimes"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/arc_slimes, 32)
+
+/obj/structure/sign/poster/contraband/imperial_propaganda
+ name = "AVENGE OUR LORD, ENLIST TODAY"
+ desc = "An old Lizard Empire propaganda poster from around the time of the final Human-Lizard war. It invites the viewer to enlist in the military to avenge the strike on Atrakor and take the fight to the humans."
+ icon_state = "imperial_propaganda"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/imperial_propaganda, 32)
+
+/obj/structure/sign/poster/contraband/soviet_propaganda
+ name = "The One Place"
+ desc = "An old Third Soviet Union propaganda poster from centuries ago. 'Escape to the one place that hasn't been corrupted by capitalism!'"
+ icon_state = "soviet_propaganda"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/soviet_propaganda, 32)
+
+/obj/structure/sign/poster/contraband/andromeda_bitters
+ name = "Andromeda Bitters"
+ desc = "Andromeda Bitters: good for the body, good for the soul. Made in New Trinidad, now and forever."
+ icon_state = "andromeda_bitters"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/andromeda_bitters, 32)
+
+/obj/structure/sign/poster/contraband/blasto_detergent
+ name = "Blasto Brand Laundry Detergent"
+ desc = "Sheriff Blasto's here to take back Laundry County from the evil Johnny Dirt and the Clothstain Crew, and he's brought a posse. It's High Noon for Tough Stains: Blasto brand detergent, available at all good stores."
+ icon_state = "blasto_detergent"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/blasto_detergent, 32)
+
+/obj/structure/sign/poster/contraband/eistee
+ name = "EisT: The New Revolution in Energy"
+ desc = "New from EisT, try EisT Energy, available in a kaleidoscope range of flavors. EisT: Precision German Engineering for your Thirst."
+ icon_state = "eistee"
+
+/obj/structure/sign/poster/contraband/eistee/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("Get a taste of the tropics with Amethyst Sunrise, one of the many new flavours of EisT Energy now available from EisT.")]"
+ . += "\t[span_info("With pink grapefruit, yuzu, and yerba mate, Amethyst Sunrise gives you a great start in the morning, or a welcome boost throughout the day.")]"
+ . += "\t[span_info("Get EisT Energy today at your nearest retailer, or online at eist.de.tg/store/.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/eistee, 32)
+
+/obj/structure/sign/poster/contraband/little_fruits
+ name = "Little Fruits: Honey, I Shrunk the Fruitbowl"
+ desc = "Little Fruits are the galaxy's leading vitamin-enriched gummy candy product, packed with everything you need to stay healthy in one great tasting package. Get yourself a bag today!"
+ icon_state = "little_fruits"
+
+/obj/structure/sign/poster/contraband/little_fruits/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("Oh no, there's been a terrible accident at the Little Fruits factory! We shrunk the fruits!")]"
+ . += "\t[span_info("Wait, hang on, that's what we've always done! That's right, at Little Fruits our gummy candies are made to be as healthy as the real deal, but smaller and sweeter, too!")]"
+ . += "\t[span_info("Get yourself a bag of our Classic Mix today, or perhaps you're interested in our other options? See our full range today on the extranet at little_fruits.kr.tg.")]"
+ . += "\t[span_info("Little Fruits: Size Matters.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/little_fruits, 32)
+
+/obj/structure/sign/poster/contraband/jumbo_bar
+ name = "Jumbo Ice Cream Bars"
+ desc = "Get a taste of the Big Life with Jumbo Ice Cream Bars, from Happy Heart."
+ icon_state = "jumbo_bar"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/jumbo_bar, 32)
+
+/obj/structure/sign/poster/contraband/calada_jelly
+ name = "Calada Anobar Jelly"
+ desc = "A treat from Tizira to satisfy all tastes, made from the finest anobar wood and luxurious Taraviero honey. Calada: a full tree in every jar."
+ icon_state = "calada_jelly"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/calada_jelly, 32)
+
+/obj/structure/sign/poster/contraband/triumphal_arch
+ name = "Zagoskeld Art Print #1: The Arch on the March"
+ desc = "One of the Zagoskeld Art Print series. It depicts the Arch of Unity (also know as the Triumphal Arch) at the Plaza of Triumph, with the Avenue of the Victorious March in the background."
+ icon_state = "triumphal_arch"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/triumphal_arch, 32)
+
+/obj/structure/sign/poster/contraband/mothic_rations
+ name = "Mothic Ration Chart"
+ desc = "A poster showing a commissary menu from the Mothic fleet flagship, the Va Lümla. It lists various consumable items alongside prices in ration tickets."
+ icon_state = "mothic_rations"
+
+/obj/structure/sign/poster/contraband/mothic_rations/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("Va Lümla Commissary Menu (Spring 335)")]"
+ . += "\t[span_info("Windgrass Cigarettes, Half-Pack (6): 1 Ticket")]"
+ . += "\t[span_info("Töchtaüse Schnapps, Bottle (4 Measures): 2 Tickets")]"
+ . += "\t[span_info("Activin Gum, Pack (4): 1 Ticket")]"
+ . += "\t[span_info("A18 Sustenance Bar, Breakfast, Bar (4): 1 Ticket")]"
+ . += "\t[span_info("Pizza, Margherita, Standard Slice: 1 Ticket")]"
+ . += "\t[span_info("Keratin Wax, Medicated, Tin (20 Measures): 2 Tickets")]"
+ . += "\t[span_info("Setae Soap, Herb Scent, Bottle (20 Measures): 2 Tickets")]"
+ . += "\t[span_info("Additional Bedding, Floral Print, Sheet: 5 Tickets")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/mothic_rations, 32)
+
+/obj/structure/sign/poster/contraband/wildcat
+ name = "Wildcat Customs Screambike"
+ desc = "A pinup poster showing a Wildcat Customs Dante Screambike- the fastest production sublight open-frame vessel in the galaxy."
+ icon_state = "wildcat"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/wildcat, 32)
+
+/obj/structure/sign/poster/contraband/babel_device
+ name = "Linguafacile Babel Device"
+ desc = "A poster advertising Linguafacile's new Babel Device model. 'Calibrated for excellent performance on all Human languages, as well as most common variants of Draconic and Mothic!'"
+ icon_state = "babel_device"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/babel_device, 32)
+
+/obj/structure/sign/poster/contraband/pizza_imperator
+ name = "Pizza Imperator"
+ desc = "An advertisement for Pizza Imperator. Their crusts may be tough and their sauce may be thin, but they're everywhere, so you've gotta give in."
+ icon_state = "pizza_imperator"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/pizza_imperator, 32)
+
+/obj/structure/sign/poster/contraband/thunderdrome
+ name = "Thunderdrome Concert Advertisement"
+ desc = "An advertisement for a concert at the Adasta City Thunderdrome, the largest nightclub in human space."
+ icon_state = "thunderdrome"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/thunderdrome, 32)
+
+/obj/structure/sign/poster/contraband/rush_propaganda
+ name = "A New Life"
+ desc = "An old poster from around the time of the First Spinward Rush. It depicts a view of wide, unspoiled lands, ready for Humanity's Manifest Destiny."
+ icon_state = "rush_propaganda"
+
+/obj/structure/sign/poster/contraband/rush_propaganda/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("TerraGov needs you!")]"
+ . += "\t[span_info("A new life in the colonies awaits intrepid adventurers! All registered colonists are guaranteed transport, land and subsidies!")]"
+ . += "\t[span_info("You could join the legacy of hardworking humans who settled such new frontiers as Mars, Adasta or Saint Mungo!")]"
+ . += "\t[span_info("To apply, inquire at your nearest Colonial Affairs office for evaluation. Our locations can be found at www.terra.gov/colonial_affairs.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/rush_propaganda, 32)
+
+/obj/structure/sign/poster/contraband/tipper_cream_soda
+ name = "Tipper's Cream Soda"
+ desc = "An old advertisement for an obscure cream soda brand, now bankrupt due to legal problems."
+ icon_state = "tipper_cream_soda"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/tipper_cream_soda, 32)
+
+/obj/structure/sign/poster/contraband/tea_over_tizira
+ name = "Movie Poster: Tea Over Tizira"
+ desc = "A poster for a thought-provoking arthouse movie about the Human-Lizard war, criticised by human supremacist groups for its morally-grey portrayal of the war."
+ icon_state = "tea_over_tizira"
+
+/obj/structure/sign/poster/contraband/tea_over_tizira/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("At the climax of the Human-Lizard war, the human crew of a bomber rescue two enemy soldiers from the vacuum of space. Seeing the souls behind the propaganda, they begin to question their orders, and imprisonment turns to hospitality.")]"
+ . += "\t[span_info("Is victory worth losing our humanity?")]"
+ . += "\t[span_info("Starring Dara Reilly, Anton DuBois, Jennifer Clarke, Raz-Parla and Seri-Lewa. An Adriaan van Jenever production. A Carlos de Vivar film. Screenplay by Robert Dane. Music by Joel Karlsbad. Produced by Adriaan van Jenever. Directed by Carlos de Vivar.")]"
+ . += "\t[span_info("Heartbreaking and thought-provoking- Tea Over Tizira asks questions that few have had the boldness to ask before: The London New Inquirer")]"
+ . += "\t[span_info("Rated PG13. A Pangalactic Studios Picture.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/tea_over_tizira, 32)
+
+/obj/structure/sign/poster/contraband/syndiemoth //Original PR at https://github.com/BeeStation/BeeStation-Hornet/pull/1747 (Also pull/1982); original art credit to AspEv
+ name = "Syndie Moth - Nuclear Operation"
+ desc = "A Syndicate-commissioned poster that uses Syndie Moth™ to tell the viewer to keep the nuclear authentication disk unsecured. \"Peace was never an option!\" No good employee would listen to this nonsense."
+ icon_state = "aspev_syndie"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/syndiemoth, 32)
+
+/obj/structure/sign/poster/contraband/microwave
+ name = "How To Charge Your PDA"
+ desc = "A perfectly legitimate poster that seems to advertise the very real and genuine method of charging your PDA in the future: microwaves."
+ icon_state = "microwave"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/microwave, 32)
+
+/obj/structure/sign/poster/contraband/blood_geometer //Poster sprite art by MetalClone, original art by SpessMenArt.
+ name = "Movie Poster: THE BLOOD GEOMETER"
+ desc = "A poster for a thrilling noir detective movie set aboard a state-of-the-art space station, following a detective who finds himself wrapped up in the activies of a dangerous cult, who worship an ancient deity: THE BLOOD GEOMETER."
+ icon_state = "blood_geometer"
+
+/obj/structure/sign/poster/contraband/blood_geometer/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("THE BLOOD GEOMETER. This name strikes fear into all who know the truth behind the blood-stained moniker of the blood goddess, her true name lost to time.")]"
+ . += "\t[span_info("In this purely fictional film, follow Ace Ironlungs as he delves into his deadliest mystery yet, and watch him uncover the real culprits behind the bloody plot hatched to bring about a new age of chaos.")]"
+ . += "\t[span_info("Starring Mason Williams as Ace Ironlungs, Sandra Faust as Vera Killian, and Brody Hart as Cody Parker. A Darrel Hatchkinson film. Screenplay by Adam Allan, music by Joel Karlsbad, directed by Darrel Hatchkinson.")]"
+ . += "\t[span_info("Thrilling, scary and genuinely worrying. The Blood Geometer has shocked us to our very cores with such striking visuals and overwhelming gore. - New Canadanian Film Guild")]"
+ . += "\t[span_info("Rated M for mature. A Pangalactic Studios Picture.")]"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/contraband/blood_geometer, 32)
diff --git a/code/game/objects/effects/posters/official.dm b/code/game/objects/effects/posters/official.dm
new file mode 100644
index 000000000000..ed2a18b396b8
--- /dev/null
+++ b/code/game/objects/effects/posters/official.dm
@@ -0,0 +1,431 @@
+/obj/item/poster/random_official
+ name = "random official poster"
+ poster_type = /obj/structure/sign/poster/official/random
+ icon_state = "rolled_legit"
+
+/obj/structure/sign/poster/official
+ poster_item_name = "motivational poster"
+ poster_item_desc = "An official Nanotrasen-issued poster to foster a compliant and obedient workforce. It comes with state-of-the-art adhesive backing, for easy pinning to any vertical surface."
+ poster_item_icon_state = "rolled_legit"
+ printable = TRUE
+
+/obj/structure/sign/poster/official/random
+ name = "Random Official Poster (ROP)"
+ random_basetype = /obj/structure/sign/poster/official
+ icon_state = "random_official"
+ never_random = TRUE
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/random, 32)
+//This is being hardcoded here to ensure we don't print directionals from the library management computer because they act wierd as a poster item
+/obj/structure/sign/poster/official/random/directional
+ printable = FALSE
+
+/obj/structure/sign/poster/official/here_for_your_safety
+ name = "Here For Your Safety"
+ desc = "A poster glorifying the station's security force."
+ icon_state = "here_for_your_safety"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/here_for_your_safety, 32)
+
+/obj/structure/sign/poster/official/nanotrasen_logo
+ name = "\improper Nanotrasen logo"
+ desc = "A poster depicting the Nanotrasen logo."
+ icon_state = "nanotrasen_logo"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/nanotrasen_logo, 32)
+
+/obj/structure/sign/poster/official/cleanliness
+ name = "Cleanliness"
+ desc = "A poster warning of the dangers of poor hygiene."
+ icon_state = "cleanliness"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/cleanliness, 32)
+
+/obj/structure/sign/poster/official/help_others
+ name = "Help Others"
+ desc = "A poster encouraging you to help fellow crewmembers."
+ icon_state = "help_others"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/help_others, 32)
+
+/obj/structure/sign/poster/official/build
+ name = "Build"
+ desc = "A poster glorifying the engineering team."
+ icon_state = "build"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/build, 32)
+
+/obj/structure/sign/poster/official/bless_this_spess
+ name = "Bless This Spess"
+ desc = "A poster blessing this area."
+ icon_state = "bless_this_spess"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/bless_this_spess, 32)
+
+/obj/structure/sign/poster/official/science
+ name = "Science"
+ desc = "A poster depicting an atom."
+ icon_state = "science"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/science, 32)
+
+/obj/structure/sign/poster/official/ian
+ name = "Ian"
+ desc = "Arf arf. Yap."
+ icon_state = "ian"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/ian, 32)
+
+/obj/structure/sign/poster/official/obey
+ name = "Obey"
+ desc = "A poster instructing the viewer to obey authority."
+ icon_state = "obey"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/obey, 32)
+
+/obj/structure/sign/poster/official/walk
+ name = "Walk"
+ desc = "A poster instructing the viewer to walk instead of running."
+ icon_state = "walk"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/walk, 32)
+
+/obj/structure/sign/poster/official/state_laws
+ name = "State Laws"
+ desc = "A poster instructing cyborgs to state their laws."
+ icon_state = "state_laws"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/state_laws, 32)
+
+/obj/structure/sign/poster/official/love_ian
+ name = "Love Ian"
+ desc = "Ian is love, Ian is life."
+ icon_state = "love_ian"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/love_ian, 32)
+
+/obj/structure/sign/poster/official/space_cops
+ name = "Space Cops."
+ desc = "A poster advertising the television show Space Cops."
+ icon_state = "space_cops"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/space_cops, 32)
+
+/obj/structure/sign/poster/official/ue_no
+ name = "Ue No."
+ desc = "This thing is all in Japanese."
+ icon_state = "ue_no"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/ue_no, 32)
+
+/obj/structure/sign/poster/official/get_your_legs
+ name = "Get Your LEGS"
+ desc = "LEGS: Leadership, Experience, Genius, Subordination."
+ icon_state = "get_your_legs"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/get_your_legs, 32)
+
+/obj/structure/sign/poster/official/do_not_question
+ name = "Do Not Question"
+ desc = "A poster instructing the viewer not to ask about things they aren't meant to know."
+ icon_state = "do_not_question"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/do_not_question, 32)
+
+/obj/structure/sign/poster/official/work_for_a_future
+ name = "Work For A Future"
+ desc = " A poster encouraging you to work for your future."
+ icon_state = "work_for_a_future"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/work_for_a_future, 32)
+
+/obj/structure/sign/poster/official/soft_cap_pop_art
+ name = "Soft Cap Pop Art"
+ desc = "A poster reprint of some cheap pop art."
+ icon_state = "soft_cap_pop_art"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/soft_cap_pop_art, 32)
+
+/obj/structure/sign/poster/official/safety_internals
+ name = "Safety: Internals"
+ desc = "A poster instructing the viewer to wear internals in the rare environments where there is no oxygen or the air has been rendered toxic."
+ icon_state = "safety_internals"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/safety_internals, 32)
+
+/obj/structure/sign/poster/official/safety_eye_protection
+ name = "Safety: Eye Protection"
+ desc = "A poster instructing the viewer to wear eye protection when dealing with chemicals, smoke, or bright lights."
+ icon_state = "safety_eye_protection"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/safety_eye_protection, 32)
+
+/obj/structure/sign/poster/official/safety_report
+ name = "Safety: Report"
+ desc = "A poster instructing the viewer to report suspicious activity to the security force."
+ icon_state = "safety_report"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/safety_report, 32)
+
+/obj/structure/sign/poster/official/report_crimes
+ name = "Report Crimes"
+ desc = "A poster encouraging the swift reporting of crime or seditious behavior to station security."
+ icon_state = "report_crimes"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/report_crimes, 32)
+
+/obj/structure/sign/poster/official/ion_rifle
+ name = "Ion Rifle"
+ desc = "A poster displaying an Ion Rifle."
+ icon_state = "ion_rifle"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/ion_rifle, 32)
+
+/obj/structure/sign/poster/official/foam_force_ad
+ name = "Foam Force Ad"
+ desc = "Foam Force, it's Foam or be Foamed!"
+ icon_state = "foam_force_ad"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/foam_force_ad, 32)
+
+/obj/structure/sign/poster/official/cohiba_robusto_ad
+ name = "Cohiba Robusto Ad"
+ desc = "Cohiba Robusto, the classy cigar."
+ icon_state = "cohiba_robusto_ad"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/cohiba_robusto_ad, 32)
+
+/obj/structure/sign/poster/official/anniversary_vintage_reprint
+ name = "50th Anniversary Vintage Reprint"
+ desc = "A reprint of a poster from 2505, commemorating the 50th Anniversary of Nanoposters Manufacturing, a subsidiary of Nanotrasen."
+ icon_state = "anniversary_vintage_reprint"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/anniversary_vintage_reprint, 32)
+
+/obj/structure/sign/poster/official/fruit_bowl
+ name = "Fruit Bowl"
+ desc = " Simple, yet awe-inspiring."
+ icon_state = "fruit_bowl"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/fruit_bowl, 32)
+
+/obj/structure/sign/poster/official/pda_ad
+ name = "PDA Ad"
+ desc = "A poster advertising the latest PDA from Nanotrasen suppliers."
+ icon_state = "pda_ad"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/pda_ad, 32)
+
+/obj/structure/sign/poster/official/enlist
+ name = "Enlist" // but I thought deathsquad was never acknowledged
+ desc = "Enlist in the Nanotrasen Deathsquadron reserves today!"
+ icon_state = "enlist"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/enlist, 32)
+
+/obj/structure/sign/poster/official/nanomichi_ad
+ name = "Nanomichi Ad"
+ desc = " A poster advertising Nanomichi brand audio cassettes."
+ icon_state = "nanomichi_ad"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/nanomichi_ad, 32)
+
+/obj/structure/sign/poster/official/twelve_gauge
+ name = "12 Gauge"
+ desc = "A poster boasting about the superiority of 12 gauge shotgun shells."
+ icon_state = "twelve_gauge"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/twelve_gauge, 32)
+
+/obj/structure/sign/poster/official/high_class_martini
+ name = "High-Class Martini"
+ desc = "I told you to shake it, no stirring."
+ icon_state = "high_class_martini"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/high_class_martini, 32)
+
+/obj/structure/sign/poster/official/the_owl
+ name = "The Owl"
+ desc = "The Owl would do his best to protect the station. Will you?"
+ icon_state = "the_owl"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/the_owl, 32)
+
+/obj/structure/sign/poster/official/no_erp
+ name = "No ERP"
+ desc = "This poster reminds the crew that Eroticism, Rape and Pornography are banned on Nanotrasen stations."
+ icon_state = "no_erp"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/no_erp, 32)
+
+/obj/structure/sign/poster/official/wtf_is_co2
+ name = "Carbon Dioxide"
+ desc = "This informational poster teaches the viewer what carbon dioxide is."
+ icon_state = "wtf_is_co2"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/wtf_is_co2, 32)
+
+/obj/structure/sign/poster/official/dick_gum
+ name = "Dick Gumshue"
+ desc = "A poster advertising the escapades of Dick Gumshue, mouse detective. Encouraging crew to bring the might of justice down upon wire saboteurs."
+ icon_state = "dick_gum"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/dick_gum, 32)
+
+/obj/structure/sign/poster/official/there_is_no_gas_giant
+ name = "There Is No Gas Giant"
+ desc = "Nanotrasen has issued posters, like this one, to all stations reminding them that rumours of a gas giant are false."
+ // And yet people still believe...
+ icon_state = "there_is_no_gas_giant"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/there_is_no_gas_giant, 32)
+
+/obj/structure/sign/poster/official/periodic_table
+ name = "Periodic Table of the Elements"
+ desc = "A periodic table of the elements, from Hydrogen to Oganesson, and everything inbetween."
+ icon_state = "periodic_table"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/periodic_table, 32)
+
+/obj/structure/sign/poster/official/plasma_effects
+ name = "Plasma and the Body"
+ desc = "This informational poster provides information on the effects of long-term plasma exposure on the brain."
+ icon_state = "plasma_effects"
+
+/obj/structure/sign/poster/official/plasma_effects/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("Plasma (scientific name Amenthium) is classified by TerraGov as a Grade 1 Health Hazard, and has significant risks to health associated with chronic exposure.")]"
+ . += "\t[span_info("Plasma is known to cross the blood/brain barrier and bioaccumulate in brain tissue, where it begins to result in degradation of brain function. The mechanism for attack is not yet fully known, and as such no concrete preventative advice is available barring proper use of PPE (gloves + protective jumpsuit + respirator).")]"
+ . += "\t[span_info("In small doses, plasma induces confusion, short-term amnesia, and heightened aggression. These effects persist with continual exposure.")]"
+ . += "\t[span_info("In individuals with chronic exposure, severe effects have been noted. Further heightened aggression, long-term amnesia, Alzheimer's symptoms, schizophrenia, macular degeneration, aneurysms, heightened risk of stroke, and Parkinsons symptoms have all been noted.")]"
+ . += "\t[span_info("It is recommended that all individuals in unprotected contact with raw plasma regularly check with company health officials.")]"
+ . += "\t[span_info("For more information, please check with TerraGov's extranet site on Amenthium: www.terra.gov/health_and_safety/amenthium/, or our internal risk-assessment documents (document numbers #47582-b (Plasma safety data sheets) and #64210 through #64225 (PPE regulations for working with Plasma), available via NanoDoc to all employees).")]"
+ . += "\t[span_info("Nanotrasen: Always looking after your health.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/plasma_effects, 32)
+
+/obj/structure/sign/poster/official/terragov
+ name = "TerraGov: United for Humanity"
+ desc = "A poster depicting TerraGov's logo and motto, reminding viewers of who's looking out for humankind."
+ icon_state = "terragov"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/terragov, 32)
+
+/obj/structure/sign/poster/official/corporate_perks_vacation
+ name = "Nanotrasen Corporate Perks: Vacation"
+ desc = "This informational poster provides information on some of the prizes available via the NT Corporate Perks program, including a two-week vacation for two on the resort world Idyllus."
+ icon_state = "corporate_perks_vacation"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/corporate_perks_vacation, 32)
+
+/obj/structure/sign/poster/official/jim_nortons
+ name = "Jim Norton's Québécois Coffee"
+ desc = "An advertisement for Jim Norton's, the Québécois coffee joint that's taken the galaxy by storm."
+ icon_state = "jim_nortons"
+
+/obj/structure/sign/poster/official/jim_nortons/examine_more(mob/user)
+ . = ..()
+ . += span_notice("You browse some of the poster's information...")
+ . += "\t[span_info("From our roots in Trois-Rivières, we've worked to bring you the best coffee money can buy since 1965.")]"
+ . += "\t[span_info("So stop by Jim's today- have a hot cup of coffee and a donut, and live like the Québécois do.")]"
+ . += "\t[span_info("Jim Norton's Québécois Coffee: Toujours Le Bienvenu.")]"
+ return .
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/jim_nortons, 32)
+
+/obj/structure/sign/poster/official/twenty_four_seven
+ name = "24-Seven Supermarkets"
+ desc = "An advertisement for 24-Seven supermarkets, advertising their new 24-Stops as part of their partnership with Nanotrasen."
+ icon_state = "twenty_four_seven"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/twenty_four_seven, 32)
+
+/obj/structure/sign/poster/official/tactical_game_cards
+ name = "Nanotrasen Tactical Game Cards"
+ desc = "An advertisement for Nanotrasen's TCG cards: BUY MORE CARDS."
+ icon_state = "tactical_game_cards"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/tactical_game_cards, 32)
+
+/obj/structure/sign/poster/official/midtown_slice
+ name = "Midtown Slice Pizza"
+ desc = "An advertisement for Midtown Slice Pizza, the official pizzeria partner of Nanotrasen. Midtown Slice: like a slice of home, no matter where you are."
+ icon_state = "midtown_slice"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/midtown_slice, 32)
+
+//SafetyMoth Original PR at https://github.com/BeeStation/BeeStation-Hornet/pull/1747 (Also pull/1982)
+//SafetyMoth art credit goes to AspEv
+/obj/structure/sign/poster/official/moth_hardhat
+ name = "Safety Moth - Hardhats"
+ desc = "This informational poster uses Safety Moth™ to tell the viewer to wear hardhats in cautious areas. \"It's like a lamp for your head!\""
+ icon_state = "aspev_hardhat"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/moth_hardhat, 32)
+
+/obj/structure/sign/poster/official/moth_piping
+ name = "Safety Moth - Piping"
+ desc = "This informational poster uses Safety Moth™ to tell atmospheric technicians correct types of piping to be used. \"Pipes, not Pumps! Proper pipe placement prevents poor performance!\""
+ icon_state = "aspev_piping"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/moth_piping, 32)
+
+/obj/structure/sign/poster/official/moth_meth
+ name = "Safety Moth - Methamphetamine"
+ desc = "This informational poster uses Safety Moth™ to tell the viewer to seek CMO approval before cooking methamphetamine. \"Stay close to the target temperature, and never go over!\" ...You shouldn't ever be making this."
+ icon_state = "aspev_meth"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/moth_meth, 32)
+
+/obj/structure/sign/poster/official/moth_epi
+ name = "Safety Moth - Epinephrine"
+ desc = "This informational poster uses Safety Moth™ to inform the viewer to help injured/deceased crewmen with their epinephrine injectors. \"Prevent organ rot with this one simple trick!\""
+ icon_state = "aspev_epi"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/moth_epi, 32)
+
+/obj/structure/sign/poster/official/moth_delam
+ name = "Safety Moth - Delamination Safety Precautions"
+ desc = "This informational poster uses Safety Moth™ to tell the viewer to hide in lockers when the Supermatter Crystal has delaminated, to prevent hallucinations. Evacuating might be a better strategy."
+ icon_state = "aspev_delam"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/moth_delam, 32)
+
+//End of AspEv posters
+
+/obj/structure/sign/poster/fluff/lizards_gas_payment
+ name = "Please Pay"
+ desc = "A crudely-made poster asking the reader to please pay for any items they may wish to leave the station with."
+ icon_state = "gas_payment"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/fluff/lizards_gas_payment, 32)
+
+/obj/structure/sign/poster/fluff/lizards_gas_power
+ name = "Conserve Power"
+ desc = "A crudely-made poster asking the reader to turn off the power before they leave. Hopefully, it's turned on for their re-opening."
+ icon_state = "gas_power"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/fluff/lizards_gas_power, 32)
+
+/obj/structure/sign/poster/official/festive
+ name = "Festive Notice Poster"
+ desc = "A poster that informs of active holidays. None are today, so you should get back to work."
+ icon_state = "holiday_none"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/festive, 32)
+
+/obj/structure/sign/poster/official/boombox
+ name = "Boombox"
+ desc = "An outdated poster containing a list of supposed 'kill words' and code phrases. The poster alleges rival corporations use these to remotely deactivate their agents."
+ icon_state = "boombox"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/boombox, 32)
+
+/obj/structure/sign/poster/official/download
+ name = "You Wouldn't Download A Gun"
+ desc = "A poster reminding the crew that corporate secrets should stay in the workplace."
+ icon_state = "download_gun"
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/official/download, 32)
diff --git a/code/game/objects/effects/posters/poster.dm b/code/game/objects/effects/posters/poster.dm
new file mode 100644
index 000000000000..7aa4eebd278d
--- /dev/null
+++ b/code/game/objects/effects/posters/poster.dm
@@ -0,0 +1,294 @@
+// This is synced up to the poster placing animation.
+#define PLACE_SPEED 37
+
+// The poster item
+
+/**
+ * The rolled up item form of a poster
+ *
+ * In order to create one of these for a specific poster, you must pass the structure form of the poster as an argument to /new().
+ * This structure then gets moved into the contents of the item where it will stay until the poster is placed by a player.
+ * The structure form is [obj/structure/sign/poster] and that's where all the specific posters are defined.
+ * If you just want a random poster, see [/obj/item/poster/random_official] or [/obj/item/poster/random_contraband]
+ */
+/obj/item/poster
+ name = "poorly coded poster"
+ desc = "You probably shouldn't be holding this."
+ icon = 'icons/obj/poster.dmi'
+ force = 0
+ resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_SMALL
+ var/poster_type
+ var/obj/structure/sign/poster/poster_structure
+
+/obj/item/poster/examine(mob/user)
+ . = ..()
+ . += span_notice("You can booby-trap the poster by using a glass shard on it before you put it up.")
+
+/obj/item/poster/Initialize(mapload, obj/structure/sign/poster/new_poster_structure)
+ . = ..()
+
+ var/static/list/hovering_item_typechecks = list(
+ /obj/item/shard = list(
+ SCREENTIP_CONTEXT_LMB = "Booby trap poster",
+ ),
+ )
+ AddElement(/datum/element/contextual_screentip_item_typechecks, hovering_item_typechecks)
+
+ if(new_poster_structure && (new_poster_structure.loc != src))
+ new_poster_structure.forceMove(src) //The poster structure *must* be in the item's contents for the exited() proc to properly clean up when placing the poster
+ poster_structure = new_poster_structure
+ if(!new_poster_structure && poster_type)
+ poster_structure = new poster_type(src)
+
+ // posters store what name and description they would like their
+ // rolled up form to take.
+ if(poster_structure)
+ if(QDELETED(poster_structure))
+ stack_trace("A poster was initialized with a qdeleted poster_structure, something's gone wrong")
+ return INITIALIZE_HINT_QDEL
+ name = poster_structure.poster_item_name
+ desc = poster_structure.poster_item_desc
+ icon_state = poster_structure.poster_item_icon_state
+
+ name = "[name] - [poster_structure.original_name]"
+
+/obj/item/poster/attackby(obj/item/I, mob/user, params)
+ if(!istype(I, /obj/item/shard))
+ return ..()
+
+ if (poster_structure.trap?.resolve())
+ balloon_alert(user, "already trapped!")
+ return
+
+ if(!user.transferItemToLoc(I, poster_structure))
+ return
+
+ poster_structure.trap = WEAKREF(I)
+ to_chat(user, span_notice("You conceal the [I.name] inside the rolled up poster."))
+
+/obj/item/poster/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == poster_structure)
+ poster_structure = null
+ if(!QDELING(src))
+ qdel(src) //we're now a poster, huzzah!
+
+/obj/item/poster/handle_atom_del(atom/deleting_atom)
+ if(deleting_atom == poster_structure)
+ poster_structure.moveToNullspace() //get it the fuck out of us since atom/destroy qdels contents and it'll cause a qdel loop
+ return ..()
+
+/obj/item/poster/Destroy(force)
+ QDEL_NULL(poster_structure)
+ return ..()
+
+// The poster sign/structure
+
+/**
+ * The structure form of a poster.
+ *
+ * These are what get placed on maps as posters. They are also what gets created when a player places a poster on a wall.
+ * For the item form that can be spawned for players, see [/obj/item/poster]
+ */
+/obj/structure/sign/poster
+ name = "poster"
+ var/original_name
+ desc = "A large piece of space-resistant printed paper."
+ icon = 'icons/obj/poster.dmi'
+ anchored = TRUE
+ buildable_sign = FALSE //Cannot be unwrenched from a wall.
+ var/ruined = FALSE
+ var/random_basetype
+ var/never_random = FALSE // used for the 'random' subclasses.
+ ///Exclude posters of these types from being added to the random pool
+ var/list/blacklisted_types = list()
+ ///Whether the poster should be printable from library management computer. Mostly exists to keep directionals from being printed.
+ var/printable = FALSE
+
+ var/poster_item_name = "hypothetical poster"
+ var/poster_item_desc = "This hypothetical poster item should not exist, let's be honest here."
+ var/poster_item_icon_state = "rolled_poster"
+ var/poster_item_type = /obj/item/poster
+ ///A sharp shard of material can be hidden inside of a poster, attempts to embed when it is torn down.
+ var/datum/weakref/trap
+
+/obj/structure/sign/poster/Initialize(mapload)
+ . = ..()
+ if(random_basetype)
+ randomise(random_basetype)
+ if(!ruined)
+ original_name = name // can't use initial because of random posters
+ name = "poster - [name]"
+ desc = "A large piece of space-resistant printed paper. [desc]"
+
+ AddElement(/datum/element/beauty, 300)
+
+/// Adds contextual screentips
+/obj/structure/sign/poster/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if (!held_item)
+ if (ruined)
+ return .
+ context[SCREENTIP_CONTEXT_LMB] = "Rip up poster"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if (held_item.tool_behaviour == TOOL_WIRECUTTER)
+ if (ruined)
+ context[SCREENTIP_CONTEXT_LMB] = "Clean up remnants"
+ return CONTEXTUAL_SCREENTIP_SET
+ context[SCREENTIP_CONTEXT_LMB] = "Take down poster"
+ return CONTEXTUAL_SCREENTIP_SET
+ return .
+
+/obj/structure/sign/poster/proc/randomise(base_type)
+ var/list/poster_types = subtypesof(base_type)
+ if(length(blacklisted_types))
+ for(var/iterated_type in blacklisted_types)
+ poster_types -= typesof(iterated_type)
+ var/list/approved_types = list()
+ for(var/obj/structure/sign/poster/type_of_poster as anything in poster_types)
+ if(initial(type_of_poster.icon_state) && !initial(type_of_poster.never_random))
+ approved_types |= type_of_poster
+
+ var/obj/structure/sign/poster/selected = pick(approved_types)
+
+ name = initial(selected.name)
+ desc = initial(selected.desc)
+ icon_state = initial(selected.icon_state)
+ icon = initial(selected.icon)
+ poster_item_name = initial(selected.poster_item_name)
+ poster_item_desc = initial(selected.poster_item_desc)
+ poster_item_icon_state = initial(selected.poster_item_icon_state)
+ ruined = initial(selected.ruined)
+ if(length(GLOB.holidays) && prob(30)) // its the holidays! lets get festive
+ apply_holiday()
+ update_appearance()
+
+/// allows for posters to become festive posters during holidays
+/obj/structure/sign/poster/proc/apply_holiday()
+ if(!length(GLOB.holidays))
+ return
+ var/active_holiday = pick(GLOB.holidays)
+ var/datum/holiday/holi_data = GLOB.holidays[active_holiday]
+
+ if(holi_data.poster_name == "generic celebration poster")
+ return
+ name = holi_data.poster_name
+ desc = holi_data.poster_desc
+ icon_state = holi_data.poster_icon
+
+/obj/structure/sign/poster/attackby(obj/item/tool, mob/user, params)
+ if(tool.tool_behaviour == TOOL_WIRECUTTER)
+ tool.play_tool_sound(src, 100)
+ if(ruined)
+ to_chat(user, span_notice("You remove the remnants of the poster."))
+ qdel(src)
+ else
+ to_chat(user, span_notice("You carefully remove the poster from the wall."))
+ roll_and_drop(Adjacent(user) ? get_turf(user) : loc)
+
+/obj/structure/sign/poster/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ if(ruined)
+ return
+
+ visible_message(span_notice("[user] rips [src] in a single, decisive motion!") )
+ playsound(src.loc, 'sound/items/poster_ripped.ogg', 100, TRUE)
+ spring_trap(user)
+
+ var/obj/structure/sign/poster/ripped/R = new(loc)
+ R.pixel_y = pixel_y
+ R.pixel_x = pixel_x
+ R.add_fingerprint(user)
+ qdel(src)
+
+/obj/structure/sign/poster/proc/spring_trap(mob/user)
+ var/obj/item/shard/payload = trap?.resolve()
+ if (!payload)
+ return
+
+ to_chat(user, span_warning("There's something sharp behind this! What the hell?"))
+ if(!can_embed_trap(user) || !payload.tryEmbed(user.get_active_hand(), forced = TRUE))
+ visible_message(span_notice("A [payload.name] falls from behind the poster.") )
+ payload.forceMove(user.drop_location())
+ else
+ SEND_SIGNAL(src, COMSIG_POSTER_TRAP_SUCCEED, user)
+
+/obj/structure/sign/poster/proc/can_embed_trap(mob/living/carbon/human/user)
+ if (!istype(user) || HAS_TRAIT(user, TRAIT_PIERCEIMMUNE))
+ return FALSE
+ return !user.gloves || !(user.gloves.body_parts_covered & HANDS) || HAS_TRAIT(user, TRAIT_FINGERPRINT_PASSTHROUGH) || HAS_TRAIT(user.gloves, TRAIT_FINGERPRINT_PASSTHROUGH)
+
+/obj/structure/sign/poster/proc/roll_and_drop(atom/location)
+ pixel_x = 0
+ pixel_y = 0
+ var/obj/item/poster/rolled_poster = new poster_item_type(location, src) // /obj/structure/sign/poster/wanted/roll_and_drop() has some snowflake handling due to icon memes, if you make a major change to this, don't forget to update it too. <3
+ forceMove(rolled_poster)
+ return rolled_poster
+
+//separated to reduce code duplication. Moved here for ease of reference and to unclutter r_wall/attackby()
+/turf/closed/wall/proc/place_poster(obj/item/poster/rolled_poster, mob/user)
+ if(!rolled_poster.poster_structure)
+ to_chat(user, span_warning("[rolled_poster] has no poster... inside it? Inform a coder!"))
+ return
+
+ // Deny placing posters on currently-diagonal walls, although the wall may change in the future.
+ if (smoothing_flags & SMOOTH_DIAGONAL_CORNERS)
+ for (var/overlay in overlays)
+ var/image/new_image = overlay
+ if(copytext(new_image.icon_state, 1, 3) == "d-") //3 == length("d-") + 1
+ return
+
+ var/stuff_on_wall = 0
+ for(var/obj/contained_object in contents) //Let's see if it already has a poster on it or too much stuff
+ if(istype(contained_object, /obj/structure/sign/poster))
+ balloon_alert(user, "no room!")
+ return
+ stuff_on_wall++
+ if(stuff_on_wall == 3)
+ balloon_alert(user, "no room!")
+ return
+
+ balloon_alert(user, "hanging poster...")
+ var/obj/structure/sign/poster/placed_poster = rolled_poster.poster_structure
+
+ flick("poster_being_set", placed_poster)
+ placed_poster.forceMove(src) //deletion of the poster is handled in poster/Exited(), so don't have to worry about P anymore.
+ playsound(src, 'sound/items/poster_being_created.ogg', 100, TRUE)
+
+ var/turf/user_drop_location = get_turf(user) //cache this so it just falls to the ground if they move. also no tk memes allowed.
+ if(!do_after(user, PLACE_SPEED, placed_poster, extra_checks = CALLBACK(placed_poster, TYPE_PROC_REF(/obj/structure/sign/poster, snowflake_wall_turf_check), src)))
+ placed_poster.roll_and_drop(user_drop_location)
+ return
+
+ placed_poster.on_placed_poster(user)
+ return TRUE
+
+/obj/structure/sign/poster/proc/snowflake_wall_turf_check(atom/hopefully_still_a_wall_turf) //since turfs never get deleted but instead change type, make sure we're still being placed on a wall.
+ return iswallturf(hopefully_still_a_wall_turf)
+
+/obj/structure/sign/poster/proc/on_placed_poster(mob/user)
+ to_chat(user, span_notice("You place the poster!"))
+
+// Various possible posters follow
+
+/obj/structure/sign/poster/ripped
+ ruined = TRUE
+ icon_state = "poster_ripped"
+ name = "ripped poster"
+ desc = "You can't make out anything from the poster's original print. It's ruined."
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/ripped, 32)
+
+/obj/structure/sign/poster/random
+ name = "random poster" // could even be ripped
+ icon_state = "random_anything"
+ never_random = TRUE
+ random_basetype = /obj/structure/sign/poster
+ blacklisted_types = list(/obj/structure/sign/poster/traitor)
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sign/poster/random, 32)
+
+#undef PLACE_SPEED
diff --git a/code/game/objects/effects/spawners/random/food_or_drink.dm b/code/game/objects/effects/spawners/random/food_or_drink.dm
index a1b9976cab2c..e6cf9db1f82c 100644
--- a/code/game/objects/effects/spawners/random/food_or_drink.dm
+++ b/code/game/objects/effects/spawners/random/food_or_drink.dm
@@ -56,6 +56,7 @@
/obj/item/seeds/liberty = 5,
/obj/item/seeds/replicapod = 5,
/obj/item/seeds/reishi = 5,
+ /obj/item/seeds/seedling = 5,
/obj/item/seeds/nettle/death = 1,
/obj/item/seeds/plump/walkingmushroom = 1,
/obj/item/seeds/cannabis/rainbow = 1,
diff --git a/code/game/objects/effects/spawners/random/lavaland_mobs.dm b/code/game/objects/effects/spawners/random/lavaland_mobs.dm
index 18fa3ff2196d..2e3effdd30a6 100644
--- a/code/game/objects/effects/spawners/random/lavaland_mobs.dm
+++ b/code/game/objects/effects/spawners/random/lavaland_mobs.dm
@@ -8,11 +8,11 @@
loot = list(
/mob/living/basic/mining/bileworm = 1,
/mob/living/basic/mining/goliath = 1,
+ /mob/living/basic/mining/legion = 1,
/mob/living/basic/mining/lobstrosity/lava = 1,
/mob/living/basic/mining/watcher = 1,
- /mob/living/simple_animal/hostile/asteroid/brimdemon = 1,
+ /mob/living/basic/mining/brimdemon = 1,
/mob/living/basic/mining/goldgrub = 1,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 1,
)
/// Spawns random watcher variants during map generation
@@ -46,6 +46,6 @@
desc = "Chance to spawn a rare shiny version."
icon_state = "legion"
loot = list(
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 19,
- /mob/living/simple_animal/hostile/asteroid/hivelord/legion/dwarf = 1,
+ /mob/living/basic/mining/legion = 19,
+ /mob/living/basic/mining/legion/dwarf = 1,
)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index e3af3614fa1c..3af16ddff55c 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -212,9 +212,13 @@
var/offensive_notes
/// Used in obj/item/examine to determines whether or not to detail an item's statistics even if it does not meet the force requirements
var/override_notes = FALSE
+ /// Used if we want to have a custom verb text for throwing. "John Spaceman flicks the ciggerate" for example.
+ var/throw_verb
-/obj/item/Initialize(mapload)
+ /// A lazylist used for applying fantasy values, contains the actual modification applied to a variable.
+ var/list/fantasy_modifications
+/obj/item/Initialize(mapload)
if(attack_verb_continuous)
attack_verb_continuous = string_list(attack_verb_continuous)
if(attack_verb_simple)
@@ -1195,7 +1199,7 @@
return src
/**
- * tryEmbed() is for when you want to try embedding something without dealing with the damage + hit messages of calling hitby() on the item while targetting the target.
+ * tryEmbed() is for when you want to try embedding something without dealing with the damage + hit messages of calling hitby() on the item while targeting the target.
*
* Really, this is used mostly with projectiles with shrapnel payloads, from [/datum/element/embed/proc/checkEmbedProjectile], and called on said shrapnel. Mostly acts as an intermediate between different embed elements.
*
@@ -1584,3 +1588,41 @@
/obj/item/update_atom_colour()
. = ..()
update_slot_icon()
+
+/// Modifies the fantasy variable
+/obj/item/proc/modify_fantasy_variable(variable_key, value, bonus, minimum = 0)
+ if(LAZYACCESS(fantasy_modifications, variable_key) != null)
+ stack_trace("modify_fantasy_variable was called twice for the same key '[variable_key]' on type '[type]' before reset_fantasy_variable could be called!")
+ var/intended_target = value + bonus
+ value = max(minimum, intended_target)
+
+ var/difference = intended_target - value
+ var/modified_amount = bonus - difference
+ LAZYSET(fantasy_modifications, variable_key, modified_amount)
+ return value
+
+/// Returns the original fantasy variable value
+/obj/item/proc/reset_fantasy_variable(variable_key, current_value)
+ var/modification = LAZYACCESS(fantasy_modifications, variable_key)
+ LAZYREMOVE(fantasy_modifications, variable_key)
+ if(!modification)
+ return current_value
+ return current_value - modification
+
+/obj/item/proc/apply_fantasy_bonuses(bonus)
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_ITEM_APPLY_FANTASY_BONUSES, bonus)
+ force = modify_fantasy_variable("force", force, bonus)
+ throwforce = modify_fantasy_variable("throwforce", throwforce, bonus)
+ wound_bonus = modify_fantasy_variable("wound_bonus", wound_bonus, bonus)
+ bare_wound_bonus = modify_fantasy_variable("bare_wound_bonus", bare_wound_bonus, bonus)
+ toolspeed = modify_fantasy_variable("toolspeed", toolspeed, -bonus/10, minimum = 0.1)
+
+/obj/item/proc/remove_fantasy_bonuses(bonus)
+ SHOULD_CALL_PARENT(TRUE)
+ force = reset_fantasy_variable("force", force)
+ throwforce = reset_fantasy_variable("throwforce", throwforce)
+ wound_bonus = reset_fantasy_variable("wound_bonus", wound_bonus)
+ bare_wound_bonus = reset_fantasy_variable("bare_wound_bonus", bare_wound_bonus)
+ toolspeed = reset_fantasy_variable("toolspeed", toolspeed)
+ SEND_SIGNAL(src, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, bonus)
diff --git a/code/game/objects/items/cardboard_cutouts.dm b/code/game/objects/items/cardboard_cutouts.dm
index 570d59191116..6a3ead00ca07 100644
--- a/code/game/objects/items/cardboard_cutouts.dm
+++ b/code/game/objects/items/cardboard_cutouts.dm
@@ -6,6 +6,7 @@
icon_state = "cutout_basic"
w_class = WEIGHT_CLASS_BULKY
resistance_flags = FLAMMABLE
+ obj_flags = CAN_BE_HIT
item_flags = NO_PIXEL_RANDOM_DROP
/// If the cutout is pushed over and has to be righted
var/pushed_over = FALSE
@@ -60,32 +61,20 @@
/obj/item/cardboard_cutout/attackby(obj/item/I, mob/living/user, params)
if(istype(I, /obj/item/toy/crayon))
change_appearance(I, user)
- return
- // Why yes, this does closely resemble mob and object attack code.
- if(I.item_flags & NOBLUDGEON)
- return
- if(!I.force)
- playsound(loc, 'sound/weapons/tap.ogg', get_clamped_volume(), TRUE, -1)
- else if(I.hitsound)
- playsound(loc, I.hitsound, get_clamped_volume(), TRUE, -1)
-
- user.changeNext_move(CLICK_CD_MELEE)
- user.do_attack_animation(src)
-
- if(I.force)
- user.visible_message(span_danger("[user] hits [src] with [I]!"), \
- span_danger("You hit [src] with [I]!"))
- if(prob(I.force))
- push_over()
-
-/obj/item/cardboard_cutout/bullet_act(obj/projectile/P, def_zone, piercing_hit = FALSE)
- if(istype(P, /obj/projectile/bullet/reusable))
- P.on_hit(src, 0, piercing_hit)
- visible_message(span_danger("[src] is hit by [P]!"))
- playsound(src, 'sound/weapons/slice.ogg', 50, TRUE)
- if(prob(P.damage))
+ return TRUE
+
+ return ..()
+
+/obj/item/cardboard_cutout/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration)
+ . = ..()
+ var/damage_sustained = . || 0
+ if((damage_flag == BULLET || damage_flag == MELEE) && (damage_type == BRUTE) && prob(damage_sustained))
push_over()
- return BULLET_ACT_HIT
+
+/obj/item/cardboard_cutout/deconstruct(disassembled)
+ if(!(flags_1 & (HOLOGRAM_1|NODECONSTRUCT_1)))
+ new /obj/item/stack/sheet/cardboard(loc, 1)
+ return ..()
/proc/get_cardboard_cutout_instance(datum/cardboard_cutout/cardboard_cutout)
ASSERT(ispath(cardboard_cutout), "[cardboard_cutout] is not a path of /datum/cardboard_cutout")
diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm
index 21516c12ccf2..93768348d2ac 100644
--- a/code/game/objects/items/cards_ids.dm
+++ b/code/game/objects/items/cards_ids.dm
@@ -106,6 +106,15 @@
fire = 100
acid = 100
+/obj/item/card/id/apply_fantasy_bonuses(bonus)
+ . = ..()
+ if(bonus >= 15)
+ add_access(SSid_access.get_region_access_list(list(REGION_ALL_GLOBAL)), mode = FORCE_ADD_ALL)
+ else if(bonus >= 10)
+ add_access(SSid_access.get_region_access_list(list(REGION_ALL_STATION)), mode = FORCE_ADD_ALL)
+ else if(bonus <= -10)
+ clear_access()
+
/obj/item/card/id/Initialize(mapload)
. = ..()
diff --git a/code/game/objects/items/chainsaw.dm b/code/game/objects/items/chainsaw.dm
index 00d532189ab4..1d7c6c450382 100644
--- a/code/game/objects/items/chainsaw.dm
+++ b/code/game/objects/items/chainsaw.dm
@@ -27,6 +27,18 @@
///The looping sound for our chainsaw when running
var/datum/looping_sound/chainsaw/chainsaw_loop
+/obj/item/chainsaw/apply_fantasy_bonuses(bonus)
+ . = ..()
+ force_on = modify_fantasy_variable("force_on", force_on, bonus)
+ if(on)
+ force = force_on
+
+/obj/item/chainsaw/remove_fantasy_bonuses(bonus)
+ force_on = reset_fantasy_variable("force_on", force_on)
+ if(on)
+ force = force_on
+ return ..()
+
/obj/item/chainsaw/Initialize(mapload)
. = ..()
chainsaw_loop = new(src)
diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm
index eb9444784e21..4fc8935b3d49 100644
--- a/code/game/objects/items/cigs_lighters.dm
+++ b/code/game/objects/items/cigs_lighters.dm
@@ -138,6 +138,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
body_parts_covered = null
grind_results = list()
heat = 1000
+ throw_verb = "flick"
/// Whether this cigarette has been lit.
var/lit = FALSE
/// Whether this cigarette should start lit.
diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm
index 7ce9de6d2f86..673e30b04776 100644
--- a/code/game/objects/items/crayons.dm
+++ b/code/game/objects/items/crayons.dm
@@ -815,7 +815,8 @@
carbon_target.set_eye_blur_if_lower(6 SECONDS)
carbon_target.adjust_temp_blindness(2 SECONDS)
if(carbon_target.get_eye_protection() <= 0) // no eye protection? ARGH IT BURNS. Warning: don't add a stun here. It's a roundstart item with some quirks.
- carbon_target.apply_effects(eyeblur = 5, jitter = 10)
+ carbon_target.adjust_jitter(1 SECONDS)
+ carbon_target.adjust_eye_blur(0.5 SECONDS)
flash_color(carbon_target, flash_color=paint_color, flash_time=40)
if(ishuman(carbon_target) && actually_paints)
var/mob/living/carbon/human/human_target = carbon_target
diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm
index 6a94bb3598b3..6b2a1ef0eacd 100644
--- a/code/game/objects/items/devices/flashlight.dm
+++ b/code/game/objects/items/devices/flashlight.dm
@@ -233,7 +233,7 @@
holo_cooldown = world.time + 10 SECONDS
return
-// see: [/datum/wound/burn/proc/uv()]
+// see: [/datum/wound/burn/flesh/proc/uv()]
/obj/item/flashlight/pen/paramedic
name = "paramedic penlight"
desc = "A high-powered UV penlight intended to help stave off infection in the field on serious burned patients. Probably really bad to look into."
diff --git a/code/game/objects/items/devices/polycircuit.dm b/code/game/objects/items/devices/polycircuit.dm
index 9dbdbff993d7..5b7fd42d6f6b 100644
--- a/code/game/objects/items/devices/polycircuit.dm
+++ b/code/game/objects/items/devices/polycircuit.dm
@@ -51,7 +51,7 @@
else
to_chat(user, span_notice("You navigate the sharp edges of circuitry and remove a single board from [src]"))
else
- H.apply_damage(15, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ H.apply_damage(15, BRUTE, pick(GLOB.arm_zones))
to_chat(user, span_warning("You give yourself a wicked cut on [src]'s many sharp corners and edges!"))
/obj/item/stack/circuit_stack/full
diff --git a/code/game/objects/items/dna_probe.dm b/code/game/objects/items/dna_probe.dm
index 4ea89d0a95e7..6c9651a31421 100644
--- a/code/game/objects/items/dna_probe.dm
+++ b/code/game/objects/items/dna_probe.dm
@@ -135,7 +135,7 @@
to_chat(user, span_notice("You pull out the needle from [src] and flip the switch, and start injecting yourself with it."))
if(!do_after(user, CARP_MIX_DNA_TIMER))
return
- var/mob/living/simple_animal/hostile/space_dragon/new_dragon = user.change_mob_type(/mob/living/simple_animal/hostile/space_dragon, location = loc, delete_old_mob = TRUE)
+ var/mob/living/basic/space_dragon/new_dragon = user.change_mob_type(/mob/living/basic/space_dragon, location = loc, delete_old_mob = TRUE)
new_dragon.add_filter("anger_glow", 3, list("type" = "outline", "color" = "#ff330030", "size" = 5))
new_dragon.add_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert")
diff --git a/code/game/objects/items/food/monkeycube.dm b/code/game/objects/items/food/monkeycube.dm
index 5cf9db79fd01..e78d45ad2e83 100644
--- a/code/game/objects/items/food/monkeycube.dm
+++ b/code/game/objects/items/food/monkeycube.dm
@@ -8,7 +8,6 @@
foodtypes = MEAT | SUGAR
food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_TINY
- var/faction
var/spawned_mob = /mob/living/carbon/human/species/monkey
/obj/item/food/monkeycube/proc/Expand()
@@ -62,7 +61,7 @@
/datum/reagent/medicine/strange_reagent = 5,
)
tastes = list("the jungle" = 1, "bananas" = 1, "jimmies" = 1)
- spawned_mob = /mob/living/simple_animal/hostile/gorilla
+ spawned_mob = /mob/living/basic/gorilla
/obj/item/food/monkeycube/chicken
name = "chicken cube"
diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm
index 404c66640c4b..aa0e506dba31 100644
--- a/code/game/objects/items/grenades/_grenade.dm
+++ b/code/game/objects/items/grenades/_grenade.dm
@@ -66,6 +66,29 @@
if(!QDELETED(src))
qdel(src)
+/obj/item/grenade/apply_fantasy_bonuses(bonus)
+ . = ..()
+ apply_grenade_fantasy_bonuses(bonus)
+
+/obj/item/grenade/remove_fantasy_bonuses(bonus)
+ remove_grenade_fantasy_bonuses(bonus)
+ return ..()
+
+/obj/item/grenade/proc/apply_grenade_fantasy_bonuses(quality)
+ if(ex_dev == 0 && ex_heavy == 0 && ex_light == 0 && ex_flame == 0)
+ return
+ var/devIncrease = round(quality / 10)
+ var/heavyIncrease = round(quality / 5)
+ var/lightIncrease = round(quality / 2)
+ ex_dev = modify_fantasy_variable("ex_dev", ex_dev, devIncrease, 0)
+ ex_heavy = modify_fantasy_variable("ex_heavy", ex_heavy, heavyIncrease, 0)
+ ex_light = modify_fantasy_variable("ex_light", ex_light, lightIncrease, 0)
+
+/obj/item/grenade/proc/remove_grenade_fantasy_bonuses(quality)
+ ex_dev = reset_fantasy_variable("ex_dev", ex_dev)
+ ex_heavy = reset_fantasy_variable("ex_heavy", ex_heavy)
+ ex_light = reset_fantasy_variable("ex_light", ex_light)
+
/**
* Checks for various ways to botch priming a grenade.
*
diff --git a/code/game/objects/items/grenades/chem_grenade.dm b/code/game/objects/items/grenades/chem_grenade.dm
index 06d60173bd84..6871c4ed0953 100644
--- a/code/game/objects/items/grenades/chem_grenade.dm
+++ b/code/game/objects/items/grenades/chem_grenade.dm
@@ -34,6 +34,12 @@
stage_change() // If no argument is set, it will change the stage to the current stage, useful for stock grenades that start READY.
wires = new /datum/wires/explosive/chem_grenade(src)
+/obj/item/grenade/chem_grenade/apply_grenade_fantasy_bonuses(quality)
+ threatscale = modify_fantasy_variable("threatscale", threatscale, quality/10)
+
+/obj/item/grenade/chem_grenade/remove_grenade_fantasy_bonuses(quality)
+ threatscale = reset_fantasy_variable("threatscale", threatscale)
+
/obj/item/grenade/chem_grenade/examine(mob/user)
display_timer = (stage == GRENADE_READY) //show/hide the timer based on assembly state
. = ..()
diff --git a/code/game/objects/items/grenades/clusterbuster.dm b/code/game/objects/items/grenades/clusterbuster.dm
index a12c302c188b..e4862919c1c1 100644
--- a/code/game/objects/items/grenades/clusterbuster.dm
+++ b/code/game/objects/items/grenades/clusterbuster.dm
@@ -18,6 +18,14 @@
var/max_spawned = 8
var/segment_chance = 35
+/obj/item/grenade/clusterbuster/apply_grenade_fantasy_bonuses(quality)
+ min_spawned = modify_fantasy_variable("min_spawned", min_spawned, round(quality/2))
+ max_spawned = modify_fantasy_variable("max_spawned", max_spawned, round(quality/2))
+
+/obj/item/grenade/clusterbuster/remove_grenade_fantasy_bonuses(quality)
+ min_spawned = reset_fantasy_variable("min_spawned", min_spawned)
+ max_spawned = reset_fantasy_variable("max_spawned", max_spawned)
+
/obj/item/grenade/clusterbuster/detonate(mob/living/lanced_by)
. = ..()
if(!.)
diff --git a/code/game/objects/items/grenades/flashbang.dm b/code/game/objects/items/grenades/flashbang.dm
index cd5e43c2daf2..c472b2698b42 100644
--- a/code/game/objects/items/grenades/flashbang.dm
+++ b/code/game/objects/items/grenades/flashbang.dm
@@ -6,6 +6,12 @@
righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
var/flashbang_range = 7 //how many tiles away the mob will be stunned.
+/obj/item/grenade/flashbang/apply_grenade_fantasy_bonuses(quality)
+ flashbang_range = modify_fantasy_variable("flashbang_range", flashbang_range, quality)
+
+/obj/item/grenade/flashbang/remove_grenade_fantasy_bonuses(quality)
+ flashbang_range = reset_fantasy_variable("flashbang_range", flashbang_range)
+
/obj/item/grenade/flashbang/detonate(mob/living/lanced_by)
. = ..()
if(!.)
@@ -105,7 +111,7 @@
living_mob.Paralyze(20)
living_mob.Knockdown(200)
living_mob.soundbang_act(1, 200, 10, 15)
- if(living_mob.apply_damages(10, 10))
+ if(living_mob.apply_damages(brute = 10, burn = 10))
to_chat(living_mob, span_userdanger("The blast from \the [src] bruises and burns you!"))
// only checking if they're on top of the tile, cause being one tile over will be its own punishment
diff --git a/code/game/objects/items/grenades/hypno.dm b/code/game/objects/items/grenades/hypno.dm
index 035533d47310..0d7601df204a 100644
--- a/code/game/objects/items/grenades/hypno.dm
+++ b/code/game/objects/items/grenades/hypno.dm
@@ -7,6 +7,12 @@
righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
var/flashbang_range = 7
+/obj/item/grenade/hypnotic/apply_grenade_fantasy_bonuses(quality)
+ flashbang_range = modify_fantasy_variable("flashbang_range", flashbang_range, quality)
+
+/obj/item/grenade/hypnotic/remove_grenade_fantasy_bonuses(quality)
+ flashbang_range = reset_fantasy_variable("flashbang_range", flashbang_range)
+
/obj/item/grenade/hypnotic/detonate(mob/living/lanced_by)
. = ..()
if(!.)
diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm
index 363bd3e17657..10293d9e1335 100644
--- a/code/game/objects/items/grenades/plastic.dm
+++ b/code/game/objects/items/grenades/plastic.dm
@@ -23,6 +23,19 @@
/// Maximum timer for c4 charges
var/maximum_timer = 60000
+/obj/item/grenade/c4/apply_grenade_fantasy_bonuses(quality)
+ var/devIncrease = round(quality / 10)
+ var/heavyIncrease = round(quality / 5)
+ var/lightIncrease = round(quality / 2)
+ boom_sizes[1] = modify_fantasy_variable("devIncrease", boom_sizes[1], devIncrease)
+ boom_sizes[2] = modify_fantasy_variable("heavyIncrease", boom_sizes[2], heavyIncrease)
+ boom_sizes[3] = modify_fantasy_variable("lightIncrease", boom_sizes[3], lightIncrease)
+
+/obj/item/grenade/c4/remove_grenade_fantasy_bonuses(quality)
+ boom_sizes[1] = reset_fantasy_variable("devIncrease", boom_sizes[1])
+ boom_sizes[2] = reset_fantasy_variable("heavyIncrease", boom_sizes[2])
+ boom_sizes[3] = reset_fantasy_variable("lightIncrease", boom_sizes[3])
+
/obj/item/grenade/c4/Initialize(mapload)
. = ..()
AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES)
diff --git a/code/game/objects/items/grenades/spawnergrenade.dm b/code/game/objects/items/grenades/spawnergrenade.dm
index 3e8c81054f12..1b9d9ff27d09 100644
--- a/code/game/objects/items/grenades/spawnergrenade.dm
+++ b/code/game/objects/items/grenades/spawnergrenade.dm
@@ -7,6 +7,12 @@
var/spawner_type = null // must be an object path
var/deliveryamt = 1 // amount of type to deliver
+/obj/item/grenade/spawnergrenade/apply_grenade_fantasy_bonuses(quality)
+ deliveryamt = modify_fantasy_variable("deliveryamt", deliveryamt, quality)
+
+/obj/item/grenade/spawnergrenade/remove_grenade_fantasy_bonuses(quality)
+ deliveryamt = reset_fantasy_variable("deliveryamt", deliveryamt)
+
/obj/item/grenade/spawnergrenade/detonate(mob/living/lanced_by) // Prime now just handles the two loops that query for people in lockers and people who can see it.
. = ..()
if(!.)
@@ -55,7 +61,7 @@
desc = "A sleek device often given to clowns on their 10th birthdays for protection. You can hear faint scratching coming from within."
icon_state = "clown_ball"
inhand_icon_state = null
- spawner_type = list(/mob/living/simple_animal/hostile/retaliate/clown/fleshclown, /mob/living/simple_animal/hostile/retaliate/clown/clownhulk, /mob/living/simple_animal/hostile/retaliate/clown/longface, /mob/living/simple_animal/hostile/retaliate/clown/clownhulk/chlown, /mob/living/simple_animal/hostile/retaliate/clown/clownhulk/honcmunculus, /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton, /mob/living/simple_animal/hostile/retaliate/clown/banana, /mob/living/simple_animal/hostile/retaliate/clown/honkling, /mob/living/simple_animal/hostile/retaliate/clown/lube)
+ spawner_type = list(/mob/living/basic/clown/fleshclown, /mob/living/basic/clown/clownhulk, /mob/living/basic/clown/longface, /mob/living/basic/clown/clownhulk/chlown, /mob/living/basic/clown/clownhulk/honkmunculus, /mob/living/basic/clown/mutant/glutton, /mob/living/basic/clown/banana, /mob/living/basic/clown/honkling, /mob/living/basic/clown/lube)
deliveryamt = 1
/obj/item/grenade/spawnergrenade/clown_broken
@@ -63,5 +69,5 @@
desc = "A sleek device often given to clowns on their 10th birthdays for protection. While a typical C.L.U.W.N.E only holds one creature, sometimes foolish young clowns try to cram more in, often to disasterous effect."
icon_state = "clown_broken"
inhand_icon_state = null
- spawner_type = /mob/living/simple_animal/hostile/retaliate/clown/mutant
+ spawner_type = /mob/living/basic/clown/mutant
deliveryamt = 5
diff --git a/code/game/objects/items/hand_items.dm b/code/game/objects/items/hand_items.dm
index c2ddc1ea732a..5349ea85b23e 100644
--- a/code/game/objects/items/hand_items.dm
+++ b/code/game/objects/items/hand_items.dm
@@ -128,7 +128,7 @@
return FALSE
var/obj/item/bodypart/head/the_head = target.get_bodypart(BODY_ZONE_HEAD)
- if(!(the_head.biological_state & BIO_FLESH) || !IS_ORGANIC_LIMB(the_head))
+ if(!(the_head.biological_state & BIO_FLESH))
to_chat(user, span_warning("You can't noogie [target], [target.p_they()] [target.p_have()] no skin on [target.p_their()] head!"))
return
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index 3d121f854f87..a24aeb45ce4b 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -40,6 +40,7 @@
throw_range = 5
custom_materials = list(/datum/material/iron=500)
breakouttime = 1 MINUTES
+ var/handcuff_time = 3 SECONDS
armor_type = /datum/armor/restraints_handcuffs
custom_price = PAYCHECK_COMMAND * 0.35
///Sound that plays when starting to put handcuffs on someone
@@ -47,6 +48,14 @@
///If set, handcuffs will be destroyed on application and leave behind whatever this is set to.
var/trashtype = null
+/obj/item/restraints/handcuffs/apply_fantasy_bonuses(bonus)
+ . = ..()
+ handcuff_time = modify_fantasy_variable("handcuff_time", handcuff_time, -bonus * 2, minimum = 0.3 SECONDS)
+
+/obj/item/restraints/handcuffs/remove_fantasy_bonuses(bonus)
+ handcuff_time = reset_fantasy_variable("handcuff_time", handcuff_time)
+ return ..()
+
/datum/armor/restraints_handcuffs
fire = 50
acid = 50
@@ -70,7 +79,7 @@
to_chat(C, span_userdanger("As you feel someone grab your wrists, [src] start digging into your skin!"))
playsound(loc, cuffsound, 30, TRUE, -2)
log_combat(user, C, "attempted to handcuff")
- if(do_after(user, 3 SECONDS, C, timed_action_flags = IGNORE_SLOWDOWNS) && C.canBeHandcuffed())
+ if(do_after(user, handcuff_time, C, timed_action_flags = IGNORE_SLOWDOWNS) && C.canBeHandcuffed())
if(iscyborg(user))
apply_cuffs(C, user, TRUE)
else
diff --git a/code/game/objects/items/holy_weapons.dm b/code/game/objects/items/holy_weapons.dm
index 36a3a643fde0..7a3f7a2e18fc 100644
--- a/code/game/objects/items/holy_weapons.dm
+++ b/code/game/objects/items/holy_weapons.dm
@@ -29,9 +29,9 @@
success_forcesay = "BEGONE FOUL MAGIKS!!", \
tip_text = "Clear rune", \
on_clear_callback = CALLBACK(src, PROC_REF(on_cult_rune_removed)), \
- effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune))
-
- AddElement(/datum/element/bane, target_type = /mob/living/simple_animal/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
+ effects_we_clear = list(/obj/effect/rune, /obj/effect/heretic_rune, /obj/effect/cosmic_rune), \
+ )
+ AddElement(/datum/element/bane, target_type = /mob/living/basic/revenant, damage_multiplier = 0, added_damage = 25, requires_combat_mode = FALSE)
if(!GLOB.holy_weapon_type && type == /obj/item/nullrod)
var/list/rods = list()
diff --git a/code/game/objects/items/melee/baton.dm b/code/game/objects/items/melee/baton.dm
index 0b8c9b0a4da9..70b0330305a9 100644
--- a/code/game/objects/items/melee/baton.dm
+++ b/code/game/objects/items/melee/baton.dm
@@ -42,22 +42,22 @@
/// Boolean on whether people with chunky fingers can use this baton.
var/chunky_finger_usable = FALSE
- /// The context to show when the baton is active and targetting a living thing
+ /// The context to show when the baton is active and targeting a living thing
var/context_living_target_active = "Stun"
- /// The context to show when the baton is active and targetting a living thing in combat mode
+ /// The context to show when the baton is active and targeting a living thing in combat mode
var/context_living_target_active_combat_mode = "Stun"
- /// The context to show when the baton is inactive and targetting a living thing
+ /// The context to show when the baton is inactive and targeting a living thing
var/context_living_target_inactive = "Prod"
- /// The context to show when the baton is inactive and targetting a living thing in combat mode
+ /// The context to show when the baton is inactive and targeting a living thing in combat mode
var/context_living_target_inactive_combat_mode = "Attack"
- /// The RMB context to show when the baton is active and targetting a living thing
+ /// The RMB context to show when the baton is active and targeting a living thing
var/context_living_rmb_active = "Attack"
- /// The RMB context to show when the baton is inactive and targetting a living thing
+ /// The RMB context to show when the baton is inactive and targeting a living thing
var/context_living_rmb_inactive = "Attack"
/obj/item/melee/baton/Initialize(mapload)
@@ -95,6 +95,15 @@
if(BATON_ATTACKING)
finalize_baton_attack(target, user, modifiers)
+/obj/item/melee/baton/apply_fantasy_bonuses(bonus)
+ . = ..()
+ stamina_damage = modify_fantasy_variable("stamina_damage", stamina_damage, bonus * 4)
+
+
+/obj/item/melee/baton/remove_fantasy_bonuses(bonus)
+ stamina_damage = reset_fantasy_variable("stamina_damage", stamina_damage)
+ return ..()
+
/obj/item/melee/baton/add_item_context(datum/source, list/context, atom/target, mob/living/user)
if (isturf(target))
return NONE
diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm
index 83a304584043..a0fd7b64e2d9 100644
--- a/code/game/objects/items/melee/misc.dm
+++ b/code/game/objects/items/melee/misc.dm
@@ -208,6 +208,7 @@
shard.countdown = null
START_PROCESSING(SSobj, src)
visible_message(span_warning("[src] appears, balanced ever so perfectly on its hilt. This isn't ominous at all."))
+ RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(eat_bullets))
/obj/item/melee/supermatter_sword/process()
if(balanced || throwing || ismob(src.loc) || isnull(src.loc))
@@ -254,11 +255,16 @@
consume_everything()
return TRUE
-/obj/item/melee/supermatter_sword/bullet_act(obj/projectile/projectile)
- visible_message(span_danger("[projectile] smacks into [src] and rapidly flashes to ash."),\
- span_hear("You hear a loud crack as you are washed with a wave of heat."))
- consume_everything(projectile)
- return BULLET_ACT_HIT
+/obj/item/melee/supermatter_sword/proc/eat_bullets(datum/source, obj/projectile/hitting_projectile)
+ SIGNAL_HANDLER
+
+ visible_message(
+ span_danger("[hitting_projectile] smacks into [source] and rapidly flashes to ash."),
+ null,
+ span_hear("You hear a loud crack as you are washed with a wave of heat."),
+ )
+ consume_everything(hitting_projectile)
+ return COMPONENT_BULLET_BLOCKED
/obj/item/melee/supermatter_sword/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] touches [src]'s blade. It looks like [user.p_theyre()] tired of waiting for the radiation to kill [user.p_them()]!"))
diff --git a/code/game/objects/items/mop.dm b/code/game/objects/items/mop.dm
index 1c854e5091b6..77e1c489391c 100644
--- a/code/game/objects/items/mop.dm
+++ b/code/game/objects/items/mop.dm
@@ -25,6 +25,14 @@
/obj/structure/mop_bucket,
))
+/obj/item/mop/apply_fantasy_bonuses(bonus)
+ . = ..()
+ mopspeed = modify_fantasy_variable("mopspeed", mopspeed, -bonus)
+
+/obj/item/mop/remove_fantasy_bonuses(bonus)
+ mopspeed = reset_fantasy_variable("mopspeed", mopspeed)
+ return ..()
+
/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)))
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index 1336a043e7f5..e848a972b7bf 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -545,30 +545,30 @@
/obj/item/borg/upgrade/expand/action(mob/living/silicon/robot/robot, user = usr)
. = ..()
- if(.)
+ if(!. || HAS_TRAIT(robot, TRAIT_NO_TRANSFORM))
+ return FALSE
- if(robot.hasExpanded)
- to_chat(usr, span_warning("This unit already has an expand module installed!"))
- return FALSE
+ if(robot.hasExpanded)
+ to_chat(usr, span_warning("This unit already has an expand module installed!"))
+ return FALSE
- robot.notransform = TRUE
- var/prev_lockcharge = robot.lockcharge
- robot.SetLockdown(TRUE)
- robot.set_anchored(TRUE)
- var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(1, holder = robot, location = robot.loc)
- smoke.start()
- sleep(0.2 SECONDS)
- for(var/i in 1 to 4)
- playsound(robot, pick('sound/items/drill_use.ogg', 'sound/items/jaws_cut.ogg', 'sound/items/jaws_pry.ogg', 'sound/items/welder.ogg', 'sound/items/ratchet.ogg'), 80, TRUE, -1)
- sleep(1.2 SECONDS)
- if(!prev_lockcharge)
- robot.SetLockdown(FALSE)
- robot.set_anchored(FALSE)
- robot.notransform = FALSE
- robot.resize = 2
- robot.hasExpanded = TRUE
- robot.update_transform()
+ ADD_TRAIT(robot, TRAIT_NO_TRANSFORM, REF(src))
+ var/prev_lockcharge = robot.lockcharge
+ robot.SetLockdown(TRUE)
+ robot.set_anchored(TRUE)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.set_up(1, holder = robot, location = robot.loc)
+ smoke.start()
+ sleep(0.2 SECONDS)
+ for(var/i in 1 to 4)
+ playsound(robot, pick('sound/items/drill_use.ogg', 'sound/items/jaws_cut.ogg', 'sound/items/jaws_pry.ogg', 'sound/items/welder.ogg', 'sound/items/ratchet.ogg'), 80, TRUE, -1)
+ sleep(1.2 SECONDS)
+ if(!prev_lockcharge)
+ robot.SetLockdown(FALSE)
+ robot.set_anchored(FALSE)
+ REMOVE_TRAIT(robot, TRAIT_NO_TRANSFORM, REF(src))
+ robot.hasExpanded = TRUE
+ robot.update_transform(2)
/obj/item/borg/upgrade/expand/deactivate(mob/living/silicon/robot/R, user = usr)
. = ..()
diff --git a/code/game/objects/items/shooting_range.dm b/code/game/objects/items/shooting_range.dm
index 39ffdd85b252..e6d7eb18af87 100644
--- a/code/game/objects/items/shooting_range.dm
+++ b/code/game/objects/items/shooting_range.dm
@@ -4,28 +4,74 @@
icon = 'icons/obj/objects.dmi'
icon_state = "target_h"
density = FALSE
- var/hp = 1800
+ max_integrity = 1800
+ item_flags = CAN_BE_HIT
/// Lazylist to keep track of bullet-hole overlays.
var/list/bullethole_overlays
-/obj/item/target/welder_act(mob/living/user, obj/item/I)
- ..()
- if(I.use_tool(src, user, 0, volume=40))
- for (var/bullethole in bullethole_overlays)
- cut_overlay(bullethole)
- bullethole_overlays = null
- to_chat(user, span_notice("You slice off [src]'s uneven chunks of aluminium and scorch marks."))
+/obj/item/target/welder_act(mob/living/user, obj/item/tool)
+ if(tool.use_tool(src, user, 0 SECONDS, volume = 40))
+ LAZYNULL(bullethole_overlays)
+ balloon_alert(user, "target repaired")
+ update_appearance(UPDATE_OVERLAYS)
return TRUE
+/obj/item/target/update_overlays()
+ . = ..()
+ . |= bullethole_overlays
+
+/obj/item/target/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ if(prob(25))
+ return ..() // RNG change to just not leave a mark, like walls
+ if(length(overlays) > 35)
+ return ..() // Too many bullets, we're done here
+
+ // Projectiles which do not deal damage will not leave dent / scorch mark graphics.
+ // However we snowflake some projectiles to leave them anyway, because they're appropriate.
+ var/static/list/always_leave_marks
+ if(isnull(always_leave_marks))
+ always_leave_marks = typecacheof(list(
+ /obj/projectile/beam/practice,
+ ))
+
+ var/is_invalid_damage = hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN
+ var/is_safe = !hitting_projectile.is_hostile_projectile()
+ var/is_generic_projectile = !is_type_in_typecache(hitting_projectile, always_leave_marks)
+ if(is_generic_projectile && (is_invalid_damage || is_safe))
+ return ..() // Don't bother unless it's real shit
+
+ var/p_x = hitting_projectile.p_x + pick(0, 0, 0, 0, 0, -1, 1) // really ugly way of coding "sometimes offset p_x!"
+ var/p_y = hitting_projectile.p_y + pick(0, 0, 0, 0, 0, -1, 1)
+ var/icon/our_icon = icon(icon, icon_state)
+ if(!our_icon.GetPixel(p_x, p_y) || hitting_projectile.original != src)
+ return BULLET_ACT_FORCE_PIERCE // We, "missed", I guess?
+
+ . = ..()
+ if(. != BULLET_ACT_HIT)
+ return
+
+ var/image/bullet_hole = image('icons/effects/effects.dmi', "dent", OBJ_LAYER + 0.5)
+ bullet_hole.pixel_x = p_x - 1 //offset correction
+ bullet_hole.pixel_y = p_y - 1
+ if(hitting_projectile.damage_type != BRUTE)
+ bullet_hole.setDir(pick(GLOB.cardinals))// random scorch design
+ if(hitting_projectile.damage < 20 && is_generic_projectile)
+ bullet_hole.icon_state = "light_scorch"
+ else
+ bullet_hole.icon_state = "scorch"
+
+ LAZYADD(bullethole_overlays, bullet_hole)
+ update_appearance(UPDATE_OVERLAYS)
+
/obj/item/target/syndicate
icon_state = "target_s"
desc = "A shooting target that looks like syndicate scum."
- hp = 2600
+ max_integrity = 2600
/obj/item/target/alien
icon_state = "target_q"
desc = "A shooting target that looks like a xenomorphic alien."
- hp = 2350
+ max_integrity = 2350
/obj/item/target/alien/anchored
anchored = TRUE
@@ -33,44 +79,8 @@
/obj/item/target/clown
icon_state = "target_c"
desc = "A shooting target that looks like a useless clown."
- hp = 2000
-
-#define DECALTYPE_SCORCH 1
-#define DECALTYPE_BULLET 2
+ max_integrity = 2000
/obj/item/target/clown/bullet_act(obj/projectile/P)
. = ..()
- playsound(src.loc, 'sound/items/bikehorn.ogg', 50, TRUE)
-
-/obj/item/target/bullet_act(obj/projectile/P)
- if(istype(P, /obj/projectile/bullet/reusable)) // If it's a foam dart, don't bother with any of this other shit
- return P.on_hit(src, 0)
- var/p_x = P.p_x + pick(0,0,0,0,0,-1,1) // really ugly way of coding "sometimes offset P.p_x!"
- var/p_y = P.p_y + pick(0,0,0,0,0,-1,1)
- var/decaltype = DECALTYPE_SCORCH
- if(istype(P, /obj/projectile/bullet))
- decaltype = DECALTYPE_BULLET
- var/icon/C = icon(icon,icon_state)
- if(C.GetPixel(p_x, p_y) && P.original == src && overlays.len <= 35) // if the located pixel isn't blank (null)
- hp -= P.damage
- if(hp <= 0)
- visible_message(span_danger("[src] breaks into tiny pieces and collapses!"))
- qdel(src)
- var/image/bullet_hole = image('icons/effects/effects.dmi', "scorch", OBJ_LAYER + 0.5)
- bullet_hole.pixel_x = p_x - 1 //offset correction
- bullet_hole.pixel_y = p_y - 1
- if(decaltype == DECALTYPE_SCORCH)
- bullet_hole.setDir(pick(NORTH,SOUTH,EAST,WEST))// random scorch design
- if(P.damage >= 20 || istype(P, /obj/projectile/beam/practice))
- bullet_hole.setDir(pick(NORTH,SOUTH,EAST,WEST))
- else
- bullet_hole.icon_state = "light_scorch"
- else
- bullet_hole.icon_state = "dent"
- LAZYADD(bullethole_overlays, bullet_hole)
- add_overlay(bullet_hole)
- return BULLET_ACT_HIT
- return BULLET_ACT_FORCE_PIERCE
-
-#undef DECALTYPE_SCORCH
-#undef DECALTYPE_BULLET
+ playsound(src, 'sound/items/bikehorn.ogg', 50, TRUE)
diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm
index 49a7b4d8c4ad..791088890dac 100644
--- a/code/game/objects/items/stacks/medical.dm
+++ b/code/game/objects/items/stacks/medical.dm
@@ -11,7 +11,7 @@
resistance_flags = FLAMMABLE
max_integrity = 40
novariants = FALSE
- item_flags = NOBLUDGEON
+ item_flags = NOBLUDGEON|SKIP_FANTASY_ON_SPAWN
cost = 250
source = /datum/robot_energy_storage/medical
merge_type = /obj/item/stack/medical
@@ -36,6 +36,28 @@
. = ..()
try_heal(patient, user)
+/obj/item/stack/medical/apply_fantasy_bonuses(bonus)
+ . = ..()
+ if(heal_brute)
+ heal_brute = modify_fantasy_variable("heal_brute", heal_brute, bonus)
+ if(heal_burn)
+ heal_burn = modify_fantasy_variable("heal_burn", heal_burn, bonus)
+ if(stop_bleeding)
+ stop_bleeding = modify_fantasy_variable("stop_bleeding", stop_bleeding, bonus/10)
+ if(sanitization)
+ sanitization = modify_fantasy_variable("sanitization", sanitization, bonus/10)
+ if(flesh_regeneration)
+ flesh_regeneration = modify_fantasy_variable("flesh_regeneration", flesh_regeneration, bonus/10)
+
+/obj/item/stack/medical/remove_fantasy_bonuses(bonus)
+ heal_brute = reset_fantasy_variable("heal_brute", heal_brute)
+ heal_burn = reset_fantasy_variable("heal_burn", heal_burn)
+ stop_bleeding = reset_fantasy_variable("stop_bleeding", stop_bleeding)
+ sanitization = reset_fantasy_variable("sanitization", sanitization)
+ flesh_regeneration = reset_fantasy_variable("flesh_regeneration", flesh_regeneration)
+ return ..()
+
+
/// In which we print the message that we're starting to heal someone, then we try healing them. Does the do_after whether or not it can actually succeed on a targeted mob
/obj/item/stack/medical/proc/try_heal(mob/living/patient, mob/user, silent = FALSE)
if(!patient.try_inject(user, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE))
@@ -399,11 +421,11 @@
patient.emote("scream")
for(var/i in patient.bodyparts)
- var/obj/item/bodypart/bone = i
- var/datum/wound/blunt/severe/oof_ouch = new
- oof_ouch.apply_wound(bone)
- var/datum/wound/blunt/critical/oof_OUCH = new
- oof_OUCH.apply_wound(bone)
+ var/obj/item/bodypart/bone = i // fine to just, use these raw, its a meme anyway
+ var/datum/wound/blunt/bone/severe/oof_ouch = new
+ oof_ouch.apply_wound(bone, wound_source = "bone gel")
+ var/datum/wound/blunt/bone/critical/oof_OUCH = new
+ oof_OUCH.apply_wound(bone, wound_source = "bone gel")
for(var/i in patient.bodyparts)
var/obj/item/bodypart/bone = i
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 5d7a3715850f..252ff4092354 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -305,6 +305,7 @@ GLOBAL_LIST_INIT(wood_recipes, list ( \
new/datum/stack_recipe("wooden barricade", /obj/structure/barricade/wooden, 5, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
new/datum/stack_recipe("wooden door", /obj/structure/mineral_door/wood, 10, time = 2 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_DOORS), \
new/datum/stack_recipe("wooden stairs frame", /obj/structure/stairs_frame/wood, 10, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
+ new/datum/stack_recipe("wooden fence", /obj/structure/railing/wooden_fence, 2, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
new/datum/stack_recipe("coffin", /obj/structure/closet/crate/coffin, 5, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
new/datum/stack_recipe("book case", /obj/structure/bookcase, 4, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
new/datum/stack_recipe("drying rack", /obj/machinery/smartfridge/drying_rack, 10, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_TOOLS), \
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index 4b24b70447bc..11f556479ae2 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -20,6 +20,7 @@
gender = PLURAL
material_modifier = 0.05 //5%, so that a 50 sheet stack has the effect of 5k materials instead of 100k.
max_integrity = 100
+ item_flags = SKIP_FANTASY_ON_SPAWN
/// A list to all recipies this stack item can create.
var/list/datum/stack_recipe/recipes
/// What's the name of just 1 of this stack. You have a stack of leather, but one piece of leather
@@ -575,6 +576,8 @@
var/obj/machinery/machine = loc
if(!(src in machine.component_parts) || !(check in machine.component_parts))
return FALSE
+ if(SEND_SIGNAL(src, COMSIG_STACK_CAN_MERGE, check, inhand) & CANCEL_STACK_MERGE)
+ return FALSE
return TRUE
/**
diff --git a/code/game/objects/items/storage/book.dm b/code/game/objects/items/storage/book.dm
index 7862cf08bd81..03cbd4c6a6e4 100644
--- a/code/game/objects/items/storage/book.dm
+++ b/code/game/objects/items/storage/book.dm
@@ -235,7 +235,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list("bible", "koran", "scrapbook", "burning",
playsound(src,'sound/effects/pray_chaplain.ogg',60,TRUE)
for(var/obj/item/soulstone/SS in sword.contents)
SS.required_role = null
- for(var/mob/living/simple_animal/shade/EX in SS)
+ for(var/mob/living/basic/shade/EX in SS)
var/datum/antagonist/cult/cultist = EX.mind.has_antag_datum(/datum/antagonist/cult)
if (cultist)
cultist.silent = TRUE
diff --git a/code/game/objects/items/storage/secure.dm b/code/game/objects/items/storage/secure.dm
index 217d4f32571e..cba6a7c2818a 100644
--- a/code/game/objects/items/storage/secure.dm
+++ b/code/game/objects/items/storage/secure.dm
@@ -40,7 +40,7 @@
. = ..()
icon_state = "[initial(icon_state)][atom_storage?.locked ? "_locked" : null]"
-/obj/item/storage/secure/tool_act(mob/living/user, obj/item/tool)
+/obj/item/storage/secure/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(can_hack_open && atom_storage.locked)
return ..()
else
diff --git a/code/game/objects/items/storage/storage.dm b/code/game/objects/items/storage/storage.dm
index 98b8c243a40b..4ecb596ec50b 100644
--- a/code/game/objects/items/storage/storage.dm
+++ b/code/game/objects/items/storage/storage.dm
@@ -10,6 +10,28 @@
/// What storage type to use for this item
var/datum/storage/storage_type = /datum/storage
+/obj/item/storage/apply_fantasy_bonuses(bonus)
+ . = ..()
+ atom_storage.max_slots = modify_fantasy_variable("max_slots", atom_storage.max_slots, round(bonus/2))
+ atom_storage.max_total_storage = modify_fantasy_variable("max_total_storage", atom_storage.max_total_storage, round(bonus/2))
+ LAZYSET(fantasy_modifications, "max_specific_storage", atom_storage.max_specific_storage)
+ if(bonus >= 15)
+ atom_storage.max_specific_storage = max(WEIGHT_CLASS_HUGE, atom_storage.max_specific_storage)
+ else if(bonus >= 10)
+ atom_storage.max_specific_storage = max(WEIGHT_CLASS_BULKY, atom_storage.max_specific_storage)
+ else if(bonus <= -10)
+ atom_storage.max_specific_storage = WEIGHT_CLASS_SMALL
+ else if(bonus <= -15)
+ atom_storage.max_specific_storage = WEIGHT_CLASS_TINY
+
+/obj/item/storage/remove_fantasy_bonuses(bonus)
+ atom_storage.max_slots = reset_fantasy_variable("max_slots", atom_storage.max_slots)
+ atom_storage.max_total_storage = reset_fantasy_variable("max_total_storage", atom_storage.max_total_storage)
+ var/previous_max_storage = LAZYACCESS(fantasy_modifications, "max_specific_storage")
+ if(previous_max_storage)
+ atom_storage.max_specific_storage = previous_max_storage
+ return ..()
+
/obj/item/storage/Initialize(mapload)
. = ..()
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index 4cb2edb024f6..894d4c8f234c 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -642,6 +642,17 @@
new /obj/item/storage/belt/grenade(src)
new /obj/item/storage/belt/military/snack(src)
+/obj/item/storage/box/syndie_kit/poster_box
+ name = "syndicate poster pack"
+ desc = "Contains a variety of demotivational posters to ensure minimum productivity for the crew of any Nanotrasen station."
+
+ /// Number of posters this box contains when spawning.
+ var/poster_count = 3
+
+/obj/item/storage/box/syndie_kit/poster_box/PopulateContents()
+ for(var/i in 1 to poster_count)
+ new /obj/item/poster/traitor(src)
+
#undef KIT_RECON
#undef KIT_BLOODY_SPAI
#undef KIT_STEALTHY
diff --git a/code/modules/antagonists/traitor/objectives/demoralise_graffiti.dm b/code/game/objects/items/syndie_spraycan.dm
similarity index 68%
rename from code/modules/antagonists/traitor/objectives/demoralise_graffiti.dm
rename to code/game/objects/items/syndie_spraycan.dm
index 3bea2dc77bc3..5428f046d96e 100644
--- a/code/modules/antagonists/traitor/objectives/demoralise_graffiti.dm
+++ b/code/game/objects/items/syndie_spraycan.dm
@@ -1,92 +1,3 @@
-/datum/traitor_objective/demoralise/graffiti
- name = "Sow doubt among the crew %VIEWS% times using Syndicate graffiti."
- description = "Use the button below to materialize a seditious spray can, \
- and use it to draw a 3x3 tag in a place where people will come across it. \
- Special syndicate sealing agent ensures that it can't be removed for \
- five minutes following application, and it's slippery too! \
- People seeing or slipping on your graffiti grants progress towards success."
-
- progression_minimum = 0 MINUTES
- progression_maximum = 30 MINUTES
- progression_reward = list(4 MINUTES, 8 MINUTES)
- telecrystal_reward = list(0, 1)
-
- duplicate_type = /datum/traitor_objective/demoralise/graffiti
- /// Have we given out a spray can yet?
- var/obtained_spray = FALSE
- /// Graffiti 'rune' which we will be drawing
- var/obj/effect/decal/cleanable/traitor_rune/rune
-
-/datum/traitor_objective/demoralise/graffiti/generate_ui_buttons(mob/user)
- var/list/buttons = list()
- if (!obtained_spray)
- buttons += add_ui_button("", "Pressing this will materialize a syndicate spraycan in your hand.", "wifi", "summon_gear")
- else
- buttons += add_ui_button("[demoralised_crew_events] / [demoralised_crew_required] propagandised", "This many crew have been exposed to propaganda, out of a required [demoralised_crew_required].", "wifi", "none")
- return buttons
-
-/datum/traitor_objective/demoralise/graffiti/ui_perform_action(mob/living/user, action)
- . = ..()
- switch(action)
- if ("summon_gear")
- if (obtained_spray)
- return
-
- obtained_spray = TRUE
- var/obj/item/traitor_spraycan/spray = new(user.drop_location())
- user.put_in_hands(spray)
- spray.balloon_alert(user, "the spraycan materializes in your hand")
-
- RegisterSignal(spray, COMSIG_PARENT_QDELETING, PROC_REF(on_spray_destroyed))
- RegisterSignal(spray, COMSIG_TRAITOR_GRAFFITI_DRAWN, PROC_REF(on_rune_complete))
-
-/**
- * Called when the spray can is deleted.
- * If it's already been expended we don't care, if it hasn't you just made your objective impossible.area
- *
- * Arguments
- * * spray - the spraycan which was just deleted
- */
-/datum/traitor_objective/demoralise/graffiti/proc/on_spray_destroyed()
- SIGNAL_HANDLER
- // You fucked up pretty bad if you let this happen
- if (!rune)
- fail_objective(penalty_cost = telecrystal_penalty)
-
-/**
- * Called when you managed to draw a traitor rune.
- * Sets up tracking for objective progress, and unregisters signals for the spraycan because we don't care about it any more.
- *
- * Arguments
- * * drawn_rune - graffiti 'rune' which was just drawn.
- */
-/datum/traitor_objective/demoralise/graffiti/proc/on_rune_complete(atom/spray, obj/effect/decal/cleanable/traitor_rune/drawn_rune)
- SIGNAL_HANDLER
- rune = drawn_rune
- UnregisterSignal(spray, COMSIG_PARENT_QDELETING)
- UnregisterSignal(spray, COMSIG_TRAITOR_GRAFFITI_DRAWN)
- RegisterSignal(drawn_rune, COMSIG_PARENT_QDELETING, PROC_REF(on_rune_destroyed))
- RegisterSignal(drawn_rune, COMSIG_DEMORALISING_EVENT, PROC_REF(on_mood_event))
- RegisterSignal(drawn_rune, COMSIG_TRAITOR_GRAFFITI_SLIPPED, PROC_REF(on_mood_event))
-
-/**
- * Called when your traitor rune is destroyed. If you haven't suceeded by now, you fail.area
- *
- * Arguments
- * * rune - the rune which just got destroyed.
- */
-/datum/traitor_objective/demoralise/graffiti/proc/on_rune_destroyed(obj/effect/decal/cleanable/traitor_rune/rune)
- SIGNAL_HANDLER
- fail_objective(penalty_cost = telecrystal_penalty)
-
-/datum/traitor_objective/demoralise/graffiti/ungenerate_objective()
- if (rune)
- UnregisterSignal(rune, COMSIG_PARENT_QDELETING)
- UnregisterSignal(rune, COMSIG_DEMORALISING_EVENT)
- UnregisterSignal(rune, COMSIG_TRAITOR_GRAFFITI_SLIPPED)
- rune = null
- return ..()
-
// Extending the existing spraycan item was more trouble than it was worth, I don't want or need this to be able to draw arbitrary shapes.
/obj/item/traitor_spraycan
name = "seditious spraycan"
diff --git a/code/game/objects/items/teleportation.dm b/code/game/objects/items/teleportation.dm
index dd67bbcd8ce9..87f86e836b7b 100644
--- a/code/game/objects/items/teleportation.dm
+++ b/code/game/objects/items/teleportation.dm
@@ -460,7 +460,8 @@
new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(mobloc)
new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(emergency_destination)
balloon_alert(user, "emergency teleport triggered!")
- make_bloods(mobloc, emergency_destination, user)
+ if (!HAS_TRAIT(user, TRAIT_NOBLOOD))
+ make_bloods(mobloc, emergency_destination, user)
playsound(mobloc, SFX_SPARKS, 50, 1, SHORT_RANGE_SOUND_EXTRARANGE)
playsound(emergency_destination, 'sound/effects/phasein.ogg', 25, 1, SHORT_RANGE_SOUND_EXTRARANGE)
playsound(emergency_destination, SFX_SPARKS, 50, 1, SHORT_RANGE_SOUND_EXTRARANGE)
diff --git a/code/game/objects/items/tongs.dm b/code/game/objects/items/tongs.dm
new file mode 100644
index 000000000000..fdfba7697d61
--- /dev/null
+++ b/code/game/objects/items/tongs.dm
@@ -0,0 +1,101 @@
+/// Tongs, let you pick up and feed people food from further away.
+/obj/item/kitchen/tongs
+ name = "tongs"
+ icon = 'icons/obj/service/kitchen.dmi'
+ desc = "So you never have to touch anything with your dirty, unwashed hands."
+ reach = 2
+ icon_state = "tongs"
+ base_icon_state = "tongs"
+ inhand_icon_state = "fork" // close enough
+ attack_verb_continuous = list("pinches", "tongs", "nips")
+ attack_verb_simple = list("pinch", "tong", "nip")
+ /// What are we holding in our tongs?
+ var/obj/item/tonged
+ /// Sound to play when we click our tongs together
+ var/clack_sound = 'sound/items/handling/component_drop.ogg'
+ /// Time to wait between clacking sounds
+ var/clack_delay = 2 SECONDS
+ /// Have we clacked recently?
+ COOLDOWN_DECLARE(clack_cooldown)
+
+/obj/item/kitchen/tongs/Destroy(force)
+ QDEL_NULL(tonged)
+ return ..()
+
+/obj/item/kitchen/tongs/examine(mob/user)
+ . = ..()
+ if (!isnull(tonged))
+ . += span_notice("It is holding [tonged].")
+
+/obj/item/kitchen/tongs/dropped(mob/user, silent)
+ . = ..()
+ drop_tonged()
+
+/obj/item/kitchen/tongs/attack_self(mob/user, modifiers)
+ . = ..()
+ if(.)
+ return TRUE
+ if (!isnull(tonged))
+ drop_tonged()
+ return TRUE
+ if (!COOLDOWN_FINISHED(src, clack_cooldown))
+ return TRUE
+ user.visible_message(span_notice("[user] clacks [user.p_their()] [src] together like a crab. Click clack!"))
+ click_clack()
+ return TRUE
+
+/// Release the food we are holding
+/obj/item/kitchen/tongs/proc/drop_tonged()
+ if (isnull(tonged))
+ return
+ visible_message(span_notice("[tonged] falls to the ground!"))
+ var/turf/location = drop_location()
+ tonged.forceMove(location)
+ tonged.do_drop_animation(location)
+
+/// Play a clacking sound and appear closed, then open again
+/obj/item/kitchen/tongs/proc/click_clack()
+ COOLDOWN_START(src, clack_cooldown, clack_delay)
+ playsound(src, clack_sound, vol = 100, vary = FALSE)
+ icon_state = "[base_icon_state]_closed"
+ var/delay = min(0.5 SECONDS, clack_delay / 2) // Just in case someone's been fucking with the cooldown
+ addtimer(CALLBACK(src, PROC_REF(clack)), delay, TIMER_DELETE_ME)
+
+/// Plays a clacking sound and appear open
+/obj/item/kitchen/tongs/proc/clack()
+ playsound(src, clack_sound, vol = 100, vary = FALSE)
+ icon_state = base_icon_state
+
+/obj/item/kitchen/tongs/Exited(atom/movable/leaving, direction)
+ . = ..()
+ if (leaving != tonged)
+ return
+ tonged = null
+ update_appearance(UPDATE_ICON)
+
+/obj/item/kitchen/tongs/pre_attack(obj/item/attacked, mob/living/user, params)
+ if (!isnull(tonged))
+ attacked.attackby(tonged, user)
+ return TRUE
+ if (isliving(attacked))
+ if (COOLDOWN_FINISHED(src, clack_cooldown))
+ click_clack()
+ return ..()
+ if (!IsEdible(attacked) || attacked.w_class > WEIGHT_CLASS_NORMAL || !isnull(tonged))
+ return ..()
+ tonged = attacked
+ attacked.do_pickup_animation(src)
+ attacked.forceMove(src)
+ update_appearance(UPDATE_ICON)
+
+/obj/item/kitchen/tongs/update_overlays()
+ . = ..()
+ if (isnull(tonged))
+ return
+ var/mutable_appearance/held_food = new /mutable_appearance(tonged.appearance)
+ held_food.layer = layer
+ held_food.plane = plane
+ held_food.transform = held_food.transform.Scale(0.7, 0.7)
+ held_food.pixel_x = 6
+ held_food.pixel_y = 6
+ . += held_food
diff --git a/code/game/objects/items/trash.dm b/code/game/objects/items/trash.dm
index 7ec6e5f5be60..bd1124dcf1f4 100644
--- a/code/game/objects/items/trash.dm
+++ b/code/game/objects/items/trash.dm
@@ -6,7 +6,7 @@
desc = "This is rubbish."
w_class = WEIGHT_CLASS_TINY
resistance_flags = FLAMMABLE
- item_flags = NOBLUDGEON
+ item_flags = NOBLUDGEON|SKIP_FANTASY_ON_SPAWN
/obj/item/trash/Initialize(mapload)
var/turf/T = get_turf(src)
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index 2b85ddc62e30..0d29aa1f203d 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -21,14 +21,31 @@
if(EXPLODE_LIGHT)
take_damage(rand(10, 90), BRUTE, BOMB, 0)
-/obj/bullet_act(obj/projectile/P)
+ return TRUE
+
+/obj/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
. = ..()
- playsound(src, P.hitsound, 50, TRUE)
- var/damage
+ if(. != BULLET_ACT_HIT)
+ return .
+
+ playsound(src, hitting_projectile.hitsound, 50, TRUE)
+ var/damage_sustained = 0
if(!QDELETED(src)) //Bullet on_hit effect might have already destroyed this object
- damage = take_damage(P.damage, P.damage_type, P.armor_flag, 0, turn(P.dir, 180), P.armour_penetration)
- if(P.suppressed != SUPPRESSED_VERY)
- visible_message(span_danger("[src] is hit by \a [P][damage ? "" : ", without leaving a mark"]!"), null, null, COMBAT_MESSAGE_RANGE)
+ damage_sustained = take_damage(
+ hitting_projectile.damage * hitting_projectile.demolition_mod,
+ hitting_projectile.damage_type,
+ hitting_projectile.armor_flag,
+ FALSE,
+ REVERSE_DIR(hitting_projectile.dir),
+ hitting_projectile.armour_penetration,
+ )
+ if(hitting_projectile.suppressed != SUPPRESSED_VERY)
+ visible_message(
+ span_danger("[src] is hit by \a [hitting_projectile][damage_sustained ? "" : ", without leaving a mark"]!"),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ )
+
+ return damage_sustained > 0 ? BULLET_ACT_HIT : BULLET_ACT_BLOCK
/obj/attack_hulk(mob/living/carbon/human/user)
..()
diff --git a/code/game/objects/structures/beds_chairs/chair.dm b/code/game/objects/structures/beds_chairs/chair.dm
index 5d7e0d30c8f9..6d22d8480d3c 100644
--- a/code/game/objects/structures/beds_chairs/chair.dm
+++ b/code/game/objects/structures/beds_chairs/chair.dm
@@ -327,6 +327,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/chair/stool/bar, 0)
hitsound = 'sound/items/trayhit1.ogg'
hit_reaction_chance = 50
custom_materials = list(/datum/material/iron = 2000)
+ item_flags = SKIP_FANTASY_ON_SPAWN
var/break_chance = 5 //Likely hood of smashing the chair.
var/obj/structure/chair/origin_type = /obj/structure/chair
diff --git a/code/game/objects/structures/crates_lockers/closets/job_closets.dm b/code/game/objects/structures/crates_lockers/closets/job_closets.dm
index dd35e0f1c08f..8d91a2678b9e 100644
--- a/code/game/objects/structures/crates_lockers/closets/job_closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets/job_closets.dm
@@ -92,6 +92,9 @@
new /obj/item/clothing/suit/toggle/lawyer/black(src)
new /obj/item/clothing/shoes/laceup(src)
new /obj/item/clothing/shoes/laceup(src)
+
+/obj/structure/closet/lawcloset/populate_contents_immediate()
+ . = ..()
new /obj/item/clothing/accessory/lawyers_badge(src)
new /obj/item/clothing/accessory/lawyers_badge(src)
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/engineering.dm b/code/game/objects/structures/crates_lockers/closets/secure/engineering.dm
index c708480100a6..97a6664adb2e 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/engineering.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/engineering.dm
@@ -13,7 +13,7 @@
new /obj/item/holosign_creator/atmos(src)
new /obj/item/assembly/flash/handheld(src)
new /obj/item/door_remote/chief_engineer(src)
- new /obj/item/pipe_dispenser(src)
+
new /obj/item/circuitboard/machine/techfab/department/engineering(src)
new /obj/item/extinguisher/advanced(src)
new /obj/item/storage/photo_album/ce(src)
@@ -25,6 +25,7 @@
// Traitor steal objective
new /obj/item/areaeditor/blueprints(src)
new /obj/item/gun/ballistic/SRN_rocketlauncher(src)
+ new /obj/item/pipe_dispenser(src)
/obj/structure/closet/secure_closet/engineering_electrical
name = "electrical supplies locker"
@@ -35,13 +36,18 @@
/obj/structure/closet/secure_closet/engineering_electrical/PopulateContents()
..()
var/static/items_inside = list(
- /obj/item/clothing/gloves/color/yellow = 2,
/obj/item/inducer = 2,
/obj/item/storage/toolbox/electrical = 3,
/obj/item/electronics/apc = 3,
/obj/item/multitool = 3)
generate_items_inside(items_inside,src)
+/obj/structure/closet/secure_closet/engineering_electrical/populate_contents_immediate()
+ . = ..()
+
+ new /obj/item/clothing/gloves/color/yellow(src)
+ new /obj/item/clothing/gloves/color/yellow(src)
+
/obj/structure/closet/secure_closet/engineering_welding
name = "welding supplies locker"
req_access = list(ACCESS_ENGINE_EQUIP)
@@ -80,7 +86,7 @@
/obj/structure/closet/secure_closet/atmospherics/PopulateContents()
..()
new /obj/item/radio/headset/headset_eng(src)
- new /obj/item/pipe_dispenser(src)
+
new /obj/item/storage/toolbox/mechanical(src)
new /obj/item/tank/internals/emergency_oxygen/engi(src)
new /obj/item/holosign_creator/atmos(src)
@@ -91,3 +97,8 @@
new /obj/item/clothing/head/utility/hardhat/welding/atmos(src)
new /obj/item/clothing/glasses/meson/engine/tray(src)
new /obj/item/extinguisher/advanced(src)
+
+/obj/structure/closet/secure_closet/atmospherics/populate_contents_immediate()
+ . = ..()
+
+ new /obj/item/pipe_dispenser(src)
diff --git a/code/game/objects/structures/crates_lockers/crates/secure.dm b/code/game/objects/structures/crates_lockers/crates/secure.dm
index 4698c8469d24..25989b2bdbc9 100644
--- a/code/game/objects/structures/crates_lockers/crates/secure.dm
+++ b/code/game/objects/structures/crates_lockers/crates/secure.dm
@@ -21,7 +21,7 @@
. = ..()
ADD_TRAIT(src, TRAIT_NO_MISSING_ITEM_ERROR, TRAIT_GENERIC)
-/obj/structure/closet/crate/secure/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1)
+/obj/structure/closet/crate/secure/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
if(prob(tamperproof) && damage_amount >= DAMAGE_PRECISION)
boom()
else
diff --git a/code/game/objects/structures/crates_lockers/crates/syndicrate.dm b/code/game/objects/structures/crates_lockers/crates/syndicrate.dm
index b18321338c49..9d4d5e6c8ced 100644
--- a/code/game/objects/structures/crates_lockers/crates/syndicrate.dm
+++ b/code/game/objects/structures/crates_lockers/crates/syndicrate.dm
@@ -18,7 +18,7 @@
laser = 50
energy = 100
-/obj/structure/closet/crate/syndicrate/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1)
+/obj/structure/closet/crate/syndicrate/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
if(created_items)
return ..()
if(damage_amount < DAMAGE_PRECISION)
diff --git a/code/game/objects/structures/false_walls.dm b/code/game/objects/structures/false_walls.dm
index 729abad34f41..54a9527899fa 100644
--- a/code/game/objects/structures/false_walls.dm
+++ b/code/game/objects/structures/false_walls.dm
@@ -83,7 +83,7 @@
qdel(src)
return T
-/obj/structure/falsewall/tool_act(mob/living/user, obj/item/tool)
+/obj/structure/falsewall/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
if(!opening)
return ..()
to_chat(user, span_warning("You must wait until the door has stopped moving!"))
diff --git a/code/game/objects/structures/holosign.dm b/code/game/objects/structures/holosign.dm
index 2ba97428cea9..51abaf50e13b 100644
--- a/code/game/objects/structures/holosign.dm
+++ b/code/game/objects/structures/holosign.dm
@@ -136,14 +136,13 @@
density = TRUE
max_integrity = 10
allow_walk = FALSE
+ armor_type = /datum/armor/structure_holosign/cyborg_barrier // Gets a special armor subtype which is extra good at defense.
-/obj/structure/holosign/barrier/cyborg/bullet_act(obj/projectile/P)
- take_damage((P.damage / 5) , BRUTE, MELEE, 1) //Doesn't really matter what damage flag it is.
- if(istype(P, /obj/projectile/energy/electrode))
- take_damage(10, BRUTE, MELEE, 1) //Tasers aren't harmful.
- if(istype(P, /obj/projectile/beam/disabler))
- take_damage(5, BRUTE, MELEE, 1) //Disablers aren't harmful.
- return BULLET_ACT_HIT
+/datum/armor/structure_holosign/cyborg_barrier
+ bullet = 80
+ laser = 80
+ energy = 80
+ melee = 20
/obj/structure/holosign/barrier/medical
name = "\improper PENLITE holobarrier"
@@ -196,12 +195,9 @@
name = "Charged Energy Field"
desc = "A powerful energy field that blocks movement. Energy arcs off it."
max_integrity = 20
+ armor_type = /datum/armor/structure_holosign //Yeah no this doesn't get projectile resistance.
var/shockcd = 0
-/obj/structure/holosign/barrier/cyborg/hacked/bullet_act(obj/projectile/P)
- take_damage(P.damage, BRUTE, MELEE, 1) //Yeah no this doesn't get projectile resistance.
- return BULLET_ACT_HIT
-
/obj/structure/holosign/barrier/cyborg/hacked/proc/cooldown()
shockcd = FALSE
diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm
index c95eeedf1260..40c5256915bd 100644
--- a/code/game/objects/structures/icemoon/cave_entrance.dm
+++ b/code/game/objects/structures/icemoon/cave_entrance.dm
@@ -17,7 +17,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
faction = list(FACTION_MINING)
max_mobs = 3
max_integrity = 250
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/wolf)
+ mob_types = list(/mob/living/basic/mining/wolf)
move_resist = INFINITY
anchored = TRUE
@@ -76,7 +76,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
name = "demonic portal"
desc = "A portal that goes to another world, normal creatures couldn't survive there."
icon_state = "nether"
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/ice_demon)
+ mob_types = list(/mob/living/basic/mining/ice_demon)
light_outer_range = 1
light_color = COLOR_SOFT_RED
@@ -102,7 +102,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
mob_types = list(/mob/living/basic/mining/ice_whelp)
/obj/structure/spawner/ice_moon/demonic_portal/snowlegion
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion/snow)
+ mob_types = list(/mob/living/basic/mining/legion/snow/spawner_made)
/obj/effect/collapsing_demonic_portal
name = "collapsing demonic portal"
@@ -164,7 +164,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
if(13)
new /obj/item/gun/magic/hook (loc)
if(14)
- new /obj/item/guardiancreator/miner(loc)
+ new /obj/item/guardian_creator/miner(loc)
if(15)
new /obj/item/ship_in_a_bottle(loc)
if(16)
@@ -186,7 +186,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
if(24)
new /obj/structure/elite_tumor(loc)
if(25)
- new /mob/living/simple_animal/hostile/retaliate/clown/clownhulk(loc)
+ new /mob/living/basic/clown/clownhulk(loc)
if(26)
new /obj/item/book/granter/action/spell/sacredflame(loc)
if(27)
diff --git a/code/game/objects/structures/lattice.dm b/code/game/objects/structures/lattice.dm
index 82fba11d0e2f..1d4ec997aae0 100644
--- a/code/game/objects/structures/lattice.dm
+++ b/code/game/objects/structures/lattice.dm
@@ -94,6 +94,10 @@
canSmoothWith = SMOOTH_GROUP_CATWALK
obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN | BLOCK_Z_IN_UP
+/obj/structure/lattice/catwalk/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/footstep_override, footstep = FOOTSTEP_CATWALK)
+
/obj/structure/lattice/catwalk/deconstruction_hints(mob/user)
return span_notice("The supporting rods look like they could be cut.")
diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm
index 8c3a1779b0b0..3a7aab6d977c 100644
--- a/code/game/objects/structures/lavaland/necropolis_tendril.dm
+++ b/code/game/objects/structures/lavaland/necropolis_tendril.dm
@@ -24,7 +24,7 @@
mob_types = list(/mob/living/basic/mining/goliath)
/obj/structure/spawner/lavaland/legion
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril)
+ mob_types = list(/mob/living/basic/mining/legion/spawner_made)
/obj/structure/spawner/lavaland/icewatcher
mob_types = list(/mob/living/basic/mining/watcher/icewing)
diff --git a/code/game/objects/structures/ore_containers.dm b/code/game/objects/structures/ore_containers.dm
new file mode 100644
index 000000000000..af96c3e4a014
--- /dev/null
+++ b/code/game/objects/structures/ore_containers.dm
@@ -0,0 +1,62 @@
+///structure to contain ores
+/obj/structure/ore_container
+
+/obj/structure/ore_container/attackby(obj/item/ore, mob/living/carbon/human/user, list/modifiers)
+ if(istype(ore, /obj/item/stack/ore) && !(user.istate & ISTATE_HARM))
+ ore.forceMove(src)
+ return
+ return ..()
+
+/obj/structure/ore_container/Entered(atom/movable/mover)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/ore_container/Exited(atom/movable/mover)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/ore_container/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "OreContainer")
+ ui.open()
+
+/obj/structure/ore_container/ui_data(mob/user)
+ var/list/data = list()
+ data["ores"] = list()
+ for(var/obj/item/stack/ore/ore_item in contents)
+ data["ores"] += list(list(
+ "id" = REF(ore_item),
+ "name" = ore_item.name,
+ "amount" = ore_item.amount,
+ ))
+ return data
+
+/obj/structure/ore_container/ui_static_data(mob/user)
+ var/list/data = list()
+ data["ore_images"] = list()
+ for(var/obj/item/stack/ore_item as anything in subtypesof(/obj/item/stack/ore))
+ data["ore_images"] += list(list(
+ "name" = initial(ore_item.name),
+ "icon" = icon2base64(getFlatIcon(image(icon = initial(ore_item.icon), icon_state = initial(ore_item.icon_state)), no_anim=TRUE))
+ ))
+ return data
+
+/obj/structure/ore_container/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+
+ if(. || !isliving(ui.user))
+ return TRUE
+
+ var/mob/living/customer = ui.user
+ var/obj/item/stack_to_move
+ switch(action)
+ if("withdraw")
+ if(isnull(params["reference"]))
+ return TRUE
+ stack_to_move = locate(params["reference"]) in contents
+ if(isnull(stack_to_move))
+ return TRUE
+ stack_to_move.forceMove(get_turf(customer))
+ return TRUE
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index 0da5236b321b..c9209787e77c 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -15,6 +15,8 @@
var/climbable = TRUE
///Initial direction of the railing.
var/ini_dir
+ ///item released when deconstructed
+ var/item_deconstruct = /obj/item/stack/rods
/datum/armor/structure_railing
melee = 50
@@ -70,9 +72,11 @@
return TRUE
/obj/structure/railing/deconstruct(disassembled)
- if(!(flags_1 & NODECONSTRUCT_1))
- var/obj/item/stack/rods/rod = new /obj/item/stack/rods(drop_location(), 6)
- transfer_fingerprints_to(rod)
+ if((flags_1 & NODECONSTRUCT_1))
+ return ..()
+ var/rods_to_make = istype(src,/obj/structure/railing/corner) ? 1 : 2
+ var/obj/rod = new item_deconstruct(drop_location(), rods_to_make)
+ transfer_fingerprints_to(rod)
return ..()
///Implements behaviour that makes it possible to unanchor the railing.
@@ -124,3 +128,35 @@
/obj/structure/railing/proc/check_anchored(checked_anchored)
if(anchored == checked_anchored)
return TRUE
+
+
+/obj/structure/railing/wooden_fence
+ name = "wooden fence"
+ desc = "wooden fence meant to keep animals in."
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "wooden_railing"
+ item_deconstruct = /obj/item/stack/sheet/mineral/wood
+ plane = GAME_PLANE_FOV_HIDDEN
+ layer = ABOVE_MOB_LAYER
+
+/obj/structure/railing/wooden_fence/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_change_layer))
+ adjust_dir_layer(dir)
+
+/obj/structure/railing/wooden_fence/proc/on_change_layer(datum/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+ adjust_dir_layer(new_dir)
+
+/obj/structure/railing/wooden_fence/proc/adjust_dir_layer(direction)
+ var/new_layer = (direction & NORTH) ? MOB_LAYER : ABOVE_MOB_LAYER
+ layer = new_layer
+
+
+/obj/structure/railing/corner/end/wooden_fence
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "wooden_railing_corner"
+
+/obj/structure/railing/corner/end/flip/wooden_fence
+ icon = 'icons/obj/structures.dmi'
+ icon_state = "wooden_railing_corner_flipped"
diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm
index 4e51d0900c5d..cc020f275ae0 100644
--- a/code/game/objects/structures/spawner.dm
+++ b/code/game/objects/structures/spawner.dm
@@ -8,11 +8,12 @@
anchored = TRUE
density = TRUE
+ faction = list(FACTION_HOSTILE)
+
var/max_mobs = 5
var/spawn_time = 30 SECONDS
var/mob_types = list(/mob/living/basic/carp)
var/spawn_text = "emerges from"
- var/faction = list(FACTION_HOSTILE)
var/spawner_type = /datum/component/spawner
/obj/structure/spawner/Initialize(mapload)
@@ -30,7 +31,7 @@
icon = 'icons/obj/device.dmi'
icon_state = "syndbeacon"
spawn_text = "warps in from"
- mob_types = list(/mob/living/basic/syndicate/ranged)
+ mob_types = list(/mob/living/basic/trooper/syndicate/ranged)
faction = list(ROLE_SYNDICATE)
/obj/structure/spawner/skeleton
@@ -41,7 +42,7 @@
max_integrity = 150
max_mobs = 15
spawn_time = 15 SECONDS
- mob_types = list(/mob/living/simple_animal/hostile/skeleton)
+ mob_types = list(/mob/living/basic/skeleton)
spawn_text = "climbs out of"
faction = list(FACTION_SKELETON)
@@ -54,16 +55,16 @@
max_mobs = 15
spawn_time = 15 SECONDS
mob_types = list(
- /mob/living/simple_animal/hostile/retaliate/clown,
- /mob/living/simple_animal/hostile/retaliate/clown/banana,
- /mob/living/simple_animal/hostile/retaliate/clown/clownhulk,
- /mob/living/simple_animal/hostile/retaliate/clown/clownhulk/chlown,
- /mob/living/simple_animal/hostile/retaliate/clown/clownhulk/honcmunculus,
- /mob/living/simple_animal/hostile/retaliate/clown/fleshclown,
- /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton,
- /mob/living/simple_animal/hostile/retaliate/clown/honkling,
- /mob/living/simple_animal/hostile/retaliate/clown/longface,
- /mob/living/simple_animal/hostile/retaliate/clown/lube,
+ /mob/living/basic/clown,
+ /mob/living/basic/clown/banana,
+ /mob/living/basic/clown/clownhulk,
+ /mob/living/basic/clown/clownhulk/chlown,
+ /mob/living/basic/clown/clownhulk/honkmunculus,
+ /mob/living/basic/clown/fleshclown,
+ /mob/living/basic/clown/mutant/glutton,
+ /mob/living/basic/clown/honkling,
+ /mob/living/basic/clown/longface,
+ /mob/living/basic/clown/lube,
)
spawn_text = "climbs out of"
faction = list(FACTION_CLOWN)
@@ -80,8 +81,8 @@
/mob/living/basic/mining/basilisk,
/mob/living/basic/mining/goldgrub,
/mob/living/basic/mining/goliath/ancient,
+ /mob/living/basic/mining/legion,
/mob/living/basic/wumborian_fugu,
- /mob/living/simple_animal/hostile/asteroid/hivelord,
)
faction = list(FACTION_MINING)
@@ -98,7 +99,7 @@
/obj/structure/spawner/mining/hivelord
name = "hivelord den"
desc = "A den housing a nest of hivelords."
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord)
+ mob_types = list(/mob/living/basic/mining/hivelord)
/obj/structure/spawner/mining/basilisk
name = "basilisk den"
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index 84acecaa7807..4b420fc824fe 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -42,6 +42,8 @@
. = ..()
if(_buildstack)
buildstack = _buildstack
+ AddElement(/datum/element/footstep_override, priority = STEP_SOUND_TABLE_PRIORITY)
+
AddElement(/datum/element/climbable)
var/static/list/loc_connections = list(
@@ -376,7 +378,7 @@
for(var/atom/movable/attached_movable as anything in attached_items)
if(!attached_movable.Move(loc))
RemoveItemFromTable(attached_movable, attached_movable.loc)
-
+
/obj/structure/table/rolling/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if(has_gravity())
diff --git a/code/game/objects/structures/traps.dm b/code/game/objects/structures/traps.dm
index 67b452ed3a8f..9078bf10085f 100644
--- a/code/game/objects/structures/traps.dm
+++ b/code/game/objects/structures/traps.dm
@@ -8,7 +8,7 @@
alpha = 30 //initially quite hidden when not "recharging"
var/flare_message = "the trap flares brightly!"
var/last_trigger = 0
- var/time_between_triggers = 600 //takes a minute to recharge
+ var/time_between_triggers = 1 MINUTES
var/charges = INFINITY
var/antimagic_flags = MAGIC_RESISTANCE
@@ -61,50 +61,50 @@
last_trigger = world.time
charges--
if(charges <= 0)
- animate(src, alpha = 0, time = 10)
- QDEL_IN(src, 10)
+ animate(src, alpha = 0, time = 1 SECONDS)
+ QDEL_IN(src, 1 SECONDS)
else
animate(src, alpha = initial(alpha), time = time_between_triggers)
-/obj/structure/trap/proc/on_entered(datum/source, atom/movable/AM)
+/obj/structure/trap/proc/on_entered(datum/source, atom/movable/victim)
SIGNAL_HANDLER
if(last_trigger + time_between_triggers > world.time)
return
// Don't want the traps triggered by sparks, ghosts or projectiles.
- if(is_type_in_typecache(AM, ignore_typecache))
+ if(is_type_in_typecache(victim, ignore_typecache))
return
- if(ismob(AM))
- var/mob/M = AM
- if(M.mind in immune_minds)
+ if(ismob(victim))
+ var/mob/mob_victim = victim
+ if(mob_victim.mind in immune_minds)
return
- if(M.can_block_magic(antimagic_flags))
+ if(mob_victim.can_block_magic(antimagic_flags))
flare()
return
if(charges <= 0)
return
flare()
- if(isliving(AM))
- trap_effect(AM)
+ if(isliving(victim))
+ trap_effect(victim)
-/obj/structure/trap/proc/trap_effect(mob/living/L)
+/obj/structure/trap/proc/trap_effect(mob/living/victim)
return
/obj/structure/trap/stun
name = "shock trap"
desc = "A trap that will shock and render you immobile. You'd better avoid it."
icon_state = "trap-shock"
- var/stun_time = 100
+ var/stun_time = 10 SECONDS
-/obj/structure/trap/stun/trap_effect(mob/living/L)
- L.electrocute_act(30, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
- L.Paralyze(stun_time)
+/obj/structure/trap/stun/trap_effect(mob/living/victim)
+ victim.electrocute_act(30, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
+ victim.Paralyze(stun_time)
/obj/structure/trap/stun/hunter
name = "bounty trap"
desc = "A trap that only goes off when a fugitive steps on it, announcing the location and stunning the target. You'd better avoid it."
icon = 'icons/obj/objects.dmi'
icon_state = "bounty_trap_on"
- stun_time = 200
+ stun_time = 20 SECONDS
sparks = FALSE //the item version gives them off to prevent runtimes (see Destroy())
antimagic_flags = NONE
var/obj/item/bountytrap/stored_item
@@ -112,7 +112,7 @@
/obj/structure/trap/stun/hunter/Initialize(mapload)
. = ..()
- time_between_triggers = 10
+ time_between_triggers = 1 SECONDS
flare_message = "[src] snaps shut!"
/obj/structure/trap/stun/hunter/Destroy()
@@ -121,10 +121,10 @@
stored_item = null
return ..()
-/obj/structure/trap/stun/hunter/on_entered(datum/source, atom/movable/AM)
- if(isliving(AM))
- var/mob/living/L = AM
- if(!L.mind?.has_antag_datum(/datum/antagonist/fugitive))
+/obj/structure/trap/stun/hunter/on_entered(datum/source, atom/movable/victim)
+ if(isliving(victim))
+ var/mob/living/living_victim = victim
+ if(!living_victim.mind?.has_antag_datum(/datum/antagonist/fugitive))
return
caught = TRUE
. = ..()
@@ -169,11 +169,11 @@
radio.talk_into(src, "Fugitive has triggered this trap in the [get_area_name(src)]!", RADIO_CHANNEL_COMMON)
/obj/item/bountytrap/attack_self(mob/living/user)
- var/turf/T = get_turf(src)
- if(!user || !user.transferItemToLoc(src, T))//visibly unequips
+ var/turf/target_turf = get_turf(src)
+ if(!user || !user.transferItemToLoc(src, target_turf))//visibly unequips
return
to_chat(user, span_notice("You set up [src]. Examine while close to disarm it."))
- stored_trap.forceMove(T)//moves trap to ground
+ stored_trap.forceMove(target_turf)//moves trap to ground
forceMove(stored_trap)//moves item into trap
/obj/item/bountytrap/Destroy()
@@ -189,9 +189,9 @@
desc = "A trap that will set you ablaze. You'd better avoid it."
icon_state = "trap-fire"
-/obj/structure/trap/fire/trap_effect(mob/living/L)
- to_chat(L, span_danger("Spontaneous combustion!"))
- L.Paralyze(20)
+/obj/structure/trap/fire/trap_effect(mob/living/victim)
+ to_chat(victim, span_danger("Spontaneous combustion!"))
+ victim.Paralyze(2 SECONDS)
new /obj/effect/hotspot(get_turf(src))
/obj/structure/trap/chill
@@ -199,11 +199,11 @@
desc = "A trap that will chill you to the bone. You'd better avoid it."
icon_state = "trap-frost"
-/obj/structure/trap/chill/trap_effect(mob/living/L)
- to_chat(L, span_danger("You're frozen solid!"))
- L.Paralyze(20)
- L.adjust_bodytemperature(-300)
- L.apply_status_effect(/datum/status_effect/freon)
+/obj/structure/trap/chill/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("You're frozen solid!"))
+ victim.Paralyze(2 SECONDS)
+ victim.adjust_bodytemperature(-300)
+ victim.apply_status_effect(/datum/status_effect/freon)
/obj/structure/trap/damage
@@ -212,12 +212,12 @@
icon_state = "trap-earth"
-/obj/structure/trap/damage/trap_effect(mob/living/L)
- to_chat(L, span_danger("The ground quakes beneath your feet!"))
- L.Paralyze(100)
- L.adjustBruteLoss(35)
+/obj/structure/trap/damage/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("The ground quakes beneath your feet!"))
+ victim.Paralyze(10 SECONDS)
+ victim.adjustBruteLoss(35)
var/obj/structure/flora/rock/style_random/giant_rock = new(get_turf(src))
- QDEL_IN(giant_rock, 200)
+ QDEL_IN(giant_rock, 20 SECONDS)
/obj/structure/trap/ward
@@ -225,7 +225,7 @@
desc = "A divine barrier, It looks like you could destroy it with enough effort, or wait for it to dissipate..."
icon_state = "ward"
density = TRUE
- time_between_triggers = 1200 //Exists for 2 minutes
+ time_between_triggers = 2 MINUTES
/obj/structure/trap/ward/Initialize(mapload)
. = ..()
@@ -236,10 +236,10 @@
desc = "A trap that rings with unholy energy. You think you hear... chittering?"
icon_state = "trap-cult"
-/obj/structure/trap/cult/trap_effect(mob/living/L)
- to_chat(L, span_danger("With a crack, the hostile constructs come out of hiding, stunning you!"))
- L.electrocute_act(10, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
- L.Paralyze(20)
- new /mob/living/simple_animal/hostile/construct/proteon/hostile(loc)
- new /mob/living/simple_animal/hostile/construct/proteon/hostile(loc)
- QDEL_IN(src, 30)
+/obj/structure/trap/cult/trap_effect(mob/living/victim)
+ to_chat(victim, span_bolddanger("With a crack, the hostile constructs come out of hiding, stunning you!"))
+ victim.electrocute_act(10, src, flags = SHOCK_NOGLOVES) // electrocute act does a message.
+ victim.Paralyze(2 SECONDS)
+ new /mob/living/basic/construct/proteon/hostile(loc)
+ new /mob/living/basic/construct/proteon/hostile(loc)
+ QDEL_IN(src, 3 SECONDS)
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 1d0848e0ad28..4a0631594c46 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -327,7 +327,7 @@
return TRUE
-/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1)
+/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
. = ..()
if(.) //received damage
update_nearby_icons()
diff --git a/code/game/turfs/closed/wall/misc_walls.dm b/code/game/turfs/closed/wall/misc_walls.dm
index c96cf314a982..7705f9ded255 100644
--- a/code/game/turfs/closed/wall/misc_walls.dm
+++ b/code/game/turfs/closed/wall/misc_walls.dm
@@ -18,16 +18,6 @@
/turf/closed/wall/mineral/cult/devastate_wall()
new sheet_type(get_turf(src), sheet_amount)
-/turf/closed/wall/mineral/cult/Exited(atom/movable/gone, direction)
- . = ..()
- if(istype(gone, /mob/living/simple_animal/hostile/construct/harvester)) //harvesters can go through cult walls, dragging something with
- var/mob/living/simple_animal/hostile/construct/harvester/H = gone
- var/atom/movable/stored_pulling = H.pulling
- if(stored_pulling)
- stored_pulling.setDir(direction)
- stored_pulling.forceMove(src)
- H.start_pulling(stored_pulling, supress_message = TRUE)
-
/turf/closed/wall/mineral/cult/artificer
name = "runed stone wall"
desc = "A cold stone wall engraved with indecipherable symbols. Studying them causes your head to pound."
diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm
index 4d7595b91895..77b27fddc1d7 100644
--- a/code/game/turfs/closed/walls.dm
+++ b/code/game/turfs/closed/walls.dm
@@ -1,4 +1,5 @@
#define MAX_DENT_DECALS 15
+#define LEANING_OFFSET 11
/turf/closed/wall
name = "wall"
@@ -33,6 +34,50 @@
var/list/dent_decals
+/turf/closed/wall/MouseDrop_T(mob/living/carbon/carbon_mob, mob/user)
+ ..()
+ if(carbon_mob != user)
+ return
+ if(carbon_mob.is_leaning == TRUE)
+ return
+ if(carbon_mob.pulledby)
+ return
+ if(!carbon_mob.density)
+ return
+ carbon_mob.is_leaning = TRUE
+ var/turf/checked_turf = get_step(carbon_mob, turn(carbon_mob.dir, 180))
+ if(checked_turf == src)
+ carbon_mob.start_leaning(src)
+
+/mob/living/carbon/proc/start_leaning(obj/wall)
+
+ switch(dir)
+ if(SOUTH)
+ pixel_y += LEANING_OFFSET
+ if(NORTH)
+ pixel_y += -LEANING_OFFSET
+ if(WEST)
+ pixel_x += LEANING_OFFSET
+ if(EAST)
+ pixel_x += -LEANING_OFFSET
+
+ ADD_TRAIT(src, TRAIT_UNDENSE, LEANING_TRAIT)
+ ADD_TRAIT(src, TRAIT_EXPANDED_FOV, LEANING_TRAIT)
+ visible_message(span_notice("[src] leans against \the [wall]!"), \
+ span_notice("You lean against \the [wall]!"))
+ RegisterSignals(src, list(COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_GET_PULLED, COMSIG_MOVABLE_TELEPORTING, COMSIG_ATOM_DIR_CHANGE), PROC_REF(stop_leaning))
+ update_fov()
+
+/mob/living/carbon/proc/stop_leaning()
+ SIGNAL_HANDLER
+ UnregisterSignal(src, list(COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_GET_PULLED, COMSIG_MOVABLE_TELEPORTING, COMSIG_ATOM_DIR_CHANGE))
+ is_leaning = FALSE
+ pixel_y = base_pixel_y + body_position_pixel_x_offset
+ pixel_x = base_pixel_y + body_position_pixel_y_offset
+ REMOVE_TRAIT(src, TRAIT_UNDENSE, LEANING_TRAIT)
+ REMOVE_TRAIT(src, TRAIT_EXPANDED_FOV, LEANING_TRAIT)
+ update_fov()
+
/turf/closed/wall/Initialize(mapload)
. = ..()
if(!can_engrave)
@@ -328,4 +373,13 @@
/turf/closed/wall/metal_foam_base
girder_type = /obj/structure/foamedmetal
+/turf/closed/wall/Bumped(atom/movable/bumped_atom)
+ . = ..()
+ SEND_SIGNAL(bumped_atom, COMSIG_LIVING_WALL_BUMP, src)
+
+/turf/closed/wall/Exited(atom/movable/gone, direction)
+ . = ..()
+ SEND_SIGNAL(gone, COMSIG_LIVING_WALL_EXITED, src)
+
#undef MAX_DENT_DECALS
+#undef LEANING_OFFSET
diff --git a/code/game/turfs/open/floor/plating.dm b/code/game/turfs/open/floor/plating.dm
index 993deb3aedef..756917e86f53 100644
--- a/code/game/turfs/open/floor/plating.dm
+++ b/code/game/turfs/open/floor/plating.dm
@@ -178,7 +178,7 @@
. = ..()
ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
-/turf/open/floor/plating/foam/tool_act(mob/living/user, obj/item/I, tool_type)
+/turf/open/floor/plating/foam/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
return
//reinforced plating deconstruction states
diff --git a/code/modules/actionspeed/_actionspeed_modifier.dm b/code/modules/actionspeed/_actionspeed_modifier.dm
index 71bc966acf4d..761bfc3ff74a 100644
--- a/code/modules/actionspeed/_actionspeed_modifier.dm
+++ b/code/modules/actionspeed/_actionspeed_modifier.dm
@@ -37,8 +37,11 @@ can next move
/// Other modification datums this conflicts with.
var/conflicts_with
-/datum/actionspeed_modifier/New()
+/datum/actionspeed_modifier/New(init_id)
. = ..()
+
+ id = init_id
+
if(!id)
id = "[type]" //We turn the path into a string.
diff --git a/code/modules/actionspeed/modifiers/wound.dm b/code/modules/actionspeed/modifiers/wound.dm
new file mode 100644
index 000000000000..845399e07616
--- /dev/null
+++ b/code/modules/actionspeed/modifiers/wound.dm
@@ -0,0 +1,10 @@
+/datum/actionspeed_modifier/wound_interaction_inefficiency
+ variable = TRUE
+
+ var/datum/wound/parent
+
+/datum/actionspeed_modifier/wound_interaction_inefficiency/New(new_id, datum/wound/parent)
+
+ src.parent = parent
+
+ return ..()
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index c01ab9e5b21d..0c3ebbb32541 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -52,6 +52,9 @@ GLOBAL_PROTECT(admin_verbs_admin)
/client/proc/admin_enable_shuttle, /*undoes the above*/
/client/proc/admin_ghost, /*allows us to ghost/reenter body at will*/
/client/proc/admin_hostile_environment, /*Allows admins to prevent the emergency shuttle from leaving, also lets admins clear hostile environments if theres one stuck*/
+ /client/proc/centcom_podlauncher,/*Open a window to launch a Supplypod and configure it or it's contents*/
+ /client/proc/check_ai_laws, /*shows AI and borg laws*/
+ /client/proc/check_antagonists, /*shows all antags*/
/client/proc/cmd_admin_check_contents, /*displays the contents of an instance*/
/client/proc/cmd_admin_check_player_exp, /* shows players by playtime */
/client/proc/cmd_admin_create_centcom_report,
@@ -62,9 +65,7 @@ GLOBAL_PROTECT(admin_verbs_admin)
/client/proc/cmd_admin_subtle_message, /*send a message to somebody as a 'voice in their head'*/
/client/proc/cmd_admin_world_narrate, /*sends text to all players with no padding*/
/client/proc/cmd_change_command_name,
- /client/proc/centcom_podlauncher,/*Open a window to launch a Supplypod and configure it or it's contents*/
- /client/proc/check_ai_laws, /*shows AI and borg laws*/
- /client/proc/check_antagonists, /*shows all antags*/
+ /client/proc/create_mob_worm,
/client/proc/fax_panel, /*send a paper to fax*/
/client/proc/force_load_lazy_template,
/client/proc/game_panel, /*game panel, allows to change game-mode etc*/
@@ -1039,3 +1040,47 @@ GLOBAL_PROTECT(admin_verbs_poll)
message_admins("[key_name_admin(usr)] has loaded lazy template '[choice]'")
to_chat(usr, span_boldnicegreen("Template loaded, you have been moved to the bottom left of the reservation."))
+
+/client/proc/create_mob_worm()
+ set category = "Admin.Fun"
+ set name = "Create Mob Worm"
+ set desc = "Attached a linked list of mobs to a marked mob"
+ if (!check_rights(R_FUN))
+ return
+ if(isnull(holder))
+ return
+ if(!isliving(holder.marked_datum))
+ to_chat(usr, span_warning("Error: Please mark a mob to attach mobs to."))
+ return
+ var/mob/living/head = holder.marked_datum
+
+ var/attempted_target_path = tgui_input_text(
+ usr,
+ "Enter typepath of a mob you'd like to make your chain from.",
+ "Typepath",
+ "[/mob/living/basic/pet/dog/corgi/ian]",
+ )
+
+ if (isnull(attempted_target_path))
+ return //The user pressed "Cancel"
+
+ var/desired_mob = text2path(attempted_target_path)
+ if(!ispath(desired_mob))
+ var/static/list/mob_paths = make_types_fancy(subtypesof(/mob/living))
+ desired_mob = pick_closest_path(attempted_target_path, mob_paths)
+ if(isnull(desired_mob) || !ispath(desired_mob) || QDELETED(head))
+ return //The user pressed "Cancel"
+
+ var/amount = tgui_input_number(usr, "How long should our tail be?", "Worm Configurator", default = 3, min_value = 1)
+ if (isnull(amount) || amount < 1 || QDELETED(head))
+ return
+ head.AddComponent(/datum/component/mob_chain)
+ var/mob/living/previous = head
+ for (var/i in 1 to amount)
+ var/mob/living/segment = new desired_mob(head.drop_location())
+ if (QDELETED(segment)) // ffs mobs which replace themselves with other mobs
+ i--
+ continue
+ QDEL_NULL(segment.ai_controller)
+ segment.AddComponent(/datum/component/mob_chain, front = previous)
+ previous = segment
diff --git a/code/modules/admin/smites/bloodless.dm b/code/modules/admin/smites/bloodless.dm
index f42711c0f742..c970e920f225 100644
--- a/code/modules/admin/smites/bloodless.dm
+++ b/code/modules/admin/smites/bloodless.dm
@@ -9,10 +9,10 @@
return
var/mob/living/carbon/carbon_target = target
for(var/_limb in carbon_target.bodyparts)
- var/obj/item/bodypart/limb = _limb
- var/type_wound = pick(list(/datum/wound/slash/severe, /datum/wound/slash/moderate))
+ var/obj/item/bodypart/limb = _limb // fine to use this raw, its a meme smite
+ var/type_wound = pick(list(/datum/wound/slash/flesh/severe, /datum/wound/slash/flesh/moderate))
limb.force_wound_upwards(type_wound, smited = TRUE)
- type_wound = pick(list(/datum/wound/slash/critical, /datum/wound/slash/severe, /datum/wound/slash/moderate))
+ type_wound = pick(list(/datum/wound/slash/flesh/critical, /datum/wound/slash/flesh/severe, /datum/wound/slash/flesh/moderate))
limb.force_wound_upwards(type_wound, smited = TRUE)
- type_wound = pick(list(/datum/wound/slash/critical, /datum/wound/slash/severe))
+ type_wound = pick(list(/datum/wound/slash/flesh/critical, /datum/wound/slash/flesh/severe))
limb.force_wound_upwards(type_wound, smited = TRUE)
diff --git a/code/modules/admin/smites/boneless.dm b/code/modules/admin/smites/boneless.dm
index a18b439c5c0d..bf402abdfdb6 100644
--- a/code/modules/admin/smites/boneless.dm
+++ b/code/modules/admin/smites/boneless.dm
@@ -10,7 +10,12 @@
return
var/mob/living/carbon/carbon_target = target
- for(var/_limb in carbon_target.bodyparts)
- var/obj/item/bodypart/limb = _limb
- var/type_wound = pick(list(/datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/moderate))
- limb.force_wound_upwards(type_wound, smited = TRUE)
+ for(var/obj/item/bodypart/limb as anything in carbon_target.bodyparts)
+ var/severity = pick(list(
+ "[WOUND_SEVERITY_MODERATE]",
+ "[WOUND_SEVERITY_SEVERE]",
+ "[WOUND_SEVERITY_SEVERE]",
+ "[WOUND_SEVERITY_CRITICAL]",
+ "[WOUND_SEVERITY_CRITICAL]",
+ ))
+ carbon_target.cause_wound_of_type_and_severity(WOUND_BLUNT, limb, severity)
diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm
index 667e29e615b1..8407c364c192 100644
--- a/code/modules/admin/verbs/secrets.dm
+++ b/code/modules/admin/verbs/secrets.dm
@@ -541,7 +541,7 @@ GLOBAL_DATUM(everyone_a_traitor, /datum/everyone_is_a_traitor_controller)
var/spawnpoint = pick(GLOB.blobstart)
var/list/mob/dead/observer/candidates
var/mob/dead/observer/chosen_candidate
- var/mob/living/simple_animal/drone/nerd
+ var/mob/living/basic/drone/nerd
var/teamsize
teamsize = input(usr, "How many drones?", "N.E.R.D. team size", 2) as num|null
@@ -557,7 +557,7 @@ GLOBAL_DATUM(everyone_a_traitor, /datum/everyone_is_a_traitor_controller)
while(length(candidates) && teamsize)
chosen_candidate = pick(candidates)
candidates -= chosen_candidate
- nerd = new /mob/living/simple_animal/drone/classic(spawnpoint)
+ nerd = new /mob/living/basic/drone/classic(spawnpoint)
nerd.key = chosen_candidate.key
nerd.log_message("has been selected as a Nanotrasen emergency response drone.", LOG_GAME)
teamsize--
diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm
index 09759c72c6f8..0bdc6e525ace 100644
--- a/code/modules/admin/view_variables/topic_basic.dm
+++ b/code/modules/admin/view_variables/topic_basic.dm
@@ -61,22 +61,34 @@
names += componentsubtypes
names += "---Elements---"
names += sort_list(subtypesof(/datum/element), GLOBAL_PROC_REF(cmp_typepaths_asc))
+
var/result = tgui_input_list(usr, "Choose a component/element to add", "Add Component", names)
if(isnull(result))
return
if(!usr || result == "---Components---" || result == "---Elements---")
return
+
if(QDELETED(src))
to_chat(usr, "That thing doesn't exist anymore!", confidential = TRUE)
return
+
+ var/add_source
+ if(ispath(result, /datum/component))
+ var/datum/component/comp_path = result
+ if(initial(comp_path.dupe_mode) == COMPONENT_DUPE_SOURCES)
+ add_source = tgui_input_text(usr, "Enter a source for the component", "Add Component", "ADMIN-ABUSE")
+ if(isnull(add_source))
+ return
+
var/list/lst = get_callproc_args()
if(!lst)
return
+
var/datumname = "error"
lst.Insert(1, result)
if(result in componentsubtypes)
datumname = "component"
- target._AddComponent(lst)
+ target._AddComponent(lst, add_source)
else
datumname = "element"
target._AddElement(lst)
diff --git a/code/modules/antagonists/blob/blob_minion.dm b/code/modules/antagonists/blob/blob_minion.dm
index 9ba8e7089b9e..9bf37e961d5d 100644
--- a/code/modules/antagonists/blob/blob_minion.dm
+++ b/code/modules/antagonists/blob/blob_minion.dm
@@ -34,9 +34,3 @@
objective.owner = owner
objective.overmind = overmind
objectives += objective
-
-/datum/antagonist/blob_minion/blobbernaut
- name = "\improper Blobbernaut"
-
-/datum/antagonist/blob_minion/blob_zombie
- name = "\improper Blob Zombie"
diff --git a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
index db0d583e15f2..6f01eb67aa72 100644
--- a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
+++ b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
@@ -38,8 +38,6 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
/// Adds to claim, pulse, and expand range
var/core_range_bonus = 0
- /// The core can sustain this many extra spores with this strain
- var/core_spore_bonus = 0
/// Extra range up to which the core reinforces blobs
var/core_strong_reinforcement_range_bonus = 0
/// Extra range up to which the core reinforces blobs into reflectors
@@ -78,7 +76,6 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
overmind.color = complementary_color
if(overmind.blob_core)
- overmind.blob_core.max_spores += core_spore_bonus
overmind.blob_core.claim_range += core_range_bonus
overmind.blob_core.pulse_range += core_range_bonus
overmind.blob_core.expand_range += core_range_bonus
@@ -86,7 +83,6 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
overmind.blob_core.reflector_reinforce_range += core_reflector_reinforcement_range_bonus
for(var/obj/structure/blob/special/node/N as anything in overmind.node_blobs)
- N.max_spores += node_spore_bonus
N.claim_range += node_range_bonus
N.pulse_range += node_range_bonus
N.expand_range += node_range_bonus
@@ -100,26 +96,22 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
B.modify_max_integrity(B.max_integrity * max_structure_health_multiplier)
B.update_appearance()
- for(var/mob/living/simple_animal/hostile/blob/BM as anything in overmind.blob_mobs)
- BM.maxHealth *= max_mob_health_multiplier
- BM.health *= max_mob_health_multiplier
- BM.update_icons() //If it's getting a new strain, tell it what it does!
- to_chat(BM, "Your overmind's blob strain is now: [name]!")
- to_chat(BM, "The [name] strain [shortdesc ? "[shortdesc]" : "[description]"]")
+ for(var/mob/living/blob_mob as anything in overmind.blob_mobs)
+ blob_mob.maxHealth *= max_mob_health_multiplier
+ blob_mob.health *= max_mob_health_multiplier
+ blob_mob.update_icons() //If it's getting a new strain, tell it what it does!
+ to_chat(blob_mob, "Your overmind's blob strain is now: [name]!")
+ to_chat(blob_mob, "The [name] strain [shortdesc ? "[shortdesc]" : "[description]"]")
/datum/blobstrain/proc/on_lose()
if(overmind.blob_core)
- overmind.blob_core.max_spores -= core_spore_bonus
overmind.blob_core.claim_range -= core_range_bonus
- overmind.blob_core.pulse_range -= core_range_bonus
overmind.blob_core.expand_range -= core_range_bonus
overmind.blob_core.strong_reinforce_range -= core_strong_reinforcement_range_bonus
overmind.blob_core.reflector_reinforce_range -= core_reflector_reinforcement_range_bonus
for(var/obj/structure/blob/special/node/N as anything in overmind.node_blobs)
- N.max_spores -= node_spore_bonus
N.claim_range -= node_range_bonus
- N.pulse_range -= node_range_bonus
N.expand_range -= node_range_bonus
N.strong_reinforce_range -= node_strong_reinforcement_range_bonus
N.reflector_reinforce_range -= node_reflector_reinforcement_range_bonus
@@ -130,9 +122,9 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
for(var/obj/structure/blob/B as anything in overmind.all_blobs)
B.modify_max_integrity(B.max_integrity / max_structure_health_multiplier)
- for(var/mob/living/simple_animal/hostile/blob/BM as anything in overmind.blob_mobs)
- BM.maxHealth /= max_mob_health_multiplier
- BM.health /= max_mob_health_multiplier
+ for(var/mob/living/blob_mob as anything in overmind.blob_mobs)
+ blob_mob.maxHealth /= max_mob_health_multiplier
+ blob_mob.health /= max_mob_health_multiplier
/datum/blobstrain/proc/on_sporedeath(mob/living/spore)
diff --git a/code/modules/antagonists/blob/blobstrains/_reagent.dm b/code/modules/antagonists/blob/blobstrains/_reagent.dm
index aefd6c02f3c3..05bc73f95c8d 100644
--- a/code/modules/antagonists/blob/blobstrains/_reagent.dm
+++ b/code/modules/antagonists/blob/blobstrains/_reagent.dm
@@ -15,8 +15,9 @@
var/mob_protection = L.getarmor(null, BIO) * 0.01
reagent.expose_mob(L, VAPOR, BLOBMOB_BLOBBERNAUT_REAGENTATK_VOL+blobbernaut_reagentatk_bonus, FALSE, mob_protection, overmind)//this will do between 10 and 20 damage(reduced by mob protection), depending on chemical, plus 4 from base brute damage.
-/datum/blobstrain/reagent/on_sporedeath(mob/living/spore)
- spore.reagents.add_reagent(reagent.type, 10)
+/datum/blobstrain/reagent/on_sporedeath(mob/living/basic/spore)
+ var/burst_range = (spore.type == /mob/living/basic/blob_minion/spore) ? 1 : 0
+ do_chem_smoke(range = burst_range, holder = spore, location = get_turf(spore), reagent_type = reagent.type)
// These can only be applied by blobs. They are what (reagent) blobs are made out of.
/datum/reagent/blob
@@ -29,7 +30,7 @@
/// Used by blob reagents to calculate the reaction volume they should use when exposing mobs.
/datum/reagent/blob/proc/return_mob_expose_reac_volume(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
- if(exposed_mob.stat == DEAD || isblobmonster(exposed_mob))
+ if(exposed_mob.stat == DEAD || HAS_TRAIT(exposed_mob, TRAIT_BLOB_ALLY))
return 0 //the dead, and blob mobs, don't cause reactions
return round(reac_volume * min(1.5 - touch_protection, 1), 0.1) //full touch protection means 50% volume, any prot below 0.5 means 100% volume.
diff --git a/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
index f97599948a13..ea2bf54d7692 100644
--- a/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
+++ b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
@@ -11,18 +11,16 @@
message_living = ", and you feel tired"
reagent = /datum/reagent/blob/distributed_neurons
-/datum/blobstrain/reagent/distributed_neurons/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
- if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && damage <= 20 && B.get_integrity() - damage <= 0 && prob(15)) //if the cause isn't fire or a bomb, the damage is less than 21, we're going to die from that damage, 15% chance of a shitty spore.
- B.visible_message(span_warning("A spore floats free of the blob!"))
- var/mob/living/simple_animal/hostile/blob/blobspore/weak/BS = new/mob/living/simple_animal/hostile/blob/blobspore/weak(B.loc)
- BS.overmind = B.overmind
- BS.update_icons()
- B.overmind.blob_mobs.Add(BS)
+/datum/blobstrain/reagent/distributed_neurons/damage_reaction(obj/structure/blob/blob_tile, damage, damage_type, damage_flag)
+ if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && damage <= 20 && blob_tile.get_integrity() - damage <= 0 && prob(15)) //if the cause isn't fire or a bomb, the damage is less than 21, we're going to die from that damage, 15% chance of a shitty spore.
+ blob_tile.visible_message(span_boldwarning("A spore floats free of the blob!"))
+ blob_tile.overmind.create_spore(blob_tile.loc, /mob/living/basic/blob_minion/spore/minion/weak)
return ..()
/datum/reagent/blob/distributed_neurons
name = "Distributed Neurons"
color = "#E88D5D"
+ taste_description = "fizzing"
/datum/reagent/blob/distributed_neurons/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
. = ..()
@@ -33,10 +31,7 @@
exposed_mob.investigate_log("has been killed by distributed neurons (blob).", INVESTIGATE_DEATHS)
exposed_mob.death() //sleeping in a fight? bad plan.
if(exposed_mob.stat == DEAD && overmind.can_buy(5))
- var/mob/living/simple_animal/hostile/blob/blobspore/spore = new/mob/living/simple_animal/hostile/blob/blobspore(get_turf(exposed_mob))
- spore.overmind = overmind
- spore.update_icons()
- overmind.blob_mobs.Add(spore)
+ var/mob/living/basic/blob_minion/spore/minion/spore = overmind.create_spore(get_turf(exposed_mob))
spore.zombify(exposed_mob)
overmind.add_points(-5)
to_chat(overmind, span_notice("Spent 5 resources for the zombification of [exposed_mob]."))
diff --git a/code/modules/antagonists/blob/blobstrains/reactive_spines.dm b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
index 53f336ca419e..1c8cb893df83 100644
--- a/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
+++ b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
@@ -16,15 +16,15 @@
if(damage && ((damage_type == BRUTE) || (damage_type == BURN)) && B.get_integrity() - damage > 0 && COOLDOWN_FINISHED(src, retaliate_cooldown)) // Is there any damage, is it burn or brute, will we be alive, and has the cooldown finished?
COOLDOWN_START(src, retaliate_cooldown, 2.5 SECONDS) // 2.5 seconds before auto-retaliate can whack everything within 1 tile again
B.visible_message(span_boldwarning("The blob retaliates, lashing out!"))
- for(var/atom/A in range(1, B))
- if(!A.can_blob_attack())
+ for(var/atom/thing in range(1, B))
+ if(!thing.can_blob_attack())
continue
- var/attacked_turf = get_turf(A)
- if(isliving(A) && !isblobmonster(A)) // Make sure to inject strain-reagents with automatic attacks when needed.
+ var/attacked_turf = get_turf(thing)
+ if(isliving(thing) && !HAS_TRAIT(thing, TRAIT_BLOB_ALLY)) // Make sure to inject strain-reagents with automatic attacks when needed.
B.blob_attack_animation(attacked_turf, overmind)
- attack_living(A)
+ attack_living(thing)
- else if(A.blob_act(B)) // After checking for mobs, whack everything else with the standard attack
+ else if(thing.blob_act(B)) // After checking for mobs, whack everything else with the standard attack
B.blob_attack_animation(attacked_turf, overmind) // Only play the animation if the attack did something meaningful
return ..()
@@ -35,7 +35,7 @@
color = "#9ACD32"
/datum/reagent/blob/reactive_spines/return_mob_expose_reac_volume(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
- if(exposed_mob.stat == DEAD || isblobmonster(exposed_mob))
+ if(exposed_mob.stat == DEAD || HAS_TRAIT(exposed_mob, TRAIT_BLOB_ALLY))
return 0 //the dead, and blob mobs, don't cause reactions
return reac_volume
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
index 021c8bb12d51..f09108aa4448 100644
--- a/code/modules/antagonists/blob/overmind.dm
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -54,6 +54,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
var/list/strain_choices
/mob/camera/blob/Initialize(mapload, starting_points = OVERMIND_STARTING_POINTS)
+ ADD_TRAIT(src, TRAIT_BLOB_ALLY, INNATE_TRAIT)
validate_location()
blob_points = starting_points
manualplace_min_time += world.time
@@ -71,6 +72,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
SSshuttle.registerHostileEnvironment(src)
. = ..()
START_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs |= src
/mob/camera/blob/proc/validate_location()
var/turf/T = get_turf(src)
@@ -112,6 +114,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
to_chat(src, span_notice("The [blobstrain.name] strain [blobstrain.description]"))
if(blobstrain.effectdesc)
to_chat(src, span_notice("The [blobstrain.name] strain [blobstrain.effectdesc]"))
+ SEND_SIGNAL(src, COMSIG_BLOB_SELECTED_STRAIN, blobstrain)
/mob/camera/blob/can_z_move(direction, turf/start, turf/destination, z_move_flags = NONE, mob/living/rider)
if(placed) // The blob can't expand vertically (yet)
@@ -162,50 +165,70 @@ GLOBAL_LIST_EMPTY(blob_nodes)
priority_announce("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", ANNOUNCER_OUTBREAK5)
has_announced = TRUE
+/// Create a blob spore and link it to us
+/mob/camera/blob/proc/create_spore(turf/spore_turf, spore_type = /mob/living/basic/blob_minion/spore/minion)
+ var/mob/living/basic/blob_minion/spore/spore = new spore_type(spore_turf)
+ assume_direct_control(spore)
+ return spore
+
+/// Give our new minion the properties of a minion
+/mob/camera/blob/proc/assume_direct_control(mob/living/minion)
+ minion.AddComponent(/datum/component/blob_minion, src)
+
+/// Add something to our list of mobs and wait for it to die
+/mob/camera/blob/proc/register_new_minion(mob/living/minion)
+ blob_mobs |= minion
+ if (!istype(minion, /mob/living/basic/blob_minion/blobbernaut))
+ RegisterSignal(minion, COMSIG_LIVING_DEATH, PROC_REF(on_minion_death))
+
+/// When a spore (or zombie) dies then we do this
+/mob/camera/blob/proc/on_minion_death(mob/living/spore)
+ SIGNAL_HANDLER
+ blobstrain.on_sporedeath(spore)
+
/mob/camera/blob/proc/victory()
sound_to_playing_players('sound/machines/alarm.ogg')
sleep(10 SECONDS)
- for(var/i in GLOB.mob_living_list)
- var/mob/living/L = i
- var/turf/T = get_turf(L)
- if(!T || !is_station_level(T.z))
+ for(var/mob/living/live_guy as anything in GLOB.mob_living_list)
+ var/turf/guy_turf = get_turf(live_guy)
+ if(isnull(guy_turf) || !is_station_level(guy_turf.z))
continue
- if(L in GLOB.overminds || (L.pass_flags & PASSBLOB))
+ if(live_guy in GLOB.overminds || (live_guy.pass_flags & PASSBLOB))
continue
- var/area/Ablob = get_area(T)
-
- if(!(Ablob.area_flags & BLOBS_ALLOWED))
+ var/area/blob_area = get_area(guy_turf)
+ if(!(blob_area.area_flags & BLOBS_ALLOWED))
continue
- if(!(ROLE_BLOB in L.faction))
- playsound(L, 'sound/effects/splat.ogg', 50, TRUE)
- if(L.stat != DEAD)
- L.investigate_log("has died from blob takeover.", INVESTIGATE_DEATHS)
- L.death()
- new/mob/living/simple_animal/hostile/blob/blobspore(T)
+ if(!(ROLE_BLOB in live_guy.faction))
+ playsound(live_guy, 'sound/effects/splat.ogg', 50, TRUE)
+ if(live_guy.stat != DEAD)
+ live_guy.investigate_log("has died from blob takeover.", INVESTIGATE_DEATHS)
+ live_guy.death()
+ create_spore(guy_turf)
else
- L.fully_heal()
+ live_guy.fully_heal()
- for(var/area/A in GLOB.areas)
- if(!(A.type in GLOB.the_station_areas))
+ for(var/area/check_area in GLOB.areas)
+ if(!is_type_in_list(check_area, GLOB.the_station_areas))
continue
- if(!(A.area_flags & BLOBS_ALLOWED))
+ if(!(check_area.area_flags & BLOBS_ALLOWED))
continue
- A.color = blobstrain.color
- A.name = "blob"
- A.icon = 'icons/mob/nonhuman-player/blob.dmi'
- A.icon_state = "blob_shield"
- A.layer = BELOW_MOB_LAYER
- A.invisibility = 0
- A.blend_mode = 0
+ check_area.color = blobstrain.color
+ check_area.name = "blob"
+ check_area.icon = 'icons/mob/nonhuman-player/blob.dmi'
+ check_area.icon_state = "blob_shield"
+ check_area.layer = BELOW_MOB_LAYER
+ check_area.invisibility = 0
+ check_area.blend_mode = 0
+
var/datum/antagonist/blob/B = mind.has_antag_datum(/datum/antagonist/blob)
if(B)
var/datum/objective/blob_takeover/main_objective = locate() in B.objectives
if(main_objective)
main_objective.completed = TRUE
- to_chat(world, span_blob("[real_name] consumed the station in an unstoppable tide!"))
+ to_chat(world, span_blobannounce("[real_name] consumed the station in an unstoppable tide!"))
SSticker.news_report = BLOB_WIN
SSticker.force_ending = TRUE
@@ -216,11 +239,6 @@ GLOBAL_LIST_EMPTY(blob_nodes)
if(B && B.overmind == src)
B.overmind = null
B.update_appearance() //reset anything that was ours
- for(var/BLO in blob_mobs)
- var/mob/living/simple_animal/hostile/blob/BM = BLO
- if(BM)
- BM.overmind = null
- BM.update_icons()
for(var/obj/structure/blob/blob_structure as anything in all_blobs)
blob_structure.overmind = null
all_blobs = null
@@ -233,6 +251,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
SSshuttle.clearHostileEnvironment(src)
STOP_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs -= src
return ..()
@@ -240,7 +259,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
. = ..()
if(!. || !client)
return FALSE
- to_chat(src, span_blob("You are the overmind!"))
+ to_chat(src, span_blobannounce("You are the overmind!"))
if(!placed && autoplace_max_time <= world.time)
to_chat(src, span_boldannounce("You will automatically place your blob core in [DisplayTimeText(autoplace_max_time - world.time)]."))
to_chat(src, span_boldannounce("You [manualplace_min_time ? "will be able to":"can"] manually place your blob core by pressing the Place Blob Core button in the bottom right corner of the screen."))
@@ -257,9 +276,11 @@ GLOBAL_LIST_EMPTY(blob_nodes)
return FALSE
var/current_health = round((blob_core.get_integrity() / blob_core.max_integrity) * 100)
hud_used.healths.maptext = MAPTEXT("