diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm
index 7a8c7bfacdcb..b81d20e262d2 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_shuttle_wreckage.dmm
@@ -284,7 +284,7 @@
/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace,
/area/ruin/unpowered)
"KN" = (
-/obj/structure/spawner/lavaland/legion,
+/mob/living/basic/mining/tendril,
/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace,
/area/ruin/unpowered)
"Lz" = (
diff --git a/_maps/templates/lazy_templates/nukie_base.dmm b/_maps/templates/lazy_templates/nukie_base.dmm
index 14088985fbcb..f625537b0fe0 100644
--- a/_maps/templates/lazy_templates/nukie_base.dmm
+++ b/_maps/templates/lazy_templates/nukie_base.dmm
@@ -254,7 +254,7 @@
/area/centcom/syndicate_mothership)
"du" = (
/obj/structure/chair/stool/directional/north,
-/obj/effect/landmark/start/nukeop,
+/obj/effect/landmark/start/nukeop_base,
/obj/structure/sign/poster/contraband/donk_co/directional/south,
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
@@ -1110,7 +1110,7 @@
/turf/open/floor/mineral/plastitanium/red,
/area/centcom/syndicate_mothership/expansion_bioterrorism)
"my" = (
-/obj/effect/landmark/start/nukeop_leader,
+/obj/effect/landmark/start/nukeop_base/leader,
/obj/effect/turf_decal/tile/bar/opposingcorners,
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
@@ -1168,7 +1168,8 @@
/obj/structure/fans/tiny,
/obj/machinery/door/poddoor/shutters/syndicate{
id = "FBBZ1";
- name = "Security Shutters"
+ name = "Security Shutters";
+ dir = 1
},
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
@@ -1425,7 +1426,7 @@
/obj/structure/chair/greyscale{
dir = 4
},
-/obj/effect/landmark/start/nukeop_overwatch,
+/obj/effect/landmark/start/nukeop_base/overwatch,
/turf/open/floor/mineral/plastitanium,
/area/centcom/syndicate_mothership)
"pU" = (
@@ -1612,10 +1613,19 @@
},
/obj/effect/turf_decal/siding/wideplating,
/obj/machinery/door/poddoor/shutters/syndicate/indestructible{
- name = "Subterrainian Cargo Lift"
+ name = "Base Lift";
+ dir = 1;
+ id_tag = "nukiespawnlift";
+ id = "nukiespawnlift"
},
/turf/open/floor/iron/dark/textured_half,
/area/centcom/syndicate_mothership/control)
+"rw" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 6
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
"rA" = (
/obj/effect/turf_decal/siding/purple{
dir = 1
@@ -2016,6 +2026,12 @@
/obj/effect/turf_decal/siding/thinplating_new/dark,
/turf/open/floor/mineral/titanium,
/area/centcom/syndicate_mothership/control)
+"we" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 5
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
"wg" = (
/obj/structure/fence/corner{
dir = 6
@@ -2080,6 +2096,10 @@
dir = 8
},
/area/centcom/syndicate_mothership/control)
+"wS" = (
+/obj/effect/landmark/nukeop_elevator/exterior,
+/turf/closed/indestructible/syndicate,
+/area/centcom/syndicate_mothership/control)
"wW" = (
/obj/structure/flora/rock/pile/style_random,
/obj/effect/light_emitter{
@@ -2376,7 +2396,9 @@
},
/obj/structure/fans/tiny,
/obj/effect/turf_decal/tile/bar/opposingcorners,
-/obj/machinery/door/poddoor/shutters/syndicate,
+/obj/machinery/door/poddoor/shutters/syndicate{
+ dir = 1
+ },
/turf/open/floor/iron,
/area/centcom/syndicate_mothership/control)
"AO" = (
@@ -2405,7 +2427,7 @@
icon_state = "map-left-MS";
pixel_y = 32
},
-/obj/effect/landmark/start/nukeop,
+/obj/effect/landmark/start/nukeop_base,
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
"AW" = (
@@ -2721,9 +2743,6 @@
/obj/structure/flora/rock/icy/style_random,
/turf/open/misc/asteroid/snow/icemoon,
/area/centcom/syndicate_mothership/control)
-"Ev" = (
-/turf/open/floor/plating/elevatorshaft,
-/area/centcom/syndicate_mothership/control)
"Ez" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden,
/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2,
@@ -3206,6 +3225,12 @@
},
/turf/open/floor/iron/dark/textured_large,
/area/centcom/syndicate_mothership/control)
+"Ja" = (
+/obj/machinery/light/small/red/directional/north{
+ allow_break_on_init = 0
+ },
+/turf/open/floor/mineral/plastitanium,
+/area/centcom/syndicate_mothership/control)
"Jc" = (
/obj/structure/table/wood,
/obj/machinery/chem_dispenser/drinks/beer{
@@ -3596,7 +3621,7 @@
dir = 8
},
/obj/structure/chair/stool/directional/east,
-/obj/effect/landmark/start/nukeop,
+/obj/effect/landmark/start/nukeop_base,
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
"Nb" = (
@@ -3883,15 +3908,6 @@
/obj/structure/flora/grass/both/style_random,
/turf/open/misc/asteroid/snow/airless,
/area/centcom/syndicate_mothership)
-"Qe" = (
-/obj/machinery/button/door/directional/south{
- desc = "Looks like the elevator is under maintenance..";
- name = "Elevator Button"
- },
-/turf/open/floor/iron/smooth_half{
- dir = 1
- },
-/area/centcom/syndicate_mothership/control)
"Qh" = (
/obj/structure/window/reinforced/survival_pod/spawner/directional/south{
name = "Frosted Window"
@@ -4336,6 +4352,12 @@
},
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
+"TH" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 10
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
"TK" = (
/obj/structure/barricade/sandbags,
/obj/effect/light_emitter{
@@ -4643,7 +4665,7 @@
/area/centcom/syndicate_mothership)
"Xu" = (
/obj/structure/chair/stool/directional/west,
-/obj/effect/landmark/start/nukeop,
+/obj/effect/landmark/start/nukeop_base,
/turf/open/floor/wood/tile,
/area/centcom/syndicate_mothership/control)
"Xv" = (
@@ -4726,6 +4748,12 @@
dir = 1
},
/area/centcom/syndicate_mothership/control)
+"XZ" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 9
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
"Yd" = (
/obj/machinery/camera/autoname/directional/south{
network = list("nukie")
@@ -6727,7 +6755,7 @@ sk
DW
RD
EM
-RD
+EM
RD
Ox
sU
@@ -9569,10 +9597,10 @@ ZZ
Me
wc
wG
-Qe
+wG
DZ
-Ev
-Ev
+Ja
+Ov
DZ
DZ
nS
@@ -9673,9 +9701,9 @@ zH
wG
wG
ru
-Ev
-Ev
-Ev
+XZ
+TH
+Ov
DZ
Ox
Vm
@@ -9775,9 +9803,9 @@ zH
wG
wG
ru
-Ev
-Ev
-Ev
+we
+rw
+Ov
DZ
Ox
To
@@ -9877,8 +9905,8 @@ wc
wG
lE
DZ
-Ev
-Ev
+Ja
+Ov
DZ
DZ
Ox
@@ -9978,7 +10006,7 @@ go
as
wG
wG
-DZ
+wS
DZ
DZ
DZ
diff --git a/_maps/templates/lazy_templates/nukie_elevator.dmm b/_maps/templates/lazy_templates/nukie_elevator.dmm
new file mode 100644
index 000000000000..6993eedc19ca
--- /dev/null
+++ b/_maps/templates/lazy_templates/nukie_elevator.dmm
@@ -0,0 +1,100 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"a" = (
+/turf/closed/indestructible/syndicate,
+/area/centcom/syndicate_mothership/control)
+"j" = (
+/obj/effect/landmark/start/nukeop_elevator,
+/obj/machinery/light/small/red/directional/north{
+ allow_break_on_init = 0
+ },
+/turf/open/floor/mineral/plastitanium,
+/area/centcom/syndicate_mothership/control)
+"l" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 6
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"s" = (
+/turf/open/floor/plating,
+/area/centcom/syndicate_mothership/control)
+"u" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 5
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"w" = (
+/obj/effect/landmark/start/nukeop_elevator,
+/turf/open/floor/mineral/plastitanium,
+/area/centcom/syndicate_mothership/control)
+"F" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 10
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"I" = (
+/obj/effect/turf_decal/siding/thinplating_new/dark{
+ dir = 9
+ },
+/turf/open/floor/mineral/plastitanium/red,
+/area/centcom/syndicate_mothership/control)
+"T" = (
+/obj/effect/landmark/nukeop_elevator/interior,
+/turf/closed/indestructible/syndicate,
+/area/centcom/syndicate_mothership/control)
+"W" = (
+/obj/effect/turf_decal/siding/wideplating{
+ dir = 1
+ },
+/obj/effect/turf_decal/siding/wideplating,
+/obj/machinery/door/poddoor/shutters/syndicate/indestructible{
+ name = "Base Lift";
+ dir = 1
+ },
+/turf/open/floor/iron/dark/textured_half,
+/area/centcom/syndicate_mothership/control)
+
+(1,1,1) = {"
+a
+a
+a
+a
+s
+"}
+(2,1,1) = {"
+a
+j
+w
+a
+a
+"}
+(3,1,1) = {"
+W
+I
+F
+w
+a
+"}
+(4,1,1) = {"
+W
+u
+l
+w
+a
+"}
+(5,1,1) = {"
+a
+j
+w
+a
+a
+"}
+(6,1,1) = {"
+T
+a
+a
+a
+s
+"}
diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm
index 1f54a54784d0..8a4e9d56aa53 100644
--- a/code/__DEFINES/ai/monsters.dm
+++ b/code/__DEFINES/ai/monsters.dm
@@ -52,6 +52,11 @@
/// Key where goliath stores a hole it wants to get into
#define BB_GOLIATH_HOLE_TARGET "BB_goliath_hole"
+// Tendril AI keys
+#define BB_TENDRIL_LASH "tendril_lash"
+#define BB_TENDRIL_CHASER "tendril_chaser"
+#define BB_TENDRIL_SPIKES "tendril_spikes"
+
// bee keys
///the bee hive we live inside
#define BB_CURRENT_HOME "BB_current_home"
diff --git a/code/__DEFINES/dcs/signals/signals_mining.dm b/code/__DEFINES/dcs/signals/signals_mining.dm
index 13ec648c1ba6..494068f37078 100644
--- a/code/__DEFINES/dcs/signals/signals_mining.dm
+++ b/code/__DEFINES/dcs/signals/signals_mining.dm
@@ -4,6 +4,9 @@
/// Fired by a mob which has been grabbed by a goliath
#define COMSIG_GOLIATH_TENTACLED_GRABBED "comsig_goliath_tentacle_grabbed"
#define COMPONENT_GOLIATH_CANCEL_TENTACLE_GRAB (1<<0)
+/// Fired by a mob which has been grabbed by a tendril
+#define COMSIG_TENDRIL_TENTACLED_GRABBED "comsig_tendril_tentacle_grabbed"
+ #define COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB (1<<0)
/// Fired by a goliath tentacle which is returning to the earth
#define COMSIG_GOLIATH_TENTACLE_RETRACTING "comsig_goliath_tentacle_retracting"
/// Fired by a mob which has triggered a brimdust explosion from itself (not the mobs that get hit)
diff --git a/code/__DEFINES/lazy_templates.dm b/code/__DEFINES/lazy_templates.dm
index 1e8dca9a7a11..77dac7bd96e0 100644
--- a/code/__DEFINES/lazy_templates.dm
+++ b/code/__DEFINES/lazy_templates.dm
@@ -1,3 +1,4 @@
+#define LAZY_TEMPLATE_KEY_NUKIEELEVATOR "LT_NUKIEELEVATOR"
#define LAZY_TEMPLATE_KEY_NUKIEBASE "LT_NUKIEBASE"
#define LAZY_TEMPLATE_KEY_WIZARDDEN "LT_WIZARDDEN"
#define LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY "LT_NINJAHOLDING"
@@ -6,6 +7,7 @@
#define LAZY_TEMPLATE_KEY_VOIDWALKER_VOID "LT_VOIDWALKERVOID"
#define LAZY_TEMPLATE_KEY_LIST_ALL(...) list( \
+ "Nukie Elevator" = LAZY_TEMPLATE_KEY_NUKIEELEVATOR, \
"Nukie Base" = LAZY_TEMPLATE_KEY_NUKIEBASE, \
"Wizard Den" = LAZY_TEMPLATE_KEY_WIZARDDEN, \
"Ninja Holding" = LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY, \
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index d8f1c9539edf..a9342860e213 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -1669,6 +1669,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Sunlight on this turf is blocked and thus you can't get solar power or whatever
#define TRAIT_TURF_SUN_BLOCKED "turf_sun_blocked"
+/// Cannot be backstabbed with a crusher
+#define TRAIT_BACKSTAB_IMMUNE "backstab_immune"
+
/// Makes the owner immune from the pacification from synthpax
#define TRAIT_SYNTHPAX_IMMUNE "synthpax_immune"
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 11b20cfb2104..3380da135819 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -1426,7 +1426,7 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects)
// Reorder the 2d pixel data of the passed in frames into a data string that can be passed to rustg_dmi_create_png
/proc/reorder_pixels(icon_width, icon_height, grid_width, grid_height, list/frames)
- var/file_height = icon_height * grid_height
+ var/file_width = icon_width * grid_width
// This little trick right here reduces the total iteration of repeat_string from the product of the arguments to their sum.
// Can't be applied to the general case without a complex partitioning algorithm,
@@ -1443,7 +1443,7 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects)
var/column = (i-1)%grid_width
for(var/y in 1 to length(frame))
var/list/row = jointext(frame[y], "")
- var/splice_start = (row_index+y-1)*file_height + column*icon_width + 1
+ var/splice_start = (row_index*icon_height+y-1)*file_width + column*icon_width + 1
linear_pixels = splicetext(linear_pixels, (splice_start-1)*9+1, (splice_start+icon_width-1)*9+1, row)
var/zero_alpha_regex = regex(@@#(?:(?!a0a0a0)([0-9]|[a-f]){6}00)@, "gi")
linear_pixels = replacetext(linear_pixels, zero_alpha_regex, COLOR_DMI_MASK)
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index ee0666fd90c2..9cc1fd3f9960 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -302,6 +302,16 @@
return TRUE
+/proc/filter_illegal_filename_chars(name)
+ var/regex/illegal_chars = new("\[\\/:*?\"<>|]", "g")
+ return !findtext(name, illegal_chars)
+
+/proc/filter_filename_pda(name)
+ if(is_ic_filtered_for_pdas(name) || is_soft_ic_filtered_for_pdas(name))
+ return FALSE
+ if(!filter_illegal_filename_chars(name))
+ return FALSE
+ return TRUE
//html_encode helper proc that returns the smallest non null of two numbers
//or 0 if they're both null (needed because of findtext returning 0 when a value is not present)
diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm
index de7dd1494c62..548f15570536 100644
--- a/code/_globalvars/lists/maintenance_loot.dm
+++ b/code/_globalvars/lists/maintenance_loot.dm
@@ -319,7 +319,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items
/obj/item/weldingtool/hugetank = 1,
/obj/item/fishing_rod/telescopic/master = 1,
/obj/item/spess_knife = 1,
- /obj/item/gun/ballistic/automatic/pistol/doorhickey = 1,
+ /obj/item/gun/ballistic/automatic/pistol/doohickey = 1,
) = 1,
list(//equipment
diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm
index f7e4a4cc0b9f..7264922bfce6 100644
--- a/code/_globalvars/lists/mapping.dm
+++ b/code/_globalvars/lists/mapping.dm
@@ -126,9 +126,6 @@ GLOBAL_LIST_EMPTY(jobspawn_overrides)
GLOBAL_LIST_EMPTY(gorilla_start)
GLOBAL_LIST_EMPTY(wizardstart)
-GLOBAL_LIST_EMPTY(nukeop_start)
-GLOBAL_LIST_EMPTY(nukeop_leader_start)
-GLOBAL_LIST_EMPTY(nukeop_overwatch_start)
GLOBAL_LIST_EMPTY(newplayer_start)
GLOBAL_LIST_EMPTY(prisonwarp) //admin prisoners go to these
GLOBAL_LIST_EMPTY(holdingfacility) //captured people go here (ninja energy net)
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index a4dbd2ff5b06..1b36312f0129 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -196,6 +196,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_ANXIOUS" = TRAIT_ANXIOUS,
"TRAIT_APATHETIC" = TRAIT_APATHETIC,
"TRAIT_ATTEMPTING_CHRISTENING" = TRAIT_ATTEMPTING_CHRISTENING,
+ "TRAIT_BACKSTAB_IMMUNE" = TRAIT_BACKSTAB_IMMUNE,
"TRAIT_BADDNA" = TRAIT_BADDNA,
"TRAIT_BADTOUCH" = TRAIT_BADTOUCH,
"TRAIT_BALD" = TRAIT_BALD,
diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
index c967112f14fc..2143b6cbb001 100644
--- a/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
+++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_roundstart.dm
@@ -250,12 +250,76 @@
)
min_pop = 30
min_antag_cap = list("denominator" = 18, "offset" = 1)
- ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
+ ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEELEVATOR)
repeatable = FALSE
/datum/dynamic_ruleset/roundstart/nukies/prepare_for_role(datum/mind/candidate)
+ // they all get the normal operative job, even the leader. (leader's job is updated when they get the antag datum)
+ // all this ultimately matters for is 1. ensuring they *don't* get a normal job and 2. spawning them in the elevator
LAZYSET(SSjob.forced_occupations, candidate, /datum/job/nuclear_operative)
+/datum/dynamic_ruleset/roundstart/nukies/execute()
+ . = ..()
+ addtimer(CALLBACK(src, PROC_REF(load_nukie_base)), 3 SECONDS)
+
+#define ELEVATOR_WIDTH 6
+#define ELEVATOR_HEIGHT 5
+
+/datum/dynamic_ruleset/roundstart/nukies/proc/load_nukie_base()
+ SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE)
+ addtimer(CALLBACK(src, PROC_REF(teleport_elevator_contents)), 2 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(open_nukie_elevator)), 3 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(call_infiltrator)), 5 SECONDS)
+
+/datum/dynamic_ruleset/roundstart/nukies/proc/teleport_elevator_contents()
+ var/obj/effect/landmark/nukeop_elevator/interior/interior = locate() in GLOB.landmarks_list
+ var/obj/effect/landmark/nukeop_elevator/exterior/exterior = locate() in GLOB.landmarks_list
+ if(isnull(interior) || isnull(exterior))
+ stack_trace("Failed to find nukie elevator landmarks during load!")
+ message_admins("Failed to find nukie elevator landmarks during load, you might have some stuck nukies that need help.")
+ return
+
+ var/turf/top_right_interior = get_turf(interior)
+ var/turf/top_right_exterior = get_turf(exterior)
+
+ var/list/turf/elevator_turfs = block(
+ top_right_interior.x,
+ top_right_interior.y,
+ top_right_interior.z,
+ top_right_interior.x - ELEVATOR_WIDTH + 1,
+ top_right_interior.y - ELEVATOR_HEIGHT + 1,
+ top_right_interior.z,
+ )
+
+ for(var/turf/elevator_turf as anything in elevator_turfs)
+ var/turf/destination_turf = locate(
+ top_right_exterior.x - (top_right_interior.x - elevator_turf.x),
+ top_right_exterior.y - (top_right_interior.y - elevator_turf.y),
+ top_right_exterior.z,
+ )
+ if(isnull(destination_turf))
+ stack_trace("Failed to find destination turf for nukie elevator!")
+ message_admins("Failed to find corresponding elevator turf for nuke ops, you might have some stuck nukies that need help.")
+ continue
+
+ for(var/atom/movable/thing in elevator_turf)
+ if(thing.anchored || thing.orbit_target)
+ continue
+ thing.forceMove(destination_turf)
+
+/datum/dynamic_ruleset/roundstart/nukies/proc/open_nukie_elevator()
+ for(var/obj/machinery/door/poddoor/shutter in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door/poddoor))
+ if(shutter.id_tag == "nukiespawnlift")
+ shutter.open()
+
+/datum/dynamic_ruleset/roundstart/nukies/proc/call_infiltrator()
+ for(var/datum/mind/leader_mind as anything in selected_minds)
+ var/datum/antagonist/nukeop/leader/nuke_leader = leader_mind.has_antag_datum(/datum/antagonist/nukeop/leader)
+ nuke_leader?.spawn_infiltrator()
+
+#undef ELEVATOR_WIDTH
+#undef ELEVATOR_HEIGHT
+
/datum/dynamic_ruleset/roundstart/nukies/create_execute_args()
return list(
new /datum/team/nuclear(),
@@ -264,9 +328,14 @@
/datum/dynamic_ruleset/roundstart/nukies/assign_role(datum/mind/candidate, datum/team/nuke_team, datum/mind/most_experienced)
if(most_experienced == candidate)
- candidate.add_antag_datum(/datum/antagonist/nukeop/leader, nuke_team)
+ var/datum/antagonist/nukeop/leader/nuke_leader = new()
+ nuke_leader.send_to_spawnpoint = FALSE // because they get spawned in the elevator
+ nuke_leader.spawn_ship = FALSE // spawned manually later
+ candidate.add_antag_datum(nuke_leader, nuke_team)
else
- candidate.add_antag_datum(/datum/antagonist/nukeop, nuke_team)
+ var/datum/antagonist/nukeop/nukie = new()
+ nukie.send_to_spawnpoint = FALSE // ditto
+ candidate.add_antag_datum(nukie, nuke_team)
/datum/dynamic_ruleset/roundstart/nukies/round_result()
var/datum/antagonist/nukeop/nukie = selected_minds[1].has_antag_datum(/datum/antagonist/nukeop)
diff --git a/code/datums/actions/mobs/projectileattack.dm b/code/datums/actions/mobs/projectileattack.dm
index f27c0671c1f3..183df71e03f1 100644
--- a/code/datums/actions/mobs/projectileattack.dm
+++ b/code/datums/actions/mobs/projectileattack.dm
@@ -5,7 +5,7 @@
desc = "Fires a set of projectiles at a selected target."
cooldown_time = 1.5 SECONDS
/// The type of the projectile to be fired
- var/projectile_type
+ var/obj/projectile/projectile_type
/// The sound played when a projectile is fired
var/projectile_sound
/// If the projectile should home in on its target
@@ -58,7 +58,7 @@
our_projectile.set_homing_target(target)
if(isnum(set_angle))
our_projectile.fire(set_angle)
- return
+ return our_projectile
our_projectile.fire()
return our_projectile
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 9d9f995e17fd..5b41ae5254d2 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
@@ -24,17 +24,8 @@
var/atom/target = controller.blackboard[target_key]
if (isnull(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
- if (!target.IsReachableBy(controller.pawn))
- controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
- return AI_BEHAVIOR_INSTANT
- var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER]
- if (isnull(can_attack_time))
- var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY]
- var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay
- controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay)
- return AI_BEHAVIOR_INSTANT
- if (can_attack_time > world.time)
+ if (!can_attack(controller, target))
return AI_BEHAVIOR_INSTANT
if (isliving(controller.pawn))
@@ -56,6 +47,23 @@
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY
+/datum/ai_behavior/basic_melee_attack/proc/can_attack(datum/ai_controller/controller, atom/target)
+ if (!target.IsReachableBy(controller.pawn))
+ controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
+ return FALSE
+
+ var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER]
+ if (isnull(can_attack_time))
+ var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY]
+ var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay
+ controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay)
+ return FALSE
+
+ if (can_attack_time > world.time)
+ return FALSE
+
+ return TRUE
+
/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
@@ -78,7 +86,7 @@
/// 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
+ var/avoid_friendly_fire = FALSE
/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
index bf7d128fef9c..b2aaaa235788 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
@@ -35,7 +35,6 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
-
controller.clear_blackboard_key(target_key)
// If we're using a field rn, just don't do anything yeah?
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 525248529615..9824d41f8238 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
@@ -3,10 +3,12 @@
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Targeting strategy key to use
var/strategy_key = BB_TARGETING_STRATEGY
+ /// Behavior to use to find targets
+ var/target_behavior = /datum/ai_behavior/find_potential_targets
/datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ controller.queue_behavior(target_behavior, target_key, strategy_key, 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
@@ -21,9 +23,8 @@
target_key = BB_BASIC_MOB_FLEE_TARGET
/datum/ai_planning_subtree/simple_find_target/increased_range
-
-/datum/ai_planning_subtree/simple_find_target/increased_range/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
- controller.queue_behavior(/datum/ai_behavior/find_potential_targets/bigger_range, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ target_behavior = /datum/ai_behavior/find_potential_targets/bigger_range
/datum/ai_planning_subtree/simple_find_target/hunt
strategy_key = BB_HUNT_TARGETING_STRATEGY
+
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 f1c04e1d8a78..c86994054613 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
@@ -9,9 +9,11 @@
var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
/// do we check for faction?
var/check_faction = FALSE
+ /// Behavior to use to select our target
+ var/target_behavior = /datum/ai_behavior/target_from_retaliate_list
/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, targeting_strategy_key, hiding_place_key, check_faction)
+ controller.queue_behavior(target_behavior, 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
diff --git a/code/datums/ai/telegraph_effects.dm b/code/datums/ai/telegraph_effects.dm
index 62bbe9a0397e..b658c651988a 100644
--- a/code/datums/ai/telegraph_effects.dm
+++ b/code/datums/ai/telegraph_effects.dm
@@ -6,6 +6,14 @@
light_range = 1
duration = 2 SECONDS
+/obj/effect/temp_visual/telegraphing/Initialize(mapload)
+ . = ..()
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/effect/temp_visual/telegraphing/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, icon_state, src, alpha = 90)
+
/obj/effect/temp_visual/telegraphing/vending_machine_tilt
duration = 1 SECONDS
@@ -15,7 +23,18 @@
src.duration = duration
return ..()
-/obj/effect/temp_visual/telegraphing/thunderbolt
+/obj/effect/temp_visual/telegraphing/circle
icon = 'icons/mob/telegraphing/telegraph.dmi'
icon_state = "target_circle"
duration = 2 SECONDS
+
+/obj/effect/temp_visual/telegraphing/circle/short
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/telegraphing/line
+ icon = 'icons/mob/telegraphing/telegraph.dmi'
+ icon_state = "line"
+ duration = 0.8 SECONDS
+
+/obj/effect/temp_visual/telegraphing/line/short
+ duration = 0.5 SECONDS
diff --git a/code/datums/components/crafting/ranged_weapon.dm b/code/datums/components/crafting/ranged_weapon.dm
index 53929a6f68db..ae234ea7516b 100644
--- a/code/datums/components/crafting/ranged_weapon.dm
+++ b/code/datums/components/crafting/ranged_weapon.dm
@@ -292,8 +292,8 @@
return ..()
/datum/crafting_recipe/deagle_prime_mag
- name = "Regal Condor Magazine (10mm Reaper)"
- result = /obj/item/ammo_box/magazine/r10mm
+ name = "Regal Condor Magazine (.45 Reaper)"
+ result = /obj/item/ammo_box/magazine/r45
reqs = list(
/obj/item/stack/sheet/iron = 10,
/obj/item/stack/sheet/mineral/gold = 10,
diff --git a/code/datums/components/crafting/tools.dm b/code/datums/components/crafting/tools.dm
index 29ea7a60010f..b1d2c107866e 100644
--- a/code/datums/components/crafting/tools.dm
+++ b/code/datums/components/crafting/tools.dm
@@ -131,6 +131,10 @@
)
category = CAT_TOOLS
+/datum/crafting_recipe/jaws_of_recovery/New()
+ LAZYADD(blacklist, typecacheof(/obj/item/crowbar/power/paramedic, ignore_root_path = TRUE))
+ return ..()
+
/datum/crafting_recipe/lantern
name = "Lantern"
result = /obj/item/flashlight/lantern
diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm
index 9fe85cbcbcd2..d527d46b6d65 100644
--- a/code/datums/lazy_template.dm
+++ b/code/datums/lazy_template.dm
@@ -112,6 +112,10 @@
reservations += reservation
return reservation
+/datum/lazy_template/nukie_elevator
+ key = LAZY_TEMPLATE_KEY_NUKIEELEVATOR
+ map_name = "nukie_elevator"
+
/datum/lazy_template/nukie_base
key = LAZY_TEMPLATE_KEY_NUKIEBASE
map_name = "nukie_base"
diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm
index 097e8740aff7..63b47165db47 100644
--- a/code/datums/mapgen/CaveGenerator.dm
+++ b/code/datums/mapgen/CaveGenerator.dm
@@ -325,7 +325,7 @@
is_megafauna = TRUE
var/can_spawn = TRUE
- if(ispath(picked_mob, /obj/structure/spawner/lavaland))
+ if(ispath(picked_mob, /mob/living/basic/mining/tendril))
// Prevents tendrils spawning in each other's collapse range
for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL])
if (get_dist(spawn_turf, target_turf) <= 2)
@@ -352,7 +352,7 @@
if (!can_spawn)
continue
- if (ispath(picked_mob, /obj/structure/spawner/lavaland))
+ if (ispath(picked_mob, /mob/living/basic/mining/tendril))
spawn_data[CAVE_SPAWN_TENDRIL] += target_turf
else
if (is_megafauna)
diff --git a/code/datums/mapgen/biomes/_biome.dm b/code/datums/mapgen/biomes/_biome.dm
index 8dc588680a8c..dc2a17563b37 100644
--- a/code/datums/mapgen/biomes/_biome.dm
+++ b/code/datums/mapgen/biomes/_biome.dm
@@ -33,6 +33,8 @@
var/mob_exclusion_radius = 12
/// Radius around megafauna within which we avoid spawning tendrils
var/megafauna_exclusion_radius = 7
+ /// Minimum distance between tendril spawns
+ var/tendril_exclusion_radius = 12
/datum/biome/New()
. = ..()
@@ -168,18 +170,21 @@
is_megafauna = TRUE
var/can_spawn = TRUE
- if(ispath(picked_mob, /obj/structure/spawner/lavaland))
- // Prevents tendrils spawning in each other's collapse range
+ if(ispath(picked_mob, /mob/living/basic/mining/tendril))
for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_TENDRIL])
- if (get_dist(spawn_turf, target_turf) <= 2)
+ if (get_dist(spawn_turf, target_turf) <= tendril_exclusion_radius)
can_spawn = FALSE
break
+ if (!can_spawn)
+ continue
+
// Also avoid spawning them next to megafauna
for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA])
if (get_dist(spawn_turf, target_turf) <= megafauna_exclusion_radius)
can_spawn = FALSE
break
+
else if (is_megafauna)
// Megafauna can spawn wherever it wants as long as its not next to another mega
for(var/turf/spawn_turf as anything in spawn_data[CAVE_SPAWN_MEGAFAUNA])
@@ -195,12 +200,11 @@
if (!can_spawn)
continue
- if (ispath(picked_mob, /obj/structure/spawner/lavaland))
+ if (ispath(picked_mob, /mob/living/basic/mining/tendril))
spawn_data[CAVE_SPAWN_TENDRIL] += target_turf
- else
- if (is_megafauna)
- spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf
- spawn_data[CAVE_SPAWN_MOB] += target_turf
+ else if (is_megafauna)
+ spawn_data[CAVE_SPAWN_MEGAFAUNA] += target_turf
+ spawn_data[CAVE_SPAWN_MOB] += target_turf
new picked_mob(target_turf)
diff --git a/code/datums/mapgen/biomes/lavaland.dm b/code/datums/mapgen/biomes/lavaland.dm
index 7461ef33d8a5..88c907dc23fe 100644
--- a/code/datums/mapgen/biomes/lavaland.dm
+++ b/code/datums/mapgen/biomes/lavaland.dm
@@ -1,6 +1,7 @@
/datum/biome/lavaland
open_turf_type = /turf/open/misc/asteroid/basalt/lava_land_surface
closed_turf_type = /turf/closed/mineral/volcanic
+ mob_exclusion_radius = 7
/datum/biome/lavaland/basalt
closed_turf_type = /turf/closed/mineral/random/volcanic
@@ -19,9 +20,7 @@
/mob/living/basic/mining/lobstrosity/lava = 20,
/obj/effect/spawner/random/lavaland_mob/raptor = 15,
/mob/living/basic/mining/goldgrub = 15,
- /obj/structure/spawner/lavaland = 1,
- /obj/structure/spawner/lavaland/goliath = 3,
- /obj/structure/spawner/lavaland/legion = 3,
+ /mob/living/basic/mining/tendril = 7,
)
flora_types = list(
@@ -58,9 +57,7 @@
/mob/living/basic/mining/lobstrosity/lava = 40,
/obj/effect/spawner/random/lavaland_mob/raptor = 15,
/mob/living/basic/mining/goldgrub = 35,
- /obj/structure/spawner/lavaland = 2,
- /obj/structure/spawner/lavaland/goliath = 3,
- /obj/structure/spawner/lavaland/legion = 3,
+ /mob/living/basic/mining/tendril = 8,
)
flora_types = list(
@@ -96,9 +93,7 @@
/mob/living/basic/mining/lobstrosity/lava = 25,
/obj/effect/spawner/random/lavaland_mob/raptor = 20,
/mob/living/basic/mining/goldgrub = 15,
- /obj/structure/spawner/lavaland = 2,
- /obj/structure/spawner/lavaland/goliath = 1,
- /obj/structure/spawner/lavaland/legion = 3,
+ /mob/living/basic/mining/tendril = 6,
)
flora_types = list(
diff --git a/code/datums/materials/requirements/_requirement.dm b/code/datums/materials/requirements/_requirement.dm
index d23e11c3fb98..3d953098927e 100644
--- a/code/datums/materials/requirements/_requirement.dm
+++ b/code/datums/materials/requirements/_requirement.dm
@@ -3,6 +3,8 @@
abstract_type = /datum/material_requirement
/// Flags which materials need to have to pass
var/required_flags = NONE
+ /// Flags which materials need to not have in order to pass
+ var/blacklisted_flags = NONE
/// Minimum property values that materials need to have to pass
var/list/property_minimums = null
/// Maximum property values that materials need to have to pass
@@ -13,6 +15,8 @@
var/list/flag_strings = list()
for (var/flag in bitfield_to_list(required_flags))
flag_strings += GLOB.material_flags_to_string[flag]
+ for (var/flag in bitfield_to_list(blacklisted_flags))
+ flag_strings += "not [GLOB.material_flags_to_string[flag]]"
if (!length(property_minimums) && !length(property_maximums))
return "[capitalize(english_list(flag_strings, and_text = " or "))] material"
@@ -29,37 +33,28 @@
return "[capitalize(english_list(flag_strings, and_text = " or "))] material with [english_list(prop_reqs)]"
/datum/material_requirement/proc/valid_material(datum/material/material)
- if (required_flags > 0 && !(material.mat_flags & required_flags))
+ if (required_flags && !(material.mat_flags & required_flags))
return FALSE
- if (required_flags < 0 && (material.mat_flags & (-required_flags)))
+ if (blacklisted_flags && (material.mat_flags & blacklisted_flags))
return FALSE
for (var/prop_id, min_val in property_minimums)
if (material.get_property(prop_id) < min_val)
return FALSE
+
for (var/prop_id, max_val in property_maximums)
if (material.get_property(prop_id) > max_val)
return FALSE
+
return TRUE
// Actual requirement datums
/datum/material_requirement/armor_material
required_flags = ITEM_MATERIAL_CLASSES
- property_minimums = list(
- MATERIAL_HARDNESS = 2,
- )
- property_maximums = list(
- MATERIAL_FLEXIBILITY = 5,
- )
/datum/material_requirement/solid_material
- property_minimums = list(
- MATERIAL_HARDNESS = 2,
- )
- property_maximums = list(
- MATERIAL_FLEXIBILITY = 5,
- )
+ blacklisted_flags = MATERIAL_CLASS_AMORPHOUS
/datum/material_requirement/rigid_material
required_flags = MATERIAL_CLASS_RIGID
diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm
index 2cb0306ebb99..6422ee8c3d19 100644
--- a/code/datums/mind/antag.dm
+++ b/code/datums/mind/antag.dm
@@ -185,6 +185,7 @@
var/datum/antagonist/nukeop/converter = creator.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE)
var/datum/antagonist/nukeop/N = new()
N.send_to_spawnpoint = FALSE
+ N.give_bonus_tc = FALSE
N.nukeop_outfit = null
add_antag_datum(N,converter.nuke_team)
diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm
index acaddb335388..8ce75e746c58 100644
--- a/code/datums/mutations/touch.dm
+++ b/code/datums/mutations/touch.dm
@@ -260,7 +260,7 @@
var/healing_budget = 35 * heal_multiplier
// Transfer damage from each limb to the mendicant's counterpart.
for(var/obj/item/bodypart/affected_limb as anything in hurtguy.get_bodyparts())
- if(!(affected_limb & BODYTYPE_ORGANIC))
+ if(!(affected_limb.bodytype & BODYTYPE_ORGANIC))
continue
var/obj/item/bodypart/mendicant_transfer_limb = mendicant.get_bodypart(affected_limb.body_zone)
var/list/mendicant_organic_limbs = list()
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 31ea87918487..57dffa377e5f 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -322,6 +322,10 @@
return
return ..()
+/obj/machinery/door/allowed(mob/accessor)
+ return ..() || emergency
+
+/// A mob is trying to open or close the door
/obj/machinery/door/proc/try_to_activate_door(mob/user, access_bypass = FALSE, bumped = FALSE)
add_fingerprint(user)
if(operating || (obj_flags & EMAGGED))
@@ -333,24 +337,8 @@
if(elevator_mode && elevator_status != LIFT_PLATFORM_UNLOCKED)
return
- var/access_check = access_bypass
- if(emergency)
- access_check = TRUE
- else if(unrestricted_side(user) && !delayed_unres_open)
- access_check = TRUE
- else if(!requiresID())
- access_check = TRUE
- else if(allowed(user)) // You
- access_check = TRUE
- else for(var/mob/living/human_backpack in user.buckled_mobs)
- if(allowed(human_backpack)) // Your partner in crime
- access_check = TRUE
- break
-
- if(!access_check && unrestricted_side(user) && attempt_delayed_unres_open(user))
- access_check = TRUE
-
- if(access_check)
+ // note: if the ID wire is cut no ID cards are checked at all! (This is intentional!)
+ if(access_bypass || (requiresID() && user_can_activate_door(user)))
if(density)
open()
else
@@ -360,6 +348,18 @@
else if(!operating && density)
run_animation(DOOR_DENY_ANIMATION)
+/// Used in try_to_activate_door
+/obj/machinery/door/proc/user_can_activate_door(mob/user)
+ PRIVATE_PROC(TRUE)
+ if(allowed(user))
+ return TRUE
+ for(var/mob/living/human_backpack in user.buckled_mobs)
+ if(allowed(human_backpack))
+ return TRUE
+ if(unrestricted_side(user))
+ return !delayed_unres_open || attempt_delayed_unres_open(user)
+ return FALSE
+
/// Allows for specific side of airlocks to be unrestricted (IE, can exit maint freely, but need access to enter)
/obj/machinery/door/proc/unrestricted_side(mob/opener)
return get_dir(src, opener) & unres_sides
diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm
index 4e7ca61d0dd0..91e858273ec2 100644
--- a/code/game/objects/effects/decals/cleanable.dm
+++ b/code/game/objects/effects/decals/cleanable.dm
@@ -82,12 +82,16 @@
RETURN_TYPE(/datum/reagents)
if (reagents)
return reagents
+ return init_reagents(decal_reagent, reagent_amount)
- if (!decal_reagent)
+/obj/effect/decal/cleanable/proc/init_reagents(reagent = null, amount = null)
+ RETURN_TYPE(/datum/reagents)
+
+ if (!reagent)
return
- create_reagents(reagent_amount)
- reagents.add_reagent(decal_reagent, reagent_amount)
+ create_reagents(amount)
+ reagents.add_reagent(reagent, amount)
return reagents
/obj/effect/decal/cleanable/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
diff --git a/code/game/objects/effects/decals/cleanable/blood.dm b/code/game/objects/effects/decals/cleanable/blood.dm
index bf53d00021a2..82253bfabf1a 100644
--- a/code/game/objects/effects/decals/cleanable/blood.dm
+++ b/code/game/objects/effects/decals/cleanable/blood.dm
@@ -542,13 +542,7 @@
/obj/effect/decal/cleanable/blood/gibs/lazy_init_reagents()
if (reagents)
return reagents
-
- if (!decal_reagent)
- return
-
- create_reagents(reagent_amount)
- reagents.add_reagent(decal_reagent, reagent_amount)
- return reagents
+ return init_reagents(decal_reagent, reagent_amount)
/obj/effect/decal/cleanable/blood/gibs/update_overlays()
. = ..()
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index 77ae51ea07ca..db968042c6d9 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -264,36 +264,64 @@ INITIALIZE_IMMEDIATE(/obj/effect/landmark)
GLOB.wizardstart += loc
return INITIALIZE_HINT_QDEL
-/obj/effect/landmark/start/nukeop
+/// Places to put midround nukeops (normal)
+GLOBAL_LIST_EMPTY(nukeop_base_start)
+/// Places to put midround nukeops (leader)
+GLOBAL_LIST_EMPTY(nukeop_base_leader_start)
+/// Places to put nukeops overwatch agents
+GLOBAL_LIST_EMPTY(nukeop_base_overwatch_start)
+/// Places to spawn nukeops on roundstart
+GLOBAL_LIST_EMPTY(nukeop_elevator_start)
+
+/obj/effect/landmark/start/nukeop_base
name = "nukeop"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_spawn"
-/obj/effect/landmark/start/nukeop/Initialize(mapload)
+/obj/effect/landmark/start/nukeop_base/Initialize(mapload)
..()
- GLOB.nukeop_start += loc
+ var/list/add_to = get_global_list()
+ add_to += loc
return INITIALIZE_HINT_QDEL
-/obj/effect/landmark/start/nukeop_leader
+/obj/effect/landmark/start/nukeop_base/proc/get_global_list()
+ return GLOB.nukeop_base_start
+
+/obj/effect/landmark/start/nukeop_base/leader
name = "nukeop leader"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_leader_spawn"
-/obj/effect/landmark/start/nukeop_leader/Initialize(mapload)
- ..()
- GLOB.nukeop_leader_start += loc
- return INITIALIZE_HINT_QDEL
+/obj/effect/landmark/start/nukeop_base/leader/get_global_list()
+ return GLOB.nukeop_base_leader_start
-/obj/effect/landmark/start/nukeop_overwatch
+/obj/effect/landmark/start/nukeop_base/overwatch
name = "nukeop overwatch"
icon = 'icons/effects/landmarks_static.dmi'
icon_state = "snukeop_leader_spawn"
-/obj/effect/landmark/start/nukeop_overwatch/Initialize(mapload)
- ..()
- GLOB.nukeop_overwatch_start += loc
+/obj/effect/landmark/start/nukeop_base/overwatch/get_global_list()
+ return GLOB.nukeop_base_overwatch_start
+
+/obj/effect/landmark/start/nukeop_elevator
+ name = "nukeop (elevator)"
+ icon = /obj/effect/landmark/start/nukeop_base::icon
+ icon_state = /obj/effect/landmark/start/nukeop_base::icon_state
+
+/obj/effect/landmark/start/nukeop_elevator/Initialize(mapload)
+ . = ..()
+ GLOB.nukeop_elevator_start += loc
return INITIALIZE_HINT_QDEL
+/obj/effect/landmark/nukeop_elevator
+ icon_state = "x"
+
+/obj/effect/landmark/nukeop_elevator/interior
+ name = "nukeop elevator interior top right corner"
+
+/obj/effect/landmark/nukeop_elevator/exterior
+ name = "nukeop elevator exterior top right corner"
+
// Must be immediate because players will
// join before SSatom initializes everything.
INITIALIZE_IMMEDIATE(/obj/effect/landmark/start/new_player)
diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm
index d5146c0a54ac..307748172db5 100644
--- a/code/game/objects/items/devices/aicard_evil.dm
+++ b/code/game/objects/items/devices/aicard_evil.dm
@@ -76,6 +76,7 @@
// create and apply syndie datum
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
+ nuke_datum.give_bonus_tc = FALSE
new_ai.mind.add_antag_datum(nuke_datum, op_datum.nuke_team)
LAZYADD(new_ai.mind.special_roles, "Syndicate AI")
new_ai.add_faction(ROLE_SYNDICATE)
diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm
index 6d0ddc9e0802..aff0ccc4e800 100644
--- a/code/game/objects/items/storage/uplink_kits.dm
+++ b/code/game/objects/items/storage/uplink_kits.dm
@@ -803,6 +803,7 @@
var/datum/antagonist/nukeop/nuke_datum = new()
nuke_datum.send_to_spawnpoint = FALSE
+ nuke_datum.give_bonus_tc = FALSE
nuke_datum.nukeop_outfit = null
human_target.mind?.add_antag_datum(nuke_datum)
human_target.add_faction(ROLE_SYNDICATE)
diff --git a/code/game/objects/items/weaponry/melee/baton.dm b/code/game/objects/items/weaponry/melee/baton.dm
index 3c5f0a4991ac..a4ceccaa4ebb 100644
--- a/code/game/objects/items/weaponry/melee/baton.dm
+++ b/code/game/objects/items/weaponry/melee/baton.dm
@@ -1009,22 +1009,19 @@
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The Secure Apprehension Device (sometimes referred to as the SAD in the officer training manuals) is \
- the unholy union of a mace and a cattleprod. This nonlethal device was designed to put a stop to ruffians, \
- scoundrels, ne'er-do-wells and criminals wherever they may rear their ugly heads.
\
+ an unholy union of mace and cattleprod. Designed to stop criminals in their tracks, Nanotrasen security members \
+ are rarely without their trusty stun baton. Assuming they haven't lost it somewhere.
\
\
- A symbol of Nanotrasen security forces, the stun baton is the primary tool officers employ against the \
- unlawful scum and villainy of the Spinward and abroad. Trained to 'baton first, interrogate later', \
- Nanotrasen security has long since earned itself a mixed reputation. Able to rapidly shut down the \
- central nervous system of a criminal with only a few direct applications of the conductive striking head \
- of the device, few would-be troublemakers want to find themselves on the wrong end of an officer brandishing \
- this baton.
\
+ Trained to 'baton first, interrogate later', Nanotrasen security has long since earned itself a mixed reputation. \
+ The device is able to rapidly shut down the central nervous system of a criminal with only a few direct applications \
+ of the conductive striking head.
\
\
TerraGov law enforcement has avoided the adoption of stun batons due to various ethical dilemmas posed by \
- their usage, largely because of the longterm physical and mental ramifications of being struck by a human cattleprod. \
- Citizens' rights advocacy groups protest against the proliferation of stun batons as a policing tool, \
- arguing that they are 'inhumane' and 'authoritarian'. Nanotrasen, on the other hand, has had no such qualms \
- when deploying stun batons as a compliance measure across all of their existing stations and facilities against \
- unruly members of staff." \
+ their utilization. Studies of their usage have shown numerous longterm physical and mental ramifications caused by \
+ being struck by a human cattleprod. Citizens' rights advocacy groups protest against the proliferation of stun \
+ batons as a policing tool, arguing that they are 'inhumane' and 'authoritarian'. Nanotrasen, on the other hand, \
+ has had no such qualms when deploying stun batons as a compliance measure across all of their existing stations \
+ and facilities against their own unruly members of staff." \
)
// Contractor Baton
@@ -1033,25 +1030,14 @@
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The Contract Acquisition Device (sometimes referred to as the CAD in encrypted correspondence) is \
- one of the more frequently encountered examples of Cybersun Industries weaponry. Extremely similar to Nanotrasen's \
- own Secure Apprehension Device (also simply known as the stun baton), the contractor baton is able to induce \
- CNS disruption in a target to render them helpless. It is also capable of devastating blunt force trauma if \
- used as a bludgeon. The contractor baton is also capable of telescopic deployment, allowing for discretion while \
- making an approach towards a target.
\
+ the most frequently encountered example of Cybersun Industries weaponry. Similar in purpose to Nanotrasen's \
+ own Secure Apprehension Device, the baton is capable of inducing rapid CNS disruption in a target to render them \
+ helpless. It also makes for an effective bludgeon, another quality it shares with the stun baton. To maximize \
+ ease of concealment, the baton is also able to be telescopically collapsed, to then be rapidly deployed at the \
+ pull of a trigger.
\
\
- The contractor baton is famously associated with contractors, elite Cybersun field agents. While the standard \
- agent would often be tasked with sabotage, terrorism, murder or theft, contractors have the critical task of \
- kidnapping high value personnel. Anyone with the potential to possess classified or sensitive data about Nanotrasen \
- security systems and devices could be a target for Cybersun.
\
-
\
- Extracting this information is most easily performed on living subjects. As such, the contractor baton was designed \
- with nonlethal incapacitation in mind. However, Cybersun Industries has long since found workarounds for extracting \
- data from the recently deceased, should the contractor find themselves with only a corpse left to send back. Death \
- may not spare you from the machinations of Cybersun Industries if they deem you a valuable asset towards their goals.
\
-
\
- Nanotrasen utilizes a number of countermeasures to contractor insurgencies, such as employing selective memory wiping \
- or falsified memory injection, the establishment of 'dummy' command staff through the artificial acceleration \
- of otherwise incompetent but useful crewmembers (whose incompetence will often result in an acceptable degree \
- of operational disruption), which provides convenient scapegoats in the event of a security breach as well as \
- frequent staff turnover and reassignment." \
+ The contractor baton is famously associated with contractors, elite Cybersun field agents sent to kidnap and extract \
+ high value enemy personnel for interrogation. Anyone with the potential to possess classified or sensitive data about \
+ Nanotrasen could find themselves a target for Cybersun. The company relentlessly employs contractors to probe Nanotrasen \
+ for vulnerabilities, starting with their employees." \
)
diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm
deleted file mode 100644
index e6ca59016ed5..000000000000
--- a/code/game/objects/structures/lavaland/necropolis_tendril.dm
+++ /dev/null
@@ -1,141 +0,0 @@
-//Necropolis Tendrils, which spawn lavaland monsters and break into a chasm when killed
-/obj/structure/spawner/lavaland
- name = "necropolis tendril"
- desc = "A vile tendril of corruption, originating deep underground. Terrible monsters are pouring out of it."
-
- icon = 'icons/mob/simple/lavaland/nest.dmi'
- icon_state = "tendril"
-
- faction = list(FACTION_MINING, FACTION_ASHWALKER)
- max_mobs = 3
- max_integrity = 250
- mob_types = list(/mob/living/basic/mining/watcher)
-
- move_resist=INFINITY // just killing it tears a massive hole in the ground, let's not move it
- anchored = TRUE
- resistance_flags = FIRE_PROOF | LAVA_PROOF
- var/obj/effect/light_emitter/tendril/emitted_light
- scanner_taggable = TRUE
- mob_gps_id = "WT"
- spawner_gps_id = "Necropolis Tendril"
-
-/obj/structure/spawner/lavaland/goliath
- mob_types = list(/mob/living/basic/mining/goliath)
- mob_gps_id = "GL"
-
-/obj/structure/spawner/lavaland/legion
- mob_types = list(/mob/living/basic/mining/legion/spawner_made)
- mob_gps_id = "LG"
-
-/obj/structure/spawner/lavaland/icewatcher
- mob_types = list(/mob/living/basic/mining/watcher/icewing)
- mob_gps_id = "WT|I" // icewing
-
-GLOBAL_LIST_INIT(tendrils, list())
-/obj/structure/spawner/lavaland/Initialize(mapload)
- . = ..()
- emitted_light = new(loc)
- for(var/F in RANGE_TURFS(1, src))
- if(ismineralturf(F))
- var/turf/closed/mineral/M = F
- M.ScrapeAway(null, CHANGETURF_IGNORE_AIR)
- AddComponent(/datum/component/gps, "Eerie Signal")
- GLOB.tendrils += src
-
-/obj/structure/spawner/lavaland/atom_deconstruct(disassembled)
- new /obj/effect/collapse(loc)
-
-/obj/structure/spawner/lavaland/examine(mob/user)
- var/list/examine_messages = ..()
- examine_messages += span_notice("Once this thing gets hurts enough, it triggers a violent final retaliation.")
- examine_messages += span_notice("You'll only have a few moments to run up, grab some loot with an open hand, and get out with it.")
- return examine_messages
-
-/obj/structure/spawner/lavaland/Destroy()
- var/last_tendril = TRUE
- if(GLOB.tendrils.len>1)
- last_tendril = FALSE
-
- if(last_tendril && !(flags_1 & ADMIN_SPAWNED_1))
- if(SSachievements.achievements_enabled)
- for(var/mob/living/L in view(7,src))
- if(L.stat || !L.client)
- continue
- L.client.give_award(/datum/award/achievement/boss/tendril_exterminator, L)
- L.client.give_award(/datum/award/score/tendril_score, L) //Progresses score by one
- GLOB.tendrils -= src
- QDEL_NULL(emitted_light)
- return ..()
-
-/obj/effect/light_emitter/tendril
- set_luminosity = 4
- set_cap = 2.5
- light_color = LIGHT_COLOR_LAVA
-
-/obj/effect/collapse
- name = "collapsing necropolis tendril"
- desc = "Get your loot and get clear!"
- layer = TABLE_LAYER
- icon = 'icons/mob/simple/lavaland/nest.dmi'
- icon_state = "tendril"
- anchored = TRUE
- density = TRUE
- /// weakref list of which mobs have gotten their loot from this effect.
- var/list/collected = list()
- /// a bit of light as to make less unfair deaths from the chasm
- var/obj/effect/light_emitter/tendril/emitted_light
-
-/obj/effect/collapse/Initialize(mapload)
- . = ..()
- emitted_light = new(loc)
- visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!"))
- balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7)
- playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE)
- addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS)
-
-/obj/effect/collapse/examine(mob/user)
- var/list/examine_messages = ..()
- if(isliving(user))
- if(has_collected(user))
- examine_messages += span_boldnotice("You've grabbed what you can, now get out!")
- else
- examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!")
- return examine_messages
-
-/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers)
- . = ..()
- if(has_collected(collector))
- to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!"))
- return
- visible_message(span_warning("Something falls free of the tendril!"))
- var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc)
- collector.start_pulling(loot)
- collected += WEAKREF(collector)
-
-/obj/effect/collapse/Destroy()
- collected.Cut()
- QDEL_NULL(emitted_light)
- return ..()
-
-///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean.
-/obj/effect/collapse/proc/has_collected(mob/collector)
- for(var/datum/weakref/weakref as anything in collected)
- var/mob/living/resolved = weakref.resolve()
- //it could have been collector, it could not have been, we don't care
- if(!resolved)
- continue
- if(resolved == collector)
- return TRUE
- return FALSE
-
-/obj/effect/collapse/proc/collapse()
- for(var/mob/M in range(7,src))
- shake_camera(M, 15, 1)
- playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE)
- visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!"))
- for(var/turf/T in RANGE_TURFS(2,src))
- if(HAS_TRAIT(T, TRAIT_NO_TERRAFORM))
- continue
- if(!T.density)
- T.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR)
- qdel(src)
diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm
index e8059c7d0eac..53febcc363f0 100644
--- a/code/modules/antagonists/_common/antag_spawner.dm
+++ b/code/modules/antagonists/_common/antag_spawner.dm
@@ -185,8 +185,8 @@
/obj/item/antag_spawner/nuke_ops/overwatch/Initialize(mapload)
. = ..()
- if(length(GLOB.nukeop_overwatch_start)) //Otherwise, it will default to the datum's spawn point anyways
- spawn_location = pick(GLOB.nukeop_overwatch_start)
+ if(length(GLOB.nukeop_base_overwatch_start)) //Otherwise, it will default to the datum's spawn point anyways
+ spawn_location = pick(GLOB.nukeop_base_overwatch_start)
//////CLOWN OP
/obj/item/antag_spawner/nuke_ops/clown
diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm
index 20c4b7e220de..3dc6e81a6ef0 100644
--- a/code/modules/antagonists/changeling/powers/mutations.dm
+++ b/code/modules/antagonists/changeling/powers/mutations.dm
@@ -478,7 +478,7 @@
return BULLET_ACT_HIT
/obj/projectile/tentacle/Destroy()
- qdel(chain)
+ QDEL_NULL(chain)
source = null
return ..()
diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm
index dccd7450f978..ae3d3c9d777f 100644
--- a/code/modules/antagonists/nukeop/datums/operative.dm
+++ b/code/modules/antagonists/nukeop/datums/operative.dm
@@ -15,6 +15,8 @@
var/datum/team/nuclear/nuke_team
/// Should the user be moved to default spawnpoint after being granted this datum.
var/send_to_spawnpoint = TRUE
+ /// Should the nukie get a little bonus tc depending on how many players there are
+ var/give_bonus_tc = TRUE
var/job_type = /datum/job/nuclear_operative
/// The DEFAULT outfit we will give to players granted this datum
@@ -49,11 +51,11 @@
equip_op()
if(send_to_spawnpoint)
move_to_spawnpoint()
- // grant extra TC for the people who start in the nukie base ie. not the lone op
- var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5)
+ if(give_bonus_tc)
+ var/extra_tc = CEILING(GLOB.joined_player_list.len / 5, 5)
var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
- if (uplink)
- uplink.uplink_handler.add_telecrystals(extra_tc)
+ uplink?.uplink_handler.add_telecrystals(extra_tc)
+
var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
if(uplink)
var/datum/team/nuclear/nuke_team = get_team()
@@ -81,7 +83,7 @@
objectives |= nuke_team.objectives
/datum/antagonist/nukeop/leader/get_spawnpoint()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team)
if(!new_team)
@@ -146,7 +148,7 @@
return TRUE
/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin)
- owner.current.forceMove(pick(GLOB.nukeop_start))
+ owner.current.forceMove(pick(GLOB.nukeop_base_start))
/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin)
var/code
@@ -192,7 +194,7 @@
if(nuke_team)
team_number = nuke_team.members.Find(owner)
- return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1]
+ return GLOB.nukeop_base_start[((team_number - 1) % GLOB.nukeop_base_start.len) + 1]
/datum/antagonist/nukeop/proc/spawn_infiltrator()
var/datum/map_template/shuttle/infiltrator/ship = SSmapping.shuttle_templates[infiltrator_id]
@@ -221,6 +223,6 @@
mobile_port.setTimer(mobile_port.ignitionTime)
/datum/antagonist/nukeop/on_respawn(mob/new_character)
- new_character.forceMove(pick(GLOB.nukeop_start))
+ new_character.forceMove(pick(GLOB.nukeop_base_start))
equip_op()
return TRUE
diff --git a/code/modules/antagonists/nukeop/datums/operative_leader.dm b/code/modules/antagonists/nukeop/datums/operative_leader.dm
index 7815c8c8631c..3f3aaaf31852 100644
--- a/code/modules/antagonists/nukeop/datums/operative_leader.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_leader.dm
@@ -1,6 +1,8 @@
/datum/antagonist/nukeop/leader
name = "Nuclear Operative Leader"
nukeop_outfit = /datum/outfit/syndicate/leader
+ /// Whether to spawn the infiltrator
+ var/spawn_ship = TRUE
/// Randomly chosen honorific, for distinction
var/title
/// The nuclear challenge remote we will spawn this player with.
@@ -63,7 +65,8 @@
return capitalize(newname)
/datum/antagonist/nukeop/leader/create_team(datum/team/nuclear/new_team)
- spawn_infiltrator()
+ if(spawn_ship)
+ spawn_infiltrator()
if(new_team)
return ..()
// Leaders always make new teams
diff --git a/code/modules/antagonists/nukeop/datums/operative_lone.dm b/code/modules/antagonists/nukeop/datums/operative_lone.dm
index b074016890aa..ba6d001ff807 100644
--- a/code/modules/antagonists/nukeop/datums/operative_lone.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_lone.dm
@@ -1,6 +1,7 @@
/datum/antagonist/nukeop/lone
name = "Lone Operative"
send_to_spawnpoint = FALSE //Handled by event
+ give_bonus_tc = FALSE
nukeop_outfit = /datum/outfit/syndicate/full/loneop
preview_outfit = /datum/outfit/nuclear_operative
preview_outfit_behind = null
diff --git a/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
index eb386342c09a..9cbad2572f87 100644
--- a/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
@@ -2,6 +2,7 @@
name = "Nuclear Operative Reinforcement"
show_in_antagpanel = FALSE
send_to_spawnpoint = FALSE
+ give_bonus_tc = FALSE
nukeop_outfit = /datum/outfit/syndicate/reinforcement
/datum/antagonist/nukeop/reinforcement/cyborg
diff --git a/code/modules/antagonists/nukeop/datums/operative_support.dm b/code/modules/antagonists/nukeop/datums/operative_support.dm
index 1b1ea923903d..b10bb4a369c0 100644
--- a/code/modules/antagonists/nukeop/datums/operative_support.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_support.dm
@@ -32,7 +32,7 @@
owner.current.grant_language(/datum/language/codespeak)
/datum/antagonist/nukeop/support/get_spawnpoint()
- return pick(GLOB.nukeop_overwatch_start)
+ return pick(GLOB.nukeop_base_overwatch_start)
/datum/antagonist/nukeop/support/forge_objectives()
var/datum/objective/overwatch/objective = new
diff --git a/code/modules/antagonists/nukeop/datums/operative_team.dm b/code/modules/antagonists/nukeop/datums/operative_team.dm
index 4eab9341d48c..98ffe695e47c 100644
--- a/code/modules/antagonists/nukeop/datums/operative_team.dm
+++ b/code/modules/antagonists/nukeop/datums/operative_team.dm
@@ -18,7 +18,7 @@
objectives += maingoal
// when a nuke team is created, the infiltrator has not loaded in yet - it takes some time. so no nuke, we have to wait
- addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed)), 4 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed)), 5 SECONDS)
/datum/team/nuclear/roundend_report()
var/list/parts = list()
@@ -192,7 +192,7 @@
infil_or_nukebase = SPAWN_AT_BASE
if(infil_or_nukebase == SPAWN_AT_BASE)
- spawn_loc = pick(GLOB.nukeop_start)
+ spawn_loc = pick(GLOB.nukeop_base_start)
var/mob/living/carbon/human/nukie = new(spawn_loc)
chosen_one.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE)
@@ -200,6 +200,7 @@
var/datum/antagonist/nukeop/antag_datum = new()
antag_datum.send_to_spawnpoint = FALSE
+ antag_datum.give_bonus_tc = FALSE
antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement
nukie.mind.add_antag_datum(antag_datum, src)
@@ -313,20 +314,25 @@
..()
SEND_SIGNAL(src, COMSIG_NUKE_TEAM_ADDITION, new_member.current)
-/datum/team/nuclear/proc/assign_nuke_delayed()
- assign_nuke()
- if(tracked_nuke && memorized_code)
- for(var/datum/mind/synd_mind in members)
- var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
- synd_datum?.memorize_code()
+/datum/team/nuclear/proc/assign_nuke_delayed(attempts = 0)
+ if(!assign_nuke())
+ if(attempts > 5)
+ stack_trace("Failed to assign nuke to team after multiple attempts. \
+ This likely means the nuke was not found. Please investigate.")
+ else
+ addtimer(CALLBACK(src, PROC_REF(assign_nuke_delayed), attempts + 1), 5 SECONDS)
+ return
+
+ for(var/datum/mind/synd_mind as anything in members)
+ var/datum/antagonist/nukeop/synd_datum = synd_mind.has_antag_datum(/datum/antagonist/nukeop)
+ synd_datum?.memorize_code()
/datum/team/nuclear/proc/assign_nuke()
- memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
if(!nuke)
- stack_trace("Syndicate nuke not found during nuke team creation.")
- memorized_code = null
- return
+ return FALSE
+
+ memorized_code = random_nukecode()
tracked_nuke = nuke
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = memorized_code
@@ -334,6 +340,7 @@
memorized_code = nuke.r_code
for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
beernuke.r_code = memorized_code
+ return TRUE
#undef SPAWN_AT_BASE
#undef SPAWN_AT_INFILTRATOR
@@ -341,14 +348,14 @@
/datum/team/nuclear/loneop
/datum/team/nuclear/loneop/assign_nuke()
- memorized_code = random_nukecode()
var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
- if(nuke)
- tracked_nuke = nuke
- if(nuke.r_code == NUKE_CODE_UNSET)
- nuke.r_code = memorized_code
- else //Already set by admins/something else?
- memorized_code = nuke.r_code
- else
- stack_trace("Station self-destruct not found during lone op team creation.")
- memorized_code = null
+ if(!nuke)
+ return FALSE
+
+ memorized_code = random_nukecode()
+ tracked_nuke = nuke
+ if(nuke.r_code == NUKE_CODE_UNSET)
+ nuke.r_code = memorized_code
+ else //Already set by admins/something else?
+ memorized_code = nuke.r_code
+ return TRUE
diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm
index 2f5249158204..a38ffbadce84 100644
--- a/code/modules/clothing/suits/reactive_armour.dm
+++ b/code/modules/clothing/suits/reactive_armour.dm
@@ -534,7 +534,7 @@
shock_turf_windup(owner.loc)
/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf_windup(turf/target)
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target)
+ new /obj/effect/temp_visual/telegraphing/circle(target)
addtimer(CALLBACK(src, PROC_REF(shock_turf), target), 1 SECONDS)
/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf(turf/target)
diff --git a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
index f2e38bf27d74..ac4b7be3435b 100644
--- a/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
+++ b/code/modules/jobs/job_types/antagonists/nuclear_operative.dm
@@ -2,26 +2,20 @@
title = ROLE_OPERATIVE
/datum/job/nuclear_operative/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_start)
+ return pick(GLOB.nukeop_elevator_start)
/datum/job/nuclear_operative/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_start)
+ return pick(GLOB.nukeop_base_start)
/datum/job/nuclear_operative/leader
-/datum/job/nuclear_operative/leader/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_leader_start)
-
/datum/job/nuclear_operative/leader/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
/datum/job/nuclear_operative/clown_operative
title = ROLE_CLOWN_OPERATIVE
/datum/job/nuclear_operative/clown_operative/leader
-/datum/job/nuclear_operative/clown_operative/leader/get_roundstart_spawn_point()
- return pick(GLOB.nukeop_leader_start)
-
/datum/job/nuclear_operative/clown_operative/leader/get_latejoin_spawn_point()
- return pick(GLOB.nukeop_leader_start)
+ return pick(GLOB.nukeop_base_leader_start)
diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
index ce57035a7661..b18e740f8cb4 100644
--- a/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
+++ b/code/modules/mapfluff/ruins/objects_and_mobs/ash_walker_den.dm
@@ -123,4 +123,77 @@
resistance_flags = FIRE_PROOF | LAVA_PROOF
max_integrity = 200
+/obj/effect/light_emitter/tendril
+ set_luminosity = 4
+ set_cap = 2.5
+ light_color = LIGHT_COLOR_LAVA
+
+/obj/effect/collapse
+ name = "collapsing necropolis tendril"
+ desc = "Get your loot and get clear!"
+ layer = TABLE_LAYER
+ icon = 'icons/mob/simple/lavaland/nest.dmi'
+ icon_state = "tendril"
+ anchored = TRUE
+ density = TRUE
+ /// weakref list of which mobs have gotten their loot from this effect.
+ var/list/collected = list()
+ /// a bit of light as to make less unfair deaths from the chasm
+ var/obj/effect/light_emitter/tendril/emitted_light
+
+/obj/effect/collapse/Initialize(mapload)
+ . = ..()
+ emitted_light = new(loc)
+ visible_message(span_bolddanger("The tendril writhes in fury as the earth around it begins to crack and break apart! Get back!"))
+ balloon_alert_to_viewers("interact to grab loot before collapse!", vision_distance = 7)
+ playsound(loc,'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(collapse)), 5 SECONDS)
+
+/obj/effect/collapse/examine(mob/user)
+ var/list/examine_messages = ..()
+ if(isliving(user))
+ if(has_collected(user))
+ examine_messages += span_boldnotice("You've grabbed what you can, now get out!")
+ else
+ examine_messages += span_boldnotice("You might have some time to grab some goodies with an open hand before it collapses!")
+ return examine_messages
+
+/obj/effect/collapse/attack_hand(mob/living/collector, list/modifiers)
+ . = ..()
+ if(has_collected(collector))
+ to_chat(collector, span_danger("You've already gotten some loot, just get out of there with it!"))
+ return
+ visible_message(span_warning("Something falls free of the tendril!"))
+ var/obj/structure/closet/crate/necropolis/tendril/loot = new /obj/structure/closet/crate/necropolis/tendril(loc)
+ collector.start_pulling(loot)
+ collected += WEAKREF(collector)
+
+/obj/effect/collapse/Destroy()
+ collected.Cut()
+ QDEL_NULL(emitted_light)
+ return ..()
+
+///Helper proc that resolves weakrefs to determine if collector is in collected list, returning a boolean.
+/obj/effect/collapse/proc/has_collected(mob/collector)
+ for(var/datum/weakref/weakref as anything in collected)
+ var/mob/living/resolved = weakref.resolve()
+ //it could have been collector, it could not have been, we don't care
+ if(!resolved)
+ continue
+ if(resolved == collector)
+ return TRUE
+ return FALSE
+
+/obj/effect/collapse/proc/collapse()
+ for(var/mob/viewer in range(7, src))
+ shake_camera(viewer, 15, 1)
+ playsound(get_turf(src),'sound/effects/explosion/explosionfar.ogg', 200, TRUE)
+ visible_message(span_bolddanger("The tendril falls inward, the ground around it widening into a yawning chasm!"))
+ for(var/turf/ground in RANGE_TURFS(2, src))
+ if(HAS_TRAIT(ground, TRAIT_NO_TERRAFORM))
+ continue
+ if(!ground.density)
+ ground.TerraformTurf(/turf/open/chasm/lavaland, /turf/open/chasm/lavaland, flags = CHANGETURF_INHERIT_AIR)
+ qdel(src)
+
#undef ASH_WALKER_SPAWN_THRESHOLD
diff --git a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
index bb07998534c0..9f72047fcdea 100644
--- a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm
@@ -218,7 +218,7 @@
var/backstabbed = FALSE
var/def_check = target.getarmor(type = BOMB)
// Backstab bonus
- if(check_behind(user, target) || boosted_mark)
+ if(check_behind(user, target) && !HAS_TRAIT(target, TRAIT_BACKSTAB_IMMUNE) || boosted_mark)
backstabbed = TRUE
combined_damage += backstab_bonus
playsound(user, backstab_sound, 100, TRUE) //Seriously who spelled it wrong
diff --git a/code/modules/mining/equipment/monster_organs/rush_gland.dm b/code/modules/mining/equipment/monster_organs/rush_gland.dm
index 69c799741700..f22bf029a8c7 100644
--- a/code/modules/mining/equipment/monster_organs/rush_gland.dm
+++ b/code/modules/mining/equipment/monster_organs/rush_gland.dm
@@ -24,6 +24,7 @@
/obj/item/organ/monster_core/rush_gland/on_mob_insert(mob/living/carbon/organ_owner)
. = ..()
RegisterSignal(organ_owner, COMSIG_GOLIATH_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig))
+ RegisterSignal(organ_owner, COMSIG_TENDRIL_TENTACLED_GRABBED, PROC_REF(trigger_organ_action_on_sig))
/obj/item/organ/monster_core/rush_gland/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
. = ..()
diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
index cb8fa5532718..4ec0dbfe49e3 100644
--- a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
+++ b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm
@@ -95,7 +95,7 @@
playsound(src, 'sound/effects/magic/lightningshock.ogg', 10, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
targeted_turfs += target_turf
balloon_alert(user, "you aim at [target_turf]...")
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target_turf)
+ new /obj/effect/temp_visual/telegraphing/circle(target_turf)
addtimer(CALLBACK(src, PROC_REF(throw_thunderbolt), target_turf, power_boosted), 1.5 SECONDS)
thunder_charges--
addtimer(CALLBACK(src, PROC_REF(recharge)), thunder_charge_time)
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
new file mode 100644
index 000000000000..b31e8a982a73
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm
@@ -0,0 +1,155 @@
+#define HEARTBEAT_NORMAL (1.8 SECONDS)
+#define HEARTBEAT_FAST (1 SECONDS)
+#define HEARTBEAT_FRANTIC (0.4 SECONDS)
+
+GLOBAL_LIST_INIT(tendrils, list())
+
+/mob/living/basic/mining/tendril
+ name = "necropolis tendril"
+ desc = "A vile tendril of corruption, originating deep underground."
+ icon = 'icons/mob/simple/lavaland/tendril.dmi'
+ icon_state = "tendril"
+ icon_living = "tendril"
+ pixel_w = -8
+ base_pixel_w = -8
+ status_flags = NONE
+ mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING
+ basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS
+ mob_size = MOB_SIZE_HUGE
+ maxHealth = 1200
+ health = 1200
+
+ friendly_verb_continuous = "flails at"
+ friendly_verb_simple = "flail at"
+ speak_emote = list("resonates")
+ obj_damage = 100
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ sharpness = SHARP_POINTY
+ wound_bonus = CANT_WOUND
+ attack_sound = 'sound/items/weapons/pierce.ogg'
+ attack_verb_continuous = "pierces through"
+ attack_verb_simple = "pierce through"
+ throw_blocked_message = "does nothing to the thick shell of"
+ move_resist = INFINITY
+
+ light_range = 3
+ light_power = 2
+ light_color = LIGHT_COLOR_LAVA
+
+ ai_controller = /datum/ai_controller/basic_controller/tendril
+
+ /// Looping heartbeat sound
+ var/datum/looping_sound/heartbeat/soundloop
+ /// Melee attack ability to used in retaliation to melee strikes and whenever it manages to grab someone
+ var/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/tendril_melee
+
+/mob/living/basic/mining/tendril/Initialize(mapload)
+ . = ..()
+ GLOB.tendrils += src
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/death_drops, /obj/structure/closet/crate/necropolis/tendril)
+ AddComponent(/datum/component/ai_target_timer)
+ AddComponent(/datum/component/gps, "Eerie Signal")
+ AddComponent(/datum/component/basic_mob_attack_telegraph, display_telegraph_overlay = FALSE, telegraph_duration = 0.4 SECONDS)
+ AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20)
+ add_traits(list(TRAIT_BACKSTAB_IMMUNE, TRAIT_IMMOBILIZED), INNATE_TRAIT)
+
+ var/static/list/abilities = list(
+ /datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash = BB_TENDRIL_LASH,
+ /datum/action/cooldown/mob_cooldown/tendril_chaser = BB_TENDRIL_CHASER,
+ /datum/action/cooldown/mob_cooldown/tendril_cross_spikes = BB_TENDRIL_SPIKES,
+ )
+ grant_actions_by_list(abilities)
+
+ tendril_melee = new(src)
+ tendril_melee.Grant(src)
+ AddComponent(/datum/component/revenge_ability, tendril_melee, targeting = GET_TARGETING_STRATEGY(ai_controller.blackboard[BB_TARGETING_STRATEGY]), max_range = 2, target_self = TRUE)
+
+ soundloop = new(src, start_immediately = FALSE)
+ soundloop.mid_length = HEARTBEAT_NORMAL
+ soundloop.pressure_affected = FALSE
+ soundloop.start()
+ update_appearance(UPDATE_OVERLAYS)
+
+ var/turf/our_turf = get_turf(src)
+ for (var/turf/rock in range(4, src))
+ var/dist = sqrt((rock.x - our_turf.x) ** 2 + (rock.y - our_turf.y) ** 2)
+ if (dist > 4.5)
+ continue
+
+ if (ismineralturf(rock))
+ rock.ScrapeAway(null, CHANGETURF_IGNORE_AIR)
+
+ if (istype(rock, /turf/open/misc/asteroid) && prob(100 / sqrt(max(1, dist))))
+ rock.ChangeTurf(/turf/open/indestructible/necropolis, null, CHANGETURF_IGNORE_AIR)
+
+/mob/living/basic/mining/tendril/Destroy()
+ GLOB.tendrils -= src
+ QDEL_NULL(soundloop)
+
+ if(!SSachievements.achievements_enabled || (flags_1 & ADMIN_SPAWNED_1))
+ return ..()
+
+ for(var/mob/living/killer in view(7, src))
+ if(killer.stat || !killer.client)
+ continue
+ killer.client.give_award(/datum/award/score/tendril_score, killer)
+ if (!length(GLOB.tendrils))
+ killer.client.give_award(/datum/award/achievement/boss/tendril_exterminator, killer)
+
+ return ..()
+
+/mob/living/basic/mining/tendril/update_overlays()
+ . = ..()
+ . += emissive_appearance(icon, "[icon_state]_e", src, effect_type = EMISSIVE_NO_BLOOM)
+ . += emissive_appearance(icon, "[icon_state]_e_bloom", src, effect_type = EMISSIVE_BLOOM)
+
+/mob/living/basic/mining/tendril/Life(seconds_per_tick)
+ . = ..()
+ update_heartbeat() // Just a single math op unless we need updating so its fine to put it here
+
+/mob/living/basic/mining/tendril/updatehealth()
+ . = ..()
+ update_heartbeat()
+
+/mob/living/basic/mining/tendril/proc/update_heartbeat()
+ if (!soundloop)
+ return
+
+ var/beat_rate = HEARTBEAT_NORMAL
+ if (ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ beat_rate = round(HEARTBEAT_FRANTIC + health / maxHealth * (HEARTBEAT_FAST - HEARTBEAT_FRANTIC), 0.05 SECONDS)
+
+ if (beat_rate != soundloop.mid_length)
+ soundloop.set_mid_length(beat_rate)
+
+/mob/living/basic/mining/tendril/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE, item_animation_override = null)
+ if(!no_effect && (visual_effect_icon || used_item))
+ do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = item_animation_override)
+
+ // Stabby visuals
+ var/obj/effect/temp_visual/spike_stab/stab = new(get_turf(src))
+ var/target_dir = get_dir(src, attacked_atom)
+ stab.transform = matrix().Turn(dir2angle(target_dir) + rand(-7, 7))
+ if (target_dir & NORTH)
+ stab.pixel_z = 24
+ else if (target_dir & SOUTH)
+ stab.pixel_z = -24
+ if (target_dir & EAST)
+ stab.pixel_w = 24
+ else if (target_dir & WEST)
+ stab.pixel_w = -24
+
+/obj/effect/temp_visual/spike_stab
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "spike_small"
+ duration = 0.4 SECONDS
+
+/mob/living/basic/mining/tendril/proc/snatch_react()
+ if (tendril_melee.IsAvailable())
+ tendril_melee.Activate(warning = FALSE)
+
+#undef HEARTBEAT_NORMAL
+#undef HEARTBEAT_FAST
+#undef HEARTBEAT_FRANTIC
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
new file mode 100644
index 000000000000..ba4bb3b219d8
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
@@ -0,0 +1,358 @@
+/// Fires out two cross patterns of damaging tentacles which reel in anything they hit, then causes a followup attack
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash
+ name = "Tentacle Lash"
+ desc = "Lash out with your tentacles in 8 directions, reeling in whatever you hit and unleashing a deadly followup attack afterwards."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "goliath_tentacle_wiggle"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 8 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ projectile_type = /obj/projectile/tentacle_lash
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash/Activate(atom/target)
+ disable_cooldown_actions()
+
+ for (var/swipe_dir in GLOB.cardinals)
+ var/turf/open/line_turf = get_step(owner, swipe_dir)
+ for (var/i in 1 to projectile_type::range)
+ if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
+ break
+ var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
+ telegraph.dir = swipe_dir
+ line_turf = get_step(line_turf, swipe_dir)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.cardinals)
+ shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
+
+ SLEEP_CHECK_DEATH(1.6 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.diagonals)
+ var/turf/open/line_turf = get_step(owner, swipe_dir)
+ for (var/i in 1 to projectile_type::range)
+ if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
+ break
+ var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
+ telegraph.dir = swipe_dir
+ line_turf = get_step(line_turf, swipe_dir)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+ for (var/swipe_dir in GLOB.diagonals)
+ shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
+
+ SLEEP_CHECK_DEATH(1.2 SECONDS, owner)
+ StartCooldown()
+ SLEEP_CHECK_DEATH(0.6 SECONDS, owner)
+ enable_cooldown_actions()
+ return TRUE
+
+/obj/projectile/tentacle_lash
+ name = "tentacle spike"
+ icon_state = "tentacle_spike"
+ pass_flags = PASSTABLE
+ damage = 5 // +10 from the grab
+ armor_flag = MELEE
+ range = 7
+ hit_prone_targets = TRUE
+ hitsound = 'sound/effects/wounds/pierce1.ogg'
+ sharpness = SHARP_POINTY
+ /// Beam connecting us and the firer
+ var/datum/beam/tentacle_beam = null
+ /// Does this projectile persist and reel in targets?
+ var/reel_in = TRUE
+ /// How long does the projectile persist?
+ var/duration = 1.2 SECONDS
+ /// Damage dealt to targets who get snatched from entering the beam or being hit directly
+ var/snatch_damage = 10
+
+/obj/projectile/tentacle_lash/stab
+ damage = 15
+ range = 2
+ duration = 0.2 SECONDS // Just for visual flair
+ reel_in = FALSE
+
+/obj/projectile/tentacle_lash/fire(fire_angle, atom/direct_target)
+ . = ..()
+ if (!firer)
+ return
+ tentacle_beam = Beam(firer, "goliath_tentacle", beam_type = (reel_in ? /obj/effect/ebeam/reacting : /obj/effect/ebeam), emissive = FALSE)
+ if (reel_in)
+ RegisterSignal(tentacle_beam, COMSIG_BEAM_ENTERED, PROC_REF(on_beam_entered))
+
+/obj/projectile/tentacle_lash/Destroy()
+ QDEL_NULL(tentacle_beam)
+ return ..()
+
+// Don't range out, stop and persist until we're done
+/obj/projectile/tentacle_lash/on_range()
+ STOP_PROCESSING(SSprojectiles, src)
+ // Reset to tile center
+ pixel_x = 0
+ pixel_y = 0
+ QDEL_IN(src, duration)
+
+/obj/projectile/tentacle_lash/prehit_pierce(atom/target)
+ // Ye 'ole colossus cheese
+ if (astype(target, /mob/living)?.stat == DEAD)
+ return PROJECTILE_PIERCE_PHASE
+ return ..()
+
+/obj/projectile/tentacle_lash/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ if (!reel_in || !isliving(target) || blocked >= 100 || pierce_hit || . != BULLET_ACT_HIT)
+ return
+ snatch_target(target)
+
+/obj/projectile/tentacle_lash/proc/on_beam_entered(datum/beam/source, obj/effect/ebeam/hit, atom/movable/entered)
+ SIGNAL_HANDLER
+
+ if (!reel_in || entered == firer || !isliving(entered))
+ return
+
+ var/mob/living/victim = entered
+ if ((!firer || !firer.faction_check_atom(victim)) && victim.stat != DEAD)
+ INVOKE_ASYNC(src, PROC_REF(snatch_target), entered)
+
+/obj/projectile/tentacle_lash/proc/snatch_target(mob/living/victim)
+ if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_TENDRIL_TENTACLED_GRABBED) & COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB)
+ return
+
+ to_chat(victim, span_userdanger("You're snatched by [firer]'s tentacles!"))
+ victim.apply_damage(snatch_damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = CANT_WOUND)
+ if (QDELETED(victim))
+ qdel(src)
+ return
+ var/snatch_callback = null
+ if (istype(firer, /mob/living/basic/mining/tendril))
+ var/mob/living/basic/mining/tendril/tendril = firer
+ snatch_callback = CALLBACK(tendril, TYPE_PROC_REF(/mob/living/basic/mining/tendril, snatch_react))
+ victim.throw_at(firer, initial(range), 1, firer, FALSE, gentle = TRUE, callback = snatch_callback)
+ playsound(victim, hitsound, 50, -3, pressure_affected = FALSE)
+ qdel(src)
+
+/// An ability which makes spikes come out of the ground towards your target
+/datum/action/cooldown/mob_cooldown/tendril_chaser
+ name = "Impaling Spikes"
+ desc = "Send a spiked subterranean tendril chasing after your target."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "spikes_stabbing"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ cooldown_time = 8 SECONDS
+ click_to_activate = TRUE
+ shared_cooldown = NONE
+ /// Lazy list of references to spike trails
+ var/list/active_chasers
+ /// Health percentage threshold at which we send out wide charsers after the main target
+ var/wide_chaser_threshold = 0.7
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_MOB_ABILITY_STARTED, PROC_REF(on_ability_started))
+ RegisterSignal(granted_to, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(on_ability_finished))
+
+// Clean up after ourselves
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Remove(mob/removed_from)
+ UnregisterSignal(removed_from, list(COMSIG_MOB_ABILITY_STARTED, COMSIG_MOB_ABILITY_FINISHED))
+ QDEL_LIST(active_chasers)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_started(mob/living/owner, datum/action/cooldown/activated)
+ SIGNAL_HANDLER
+
+ // Delete all of our chasers when our owner triggers cross spikes as to not cause guaranteed damage
+ if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
+ QDEL_LIST(active_chasers)
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_finished(mob/living/owner, datum/action/cooldown/activated)
+ SIGNAL_HANDLER
+
+ if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
+ ResetCooldown()
+
+/datum/action/cooldown/mob_cooldown/tendril_chaser/Activate(atom/target)
+ . = ..()
+ var/primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser
+ if (isliving(owner))
+ var/mob/living/as_living = owner
+ if (as_living.health / as_living.maxHealth <= wide_chaser_threshold)
+ primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
+
+ var/obj/effect/temp_visual/effect_trail/tendril_chaser/chaser = new primary_type(get_turf(owner), target)
+ LAZYADD(active_chasers, WEAKREF(chaser))
+ RegisterSignal(chaser, COMSIG_QDELETING, PROC_REF(on_chaser_destroyed))
+ playsound(owner, 'sound/effects/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
+
+/// Remove a spike trail from our list of active trails
+/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_chaser_destroyed(atom/chaser)
+ SIGNAL_HANDLER
+ LAZYREMOVE(active_chasers, WEAKREF(chaser))
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser
+ duration = 10 SECONDS
+ move_speed = 4
+ spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril
+ /// Do we spawn spikes around ourselves as well or only on our own turf?
+ var/area_spawn = FALSE
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
+ area_spawn = TRUE
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/add_spawner()
+ return
+
+/obj/effect/temp_visual/effect_trail/tendril_chaser/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if (!area_spawn)
+ var/turf/spawn_turf = get_turf(src)
+ if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_turf) && isopenturf(spawn_turf))
+ new spawned_effect(spawn_turf)
+ return
+
+ for (var/spawn_dir in GLOB.cardinals)
+ if (spawn_dir & REVERSE_DIR(movement_dir))
+ continue
+ var/turf/spawn_loc = get_step(src, spawn_dir)
+ if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_loc) && isopenturf(spawn_loc))
+ new spawned_effect(spawn_loc)
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril
+ icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ icon_state = "spikes_stabbing"
+ duration = 0.7 SECONDS
+ position_variance = 3
+ impale_damage = 10
+ damage_blacklist_typecache = list(
+ /mob/living/basic/mining/tendril,
+ )
+ impale_wound_bonus = CANT_WOUND
+ // Have we hit someone yet?
+ var/hit_loser = FALSE
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/single
+ icon_state = "spike"
+ duration = 1 SECONDS
+ harm_delay = 0.25 SECONDS
+ position_variance = 5
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/impale()
+ . = ..()
+ hit_loser |= .
+ RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+
+/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if (!isliving(arrived))
+ return
+
+ if (harm_mob(arrived) && !hit_loser)
+ playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
+ hit_loser = TRUE
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes
+ name = "Cross Spikes"
+ desc = "Create a wave of spikes around yourself, impaling anyone caught in it."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "spike"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 10 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ /// Health threshold at which we reduce the amount of empty spots on the ground
+ var/health_threshold = 0.3
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/Activate(atom/target)
+ disable_cooldown_actions()
+ spawn_spikes()
+ SLEEP_CHECK_DEATH(0.4 SECONDS, owner)
+ spawn_spikes(inverse = TRUE)
+ StartCooldown()
+ enable_cooldown_actions()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/proc/spawn_spikes(inverse = FALSE)
+ var/list/turf/spike_turfs = list()
+ var/turf/owner_turf = get_turf(owner)
+
+ var/reduced_spawns = FALSE
+ if (isliving(owner))
+ var/mob/living/as_living = owner
+ if (as_living.health / as_living.maxHealth <= health_threshold)
+ reduced_spawns = TRUE
+
+ for (var/turf/open/target_turf in oview(7, owner_turf))
+ if (sqrt((target_turf.x - owner_turf.x) ** 2 + (target_turf.y - owner_turf.y) ** 2) > 9.5) // big circle is a lie
+ continue
+
+ if (reduced_spawns)
+ if (abs(target_turf.x - owner_turf.x) % 2 == abs(target_turf.y - owner_turf.y + inverse) % 2)
+ new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
+ spike_turfs += target_turf
+ continue
+
+ var/row_diff = inverse ? abs(target_turf.x - owner_turf.x) : abs(target_turf.y - owner_turf.y)
+ var/column_diff = inverse ? abs(target_turf.y - owner_turf.y) : abs(target_turf.x - owner_turf.x)
+ var/row = floor((row_diff + 1) / 3)
+ if (row % 2 == 0)
+ if (row_diff - row * 3 == 0)
+ if (column_diff % 4 == 2)
+ continue
+ else
+ if (column_diff % 4 != 0)
+ continue
+ else
+ if (row_diff - row * 3 == 0)
+ if (column_diff % 4 == 0)
+ continue
+ else
+ if (column_diff % 4 != 2)
+ continue
+
+ new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
+ spike_turfs += target_turf
+
+ SLEEP_CHECK_DEATH(1 SECONDS, owner)
+
+ for (var/turf/open/to_spawn in spike_turfs)
+ new /obj/effect/temp_visual/emerging_ground_spike/tendril/single(to_spawn)
+
+ SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee
+ name = "Tentacle Stab"
+ desc = "Stab nearby hostiles with long tentacles."
+ button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
+ button_icon_state = "goliath_tentacle_wiggle"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "bg_demon_border"
+ click_to_activate = FALSE
+ cooldown_time = 4 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+ projectile_type = /obj/projectile/tentacle_lash/stab
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/Activate(atom/target_atom, warning = TRUE)
+ if (warning)
+ for (var/stab_dir in GLOB.alldirs)
+ var/turf/open/stab_turf = get_step(owner, stab_dir)
+ if (!istype(stab_turf))
+ continue
+ var/obj/effect/temp_visual/telegraphing/line/short/telegraph = new(stab_turf)
+ telegraph.dir = stab_dir
+
+ SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
+
+ for (var/stab_dir in GLOB.alldirs)
+ shoot_projectile(get_turf(owner), get_step(owner, stab_dir), firer = owner)
+
+ SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
+ StartCooldown()
+ return TRUE
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
new file mode 100644
index 000000000000..77da7086dfa6
--- /dev/null
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
@@ -0,0 +1,35 @@
+/datum/ai_controller/basic_controller/tendril
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/call_reinforcements/mining,
+ /datum/ai_planning_subtree/target_retaliate/check_faction,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser,
+ /datum/ai_planning_subtree/use_mob_ability/tendril_spikes,
+ /datum/ai_planning_subtree/use_mob_ability/tendril_lash,
+ )
+
+/datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser
+ ability_key = BB_TENDRIL_CHASER
+ operational_datums = list(/datum/component/ai_target_timer)
+ finish_planning = FALSE
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_lash
+ ability_key = BB_TENDRIL_LASH
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_lash/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ return FALSE
+ return ..()
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_spikes
+ ability_key = BB_TENDRIL_SPIKES
+
+/datum/ai_planning_subtree/use_mob_ability/tendril_spikes/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
+ return FALSE
+ return ..()
diff --git a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
index b50b089b0ffa..60288e9c5e74 100644
--- a/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
+++ b/code/modules/mob/living/basic/space_fauna/meteor_heart/chasing_spikes.dm
@@ -45,6 +45,8 @@
var/position_variance = 8
/// Damage to deal on impale
var/impale_damage = 15
+ /// Wound bonus on impale
+ var/impale_wound_bonus = 0
/// Typecache of types of mobs not to damage
var/list/damage_blacklist_typecache = list(
/mob/living/basic/meteor_heart,
@@ -67,14 +69,22 @@
/obj/effect/temp_visual/emerging_ground_spike/proc/impale()
if (!isturf(loc))
return
+
var/hit_someone = FALSE
for(var/mob/living/victim in loc)
- if (is_type_in_typecache(victim, damage_blacklist_typecache))
- continue
- hit_someone = TRUE
- var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
- victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
+ if (harm_mob(victim))
+ hit_someone = TRUE
+
if (hit_someone)
playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
else
playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE)
+ return hit_someone
+
+/obj/effect/temp_visual/emerging_ground_spike/proc/harm_mob(mob/living/victim)
+ if (is_type_in_typecache(victim, damage_blacklist_typecache))
+ return FALSE
+
+ var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
+ victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, wound_bonus = impale_wound_bonus, sharpness = SHARP_POINTY)
+ return TRUE
diff --git a/code/modules/modular_computers/computers/item/pda.dm b/code/modules/modular_computers/computers/item/pda.dm
index a2c83e5d6e7d..cfb6eb66554a 100644
--- a/code/modules/modular_computers/computers/item/pda.dm
+++ b/code/modules/modular_computers/computers/item/pda.dm
@@ -362,6 +362,7 @@
/obj/item/modular_computer/pda/silicon/Destroy()
silicon_owner = null
+ robotact = null
return ..()
///Silicons don't have the tools (or hands) to make circuits setups with their own PDAs.
diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm
index 7721d50b7639..661318f4baf9 100644
--- a/code/modules/modular_computers/file_system/data.dm
+++ b/code/modules/modular_computers/file_system/data.dm
@@ -98,6 +98,10 @@
src.workspace = workspace
src.source_photo_or_painting = source_photo_or_painting
+/datum/computer_file/data/paint_project/Destroy(force)
+ source_photo_or_painting = null
+ return ..()
+
/datum/computer_file/data/paint_project/clone(rename)
var/datum/computer_file/data/paint_project/temp = ..()
temp.workspace = workspace.copy()
diff --git a/code/modules/modular_computers/file_system/image_file.dm b/code/modules/modular_computers/file_system/image_file.dm
index f029793701d2..8dcfd942f227 100644
--- a/code/modules/modular_computers/file_system/image_file.dm
+++ b/code/modules/modular_computers/file_system/image_file.dm
@@ -27,6 +27,10 @@
src.image_name = image_name
set_source(source_photo_or_painting)
+/datum/computer_file/image/Destroy(force)
+ source_photo_or_painting = null
+ return ..()
+
/datum/computer_file/image/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing)
. = ..()
assign_path()
diff --git a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
index f71d985197c9..5ea1bb1f51b9 100644
--- a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
+++ b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm
@@ -126,8 +126,8 @@
if("PRG_savelog")
if(!channel)
return
- var/logname = stripped_input(params["log_name"])
- if(!logname)
+ var/logname = trim(params["log_name"], MAX_MESSAGE_LEN)
+ if(!length(logname) || !filter_filename_pda(logname))
return
var/datum/computer_file/data/text/logfile = new()
// Now we will generate HTML-compliant file that can actually be viewed/printed.
diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm
index d25a9894307e..8c3dbfe213a5 100644
--- a/code/modules/modular_computers/file_system/programs/file_browser.dm
+++ b/code/modules/modular_computers/file_system/programs/file_browser.dm
@@ -119,8 +119,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
var/datum/computer_file/file = computer.find_file_by_name(params["name"])
if(!file)
return
- var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
- if(!newname || newname != params["new_name"])
+ var/newname = trim(params["new_name"], MAX_MESSAGE_LEN)
+ if(!length(newname) || !filter_filename_pda(newname))
playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE)
return
file.filename = newname
@@ -131,8 +131,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
var/datum/computer_file/file = computer.find_file_by_name(params["name"], computer.inserted_disk)
if(!file)
return
- var/newname = reject_bad_name(params["new_name"], allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
- if(!newname || newname != params["new_name"])
+ var/newname = trim(params["new_name"], MAX_MESSAGE_LEN)
+ if(!length(newname) || !filter_filename_pda(newname))
playsound(computer, 'sound/machines/terminal/terminal_error.ogg', 25, FALSE)
return
file.filename = newname
diff --git a/code/modules/modular_computers/file_system/programs/nanopaint.dm b/code/modules/modular_computers/file_system/programs/nanopaint.dm
index 6e2d4bd49ab7..4d499db7c240 100644
--- a/code/modules/modular_computers/file_system/programs/nanopaint.dm
+++ b/code/modules/modular_computers/file_system/programs/nanopaint.dm
@@ -175,11 +175,14 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
return
dialog = null
var/uid = params["uid"]
- var/new_file_name = params["name"]
+ var/new_file_name = trim(params["name"], MAX_MESSAGE_LEN)
var/saving_to_disk = params["onDisk"]
var/datum/computer_file/new_file_type = text2path(params["typepath"])
var/extension = new_file_type::filetype
var/datum/computer_file/existing_file
+ if(!length(new_file_name))
+ dialog = list("type" = "error", "message" = "No name specified.")
+ return TRUE
if(saving_to_disk)
if(!computer.inserted_disk)
dialog = list("type" = "error", "message" = "[new_file_name] - The disk has been removed.")
@@ -208,6 +211,12 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
else
INVOKE_ASYNC(src, PROC_REF(write_to_file), user, existing_file)
return TRUE
+ if(is_ic_filtered_for_pdas(new_file_name) || is_soft_ic_filtered_for_pdas(new_file_name))
+ dialog = list("type" = "error", "message" = "The entered file name violates company policy.")
+ return TRUE
+ if(!filter_illegal_filename_chars(new_file_name))
+ dialog = list("type" = "error", "message" = "[new_file_name] - File names cannot include the following characters. \n \\ / : * ? \" < > |")
+ return TRUE
INVOKE_ASYNC(src, PROC_REF(save_file), user, new_file_name, new_file_type, saving_to_disk && computer.inserted_disk)
return TRUE
@@ -283,7 +292,7 @@ GLOBAL_LIST_INIT(nanopaint_supported_filetypes, zebra_typecacheof(list(\
/datum/computer_file/program/nanopaint/proc/save_file(mob/user, name, file_type, obj/item/disk/computer/target_disk)
var/datum/computer_file/file = new file_type()
- file.filename = reject_bad_name(name, allow_numbers = TRUE, cap_after_symbols = FALSE, cap_at_start = FALSE)
+ file.filename = name
var/file_stored
if(target_disk)
file_stored = target_disk.add_file(file)
diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm
index aa2016c42897..52184d2c9592 100644
--- a/code/modules/power/lighting/light.dm
+++ b/code/modules/power/lighting/light.dm
@@ -81,6 +81,8 @@
var/power_consumption_rate = 20
///break if moved, if false also makes it ignore if the wall its on breaks
var/break_if_moved = TRUE
+ /// If TRUE we can break on init
+ var/allow_break_on_init = TRUE
// create a new lighting fixture
/obj/machinery/light/Initialize(mapload)
@@ -126,13 +128,14 @@
/obj/machinery/light/post_machine_initialize()
. = ..()
#ifndef MAP_TEST
- switch(fitting)
- if("tube")
- if(prob(2))
- break_light_tube(TRUE)
- if("bulb")
- if(prob(5))
- break_light_tube(TRUE)
+ if(allow_break_on_init)
+ switch(fitting)
+ if("tube")
+ if(prob(2))
+ break_light_tube(TRUE)
+ if("bulb")
+ if(prob(5))
+ break_light_tube(TRUE)
#endif
update(trigger = FALSE)
diff --git a/code/modules/procedural_mapping/mapGenerators/lavaland.dm b/code/modules/procedural_mapping/mapGenerators/lavaland.dm
index 5251f5e8435a..b638e7908dd8 100644
--- a/code/modules/procedural_mapping/mapGenerators/lavaland.dm
+++ b/code/modules/procedural_mapping/mapGenerators/lavaland.dm
@@ -18,9 +18,7 @@
/datum/map_generator_module/splatter_layer/lavaland_tendrils
spawnableTurfs = list()
- spawnableAtoms = list(/obj/structure/spawner/lavaland = 5,
- /obj/structure/spawner/lavaland/legion = 5,
- /obj/structure/spawner/lavaland/goliath = 5)
+ spawnableAtoms = list(/mob/living/basic/mining/tendril = 15)
/datum/map_generator/lavaland/ground_only
modules = list(/datum/map_generator_module/bottom_layer/lavaland_default)
diff --git a/code/modules/projectiles/ammunition/ballistic/pistol.dm b/code/modules/projectiles/ammunition/ballistic/pistol.dm
index e5ddae7c2e91..0805b0060f9d 100644
--- a/code/modules/projectiles/ammunition/ballistic/pistol.dm
+++ b/code/modules/projectiles/ammunition/ballistic/pistol.dm
@@ -22,11 +22,6 @@
desc = "A 10mm incendiary bullet casing."
projectile_type = /obj/projectile/bullet/incendiary/c10mm
-/obj/item/ammo_casing/c10mm/reaper
- name = "10mm reaper bullet casing"
- desc = "A 10mm reaper bullet casing."
- projectile_type = /obj/projectile/bullet/c10mm/reaper
-
// 9mm (Makarov, Stechkin APS)
/obj/item/ammo_casing/c9mm
diff --git a/code/modules/projectiles/ammunition/ballistic/smg.dm b/code/modules/projectiles/ammunition/ballistic/smg.dm
index 7e84bc1a9182..e2be056126b5 100644
--- a/code/modules/projectiles/ammunition/ballistic/smg.dm
+++ b/code/modules/projectiles/ammunition/ballistic/smg.dm
@@ -41,3 +41,8 @@
name = ".45 incendiary bullet casing"
desc = "A .45 bullet casing."
projectile_type = /obj/projectile/bullet/incendiary/c45
+
+/obj/item/ammo_casing/c45/reaper
+ name = ".45 reaper bullet casing"
+ desc = "A .45 reaper bullet casing."
+ projectile_type = /obj/projectile/bullet/c45/reaper
diff --git a/code/modules/projectiles/boxes_magazines/external/pistol.dm b/code/modules/projectiles/boxes_magazines/external/pistol.dm
index 7fbece15863e..27c3d39f6b3c 100644
--- a/code/modules/projectiles/boxes_magazines/external/pistol.dm
+++ b/code/modules/projectiles/boxes_magazines/external/pistol.dm
@@ -92,25 +92,6 @@
MAGAZINE_TYPE_ARMORPIERCE
ammo_type = /obj/item/ammo_casing/c10mm/ap
-// Regal Condor (10mm) //
-
-/obj/item/ammo_box/magazine/r10mm
- name = "regal condor magazine (10mm Reaper)"
- desc = "A very expensive 10mm handgun magazine, suitable for the Regal Condor. Loaded with \"reaper\" rounds, which are dangerously effective against everything."
- icon_state = "r10mm-8"
- base_icon_state = "r10mm"
- ammo_type = /obj/item/ammo_casing/c10mm/reaper
- caliber = CALIBER_10MM
- max_ammo = 8
- multiple_sprites = AMMO_BOX_PER_BULLET
- multiple_sprite_use_base = TRUE
- custom_materials = list(
- /datum/material/iron = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/gold = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/silver = SHEET_MATERIAL_AMOUNT * 10,
- /datum/material/plasma = SHEET_MATERIAL_AMOUNT * 10,
- )
-
// M1911 (.45) //
/obj/item/ammo_box/magazine/m45
@@ -134,3 +115,22 @@
caliber = CALIBER_50AE
max_ammo = 7
multiple_sprites = AMMO_BOX_PER_BULLET
+
+// Regal Condor (.45) //
+
+/obj/item/ammo_box/magazine/r45
+ name = "regal condor magazine (.45 Reaper)"
+ desc = "A very expensive .45 handgun magazine, suitable for the Regal Condor. Loaded with \"reaper\" rounds, which are dangerously effective against everything."
+ icon_state = "r45-8"
+ base_icon_state = "r45"
+ ammo_type = /obj/item/ammo_casing/c45/reaper
+ caliber = CALIBER_10MM
+ max_ammo = 8
+ multiple_sprites = AMMO_BOX_PER_BULLET
+ multiple_sprite_use_base = TRUE
+ custom_materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/gold = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/silver = SHEET_MATERIAL_AMOUNT * 10,
+ /datum/material/plasma = SHEET_MATERIAL_AMOUNT * 10,
+ )
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 843f0fcb741c..d0f61730727d 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -124,6 +124,7 @@
add_seclight_point()
add_bayonet_point()
+ add_deep_lore()
RegisterSignal(src, COMSIG_ITEM_IN_UNWRAPPED_TRAITOR_MAIL, PROC_REF(on_mail_unwrap))
/obj/item/gun/Destroy()
@@ -154,6 +155,10 @@
/obj/item/gun/proc/add_bayonet_point()
return
+/// For when you want to add the lore element to a gun, this is the proc to use.
+/obj/item/gun/proc/add_deep_lore()
+ return
+
/obj/item/gun/Exited(atom/movable/gone, direction)
. = ..()
if(gone == pin)
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index c28ed3c012dd..08ddb0155ed1 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -60,6 +60,17 @@
empty_indicator = TRUE
suppressor_x_offset = 12
+/obj/item/gun/ballistic/automatic/pistol/clandestine/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "Manufactured by Scarborough Arms, the Ansem is regarded as one of the best sidearms on the market. Minimal recoil combined with \
+ high stopping power makes it a favourite amongst soldiers of fortune across known space.
\
+
\
+ The ease of concealment due to the weapon's sleek profile has given it a well-earned reputation as the 'smuggler's gun of course'. While \
+ space pirate bands tend to favour the HDS-1 created by Nanotrasen due to their low maintenance cost, the Ansem has seen plenty of use by \
+ mercernaries, hitmen and espionage agents. Dozens of seized Ansem pistols line evidence lockups across the Spinward. Many with extremely \
+ long and bloodied histories from years of service amongst underground criminal elements." \
+ )
+
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher
name = "\improper Ansem/SC pistol"
desc = "A modified variant of the Ansem, spiritual successor to the Makarov, featuring an integral suppressor and push-button trigger on the grip \
@@ -74,6 +85,8 @@
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher/Initialize(mapload)
. = ..()
underbarrel = new /obj/item/gun/energy/recharge/fisher(src)
+
+/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher/add_deep_lore()
AddElement(/datum/element/examine_lore, \
lore = "The Ansem/SC is a Scarborough Arms overhaul suite for their own Ansem handgun, designed for special operators who operate operationally, \
especially against people who like using lightbulbs.
\
@@ -125,6 +138,18 @@
lock_back_sound = 'sound/items/weapons/gun/pistol/slide_lock.ogg'
bolt_drop_sound = 'sound/items/weapons/gun/pistol/slide_drop.ogg'
+/obj/item/gun/ballistic/automatic/pistol/m1911/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "The Colt M1911, created by John Moses Browning centuries ago, is the primordial ancestor of modern automatic firearms. \
+ Somehow, to this day, there are still officers who refuse to utilize anything that isn't chambered in 'God's Caliber'.
\
+
\
+ Few want to tell any of these people that .45 ACP has struggled to puncture modern ballistic plating for almost five centuries.
\
+
\
+ You are VERY confident that this particular example was lathed this decade. Most M1911's seen in the wild today are recreations of the weapon \
+ made with modern alloyed metals and manufactoring techniques, mass produced for the consumer market. This does not stop fraudsters from \
+ claiming that their weapon is a 'family heirloom' that has saved them from various conflicts that they insist they participated in." \
+ )
+
/**
* Weak 1911 for syndicate chimps. It comes in a 4 TC kit.
* 15 damage every.. second? 7 shots to kill. Not fast.
@@ -136,6 +161,8 @@
projectile_wound_bonus = -12
pin = /obj/item/firing_pin/monkey
+/obj/item/gun/ballistic/automatic/pistol/m1911/chimpgun/add_deep_lore()
+ return
/obj/item/gun/ballistic/automatic/pistol/m1911/no_mag
spawnwithmagazine = FALSE
@@ -180,17 +207,30 @@
name = "\improper Regal Condor"
desc = "Unlike the Desert Eagle, this weapon seems to utilize some kind of advanced internal stabilization system to significantly \
reduce felt recoil and increase overall accuracy, at the cost of using a smaller caliber. \
- This does allow it to fire a very quick 2-round burst. Uses 10mm ammo."
+ This allows it to fire a very quick 2-round burst. Chambered in .45."
icon_state = "reagle"
inhand_icon_state = "deagleg"
burst_size = 2
burst_delay = 1
projectile_damage_multiplier = 1.25
- accepted_magazine_type = /obj/item/ammo_box/magazine/r10mm
+ accepted_magazine_type = /obj/item/ammo_box/magazine/r45
actions_types = list(/datum/action/item_action/toggle_firemode)
obj_flags = UNIQUE_RENAME // if you did the sidequest, you get the customization
custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 30, /datum/material/silver = SHEET_MATERIAL_AMOUNT * 25, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 11.5, /datum/material/telecrystal = SHEET_MATERIAL_AMOUNT * 4)
+/obj/item/gun/ballistic/automatic/pistol/deagle/regal/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "One of the Culling Arms of the Tiger Cooperative, a collection of relic weapons said to have come to Martinson in a frenzied vision from \
+ God to enact holy retribution upon the enemies of the faith. Almost all examples of these guns are replicas of the original pattern. This one is no \
+ different. However, it carries some of the gravitus of the original firearm. And, some say, the holy might that it wielded against the enemies of the cult.
\
+
\
+ The fact that you are looking at this relic weapon means one of two things.
\
+
\
+ A) A Tiger Cooperative evangelist has died during or before they could commit a bloody massacre in the name of God. Or
\
+
\
+ B) You are that evangelist, and will soon be adding another chapter to this gun's dark history." \
+ )
+
/obj/item/gun/ballistic/automatic/pistol/aps
name = "\improper Stechkin APS machine pistol"
desc = "An old Soviet machine pistol. It fires quickly, but kicks like a mule. Uses 9mm ammo. Has a threaded barrel for suppressors."
@@ -225,10 +265,9 @@
#define DOORHICKEY_GUN_MIN_DAMAGE 70
#define DOORHICKEY_GUN_MAX_DAMAGE 140
-/obj/item/gun/ballistic/automatic/pistol/doorhickey
+/obj/item/gun/ballistic/automatic/pistol/doohickey
name = "\improper Liberator"
- desc = "A poorly made 3D printed \"gun\", only capable of firing a single shot. Well-known throughout the Spinward Sector \
- after an incident where 3 assistants were killed by shrapnel from such a device exploding while attempting to shoot a mouse."
+ desc = "A poorly made 3D printed \"gun\", only capable of firing a single shot."
icon_state = "doorhickey"
custom_materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 2)
bolt_type = BOLT_TYPE_NO_BOLT
@@ -243,7 +282,7 @@
projectile_damage_multiplier = 0.5
spread = 10
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/unload_ammo(mob/living/user, forced = FALSE)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/unload_ammo(mob/living/user, forced = FALSE)
if (forced)
return ..()
@@ -254,7 +293,7 @@
return
. = ..()
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/load_gun(obj/item/ammo, mob/living/user)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/load_gun(obj/item/ammo, mob/living/user)
. = ..()
if (!.)
return
@@ -268,7 +307,7 @@
unload_ammo(user, forced = TRUE)
return FALSE
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread)
var/dmg_multiplier = 1
if (get_dist(target, user) <= 1)
@@ -287,7 +326,7 @@
. = ..()
projectile_damage_multiplier /= dmg_multiplier
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/shoot_live_shot(mob/living/user, pointblank = FALSE, atom/pbtarget = null, message = TRUE)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/shoot_live_shot(mob/living/user, pointblank = FALSE, atom/pbtarget = null, message = TRUE)
. = ..()
if (!.)
return
@@ -326,6 +365,26 @@
new /obj/effect/decal/cleanable/plastic(get_turf(src))
take_damage(damage_to_take)
+/obj/item/gun/ballistic/automatic/pistol/doohickey/add_deep_lore()
+ AddElement(/datum/element/examine_lore, \
+ lore = "The Liberator pattern, according to digital historians, was first posted to a fringe imageboard on the NTNet. The post included a now dead \
+ link to a defuncting hosting service through which board members were encouraged to download and 'admire' the design. The thread's author began their \
+ post by describing their extreme vitriolic hatred of so-called 'moon men' amongst the Nanotrasen 'elites'. And that, through 'the sauce', they had \
+ been shown a means to 'end the oppression of Luna's Tormentors'. They called it the 'Liberator' as a result.
\
+
\
+ Response to this thread began as skeptism amongst posters regarding the 3D pattern. Many highlighted the dubious functionality, let alone safe operation \
+ of a gun created using cheap plastic and minimal machined parts firing a high pressure round through an unrifled barrel. But the thread suddenly began to \
+ gain traction immediately following a particular news article published by the Spinward Dove, a respected editorial in the Spinward Sector. The author of this \
+ article recounts that, during a particularly brisk day in New Moscow, they observed three off-duty Nanotrasen assistants attempting to use what the author \
+ described as a 'bizarre plastic doohickey' to shoot a mouse at point blank. The horrific aftermath, and images of the rattled but otherwise unscathed mouse, lead many \
+ to try and find from where these now crippled assistants had come across such an absurd device. Word spread of the post, and its author's continuously nonsensical \
+ rants regarding their hated foe in other areas of the imageboard.
\
+
\
+ The author of the thread returned immediately following the news, claiming that those who frequented the imageboard had 'stolen' their 'dreams of freedom', and \
+ that clearly the moon men were attempting to discredit their invention through this publicity stunt. A slur-laiden rant from the thread author followed this declaration, \
+ leading to the the moderators of the imageboard to step in and close the thread for good." \
+ )
+
/obj/item/disk/design_disk/liberator
name = "illegal 3D printer design disk"
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index de7922b8ef8d..a47d25218663 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -11,8 +11,6 @@
/obj/item/gun/energy/laser/Initialize(mapload)
. = ..()
- add_deep_lore()
-
// Only regular lasguns can be slapcrafted
if(type != /obj/item/gun/energy/laser)
return
@@ -305,7 +303,7 @@
// Laser Gun
-/obj/item/gun/energy/laser/proc/add_deep_lore()
+/obj/item/gun/energy/laser/add_deep_lore()
AddElement(/datum/element/examine_lore, \
lore_hint = span_notice("You can [EXAMINE_HINT("look closer")] to learn a little more about [src]."), \
lore = "The NT Type 5 Heat Delivery System (sometimes referred to as the HDS-5 in promotional material) is what truly put Nanotrasen \
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 3c695a10b738..d7aa59766b3c 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -1251,8 +1251,8 @@
free_hitscan_forceMove = TRUE
forceMove(source_loc)
starting = source_loc
- pixel_x = source.pixel_x
- pixel_y = source.pixel_y
+ pixel_x = source.pixel_x - source.base_pixel_x
+ pixel_y = source.pixel_y - source.base_pixel_y
original = target
// Trim off excess pixel_x/y by converting them into turf offset
diff --git a/code/modules/projectiles/projectile/bullets/pistol.dm b/code/modules/projectiles/projectile/bullets/pistol.dm
index 9c799fb9c0a5..cb852ad446db 100644
--- a/code/modules/projectiles/projectile/bullets/pistol.dm
+++ b/code/modules/projectiles/projectile/bullets/pistol.dm
@@ -53,26 +53,6 @@
damage = 20
fire_stacks = 3
-/obj/projectile/bullet/c10mm/reaper
- name = "10mm reaper pellet"
- icon_state = null
- damage = 50
- armour_penetration = 40
- tracer_type = /obj/effect/projectile/tracer/sniper
- impact_type = /obj/effect/projectile/impact/sniper
- muzzle_type = /obj/effect/projectile/muzzle/sniper
- hitscan = TRUE
- impact_effect_type = null
- hitscan_light_intensity = 3
- hitscan_light_range = 0.75
- hitscan_light_color_override = LIGHT_COLOR_DIM_YELLOW
- muzzle_flash_intensity = 5
- muzzle_flash_range = 1
- muzzle_flash_color_override = LIGHT_COLOR_DIM_YELLOW
- impact_light_intensity = 5
- impact_light_range = 1
- impact_light_color_override = LIGHT_COLOR_DIM_YELLOW
-
// .160 Smart
/obj/projectile/bullet/c160smart
diff --git a/code/modules/projectiles/projectile/bullets/smg.dm b/code/modules/projectiles/projectile/bullets/smg.dm
index 38280b6f91e9..2c64bac2878c 100644
--- a/code/modules/projectiles/projectile/bullets/smg.dm
+++ b/code/modules/projectiles/projectile/bullets/smg.dm
@@ -20,6 +20,26 @@
damage = 15
fire_stacks = 2
+/obj/projectile/bullet/c45/reaper
+ name = ".45 reaper pellet"
+ icon_state = null
+ damage = 50
+ armour_penetration = 40
+ tracer_type = /obj/effect/projectile/tracer/sniper
+ impact_type = /obj/effect/projectile/impact/sniper
+ muzzle_type = /obj/effect/projectile/muzzle/sniper
+ hitscan = TRUE
+ impact_effect_type = null
+ hitscan_light_intensity = 3
+ hitscan_light_range = 0.75
+ hitscan_light_color_override = LIGHT_COLOR_DIM_YELLOW
+ muzzle_flash_intensity = 5
+ muzzle_flash_range = 1
+ muzzle_flash_color_override = LIGHT_COLOR_DIM_YELLOW
+ impact_light_intensity = 5
+ impact_light_range = 1
+ impact_light_color_override = LIGHT_COLOR_DIM_YELLOW
+
// 4.6x30mm (Autorifles)
/obj/projectile/bullet/c46x30mm
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 728d0e70e803..8d9f0989c1c6 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -726,7 +726,7 @@
var/obj/effect/decal/cleanable/food/flour/flour_decal = exposed_turf.spawn_unique_cleanable(/obj/effect/decal/cleanable/food/flour)
if(flour_decal)
- flour_decal.reagents.add_reagent(/datum/reagent/consumable/flour, reac_volume)
+ flour_decal.init_reagents(/datum/reagent/consumable/flour, reac_volume)
/datum/reagent/consumable/cherryjelly
name = "Cherry Jelly"
diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm
index d9d15931c0f0..54339a7aacff 100644
--- a/code/modules/research/anomaly/anomaly_core.dm
+++ b/code/modules/research/anomaly/anomaly_core.dm
@@ -206,7 +206,7 @@
if(!length(possible_targets))
return
var/turf/target = pick(possible_targets)
- new /obj/effect/temp_visual/telegraphing/thunderbolt(target)
+ new /obj/effect/temp_visual/telegraphing/circle(target)
addtimer(CALLBACK(src, PROC_REF(strike), target), 1 SECONDS)
/obj/item/assembly/signaler/anomaly/weather/proc/strike(turf/target)
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index b09378ea49b8..767c5bfcfb3b 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -636,7 +636,7 @@
id = "liberator_gun"
build_type = AUTOLATHE
materials = list(/datum/material/plastic = SHEET_MATERIAL_AMOUNT * 2, /datum/material/iron = SMALL_MATERIAL_AMOUNT * 15)
- build_path = /obj/item/gun/ballistic/automatic/pistol/doorhickey
+ build_path = /obj/item/gun/ballistic/automatic/pistol/doohickey
category = list(RND_CATEGORY_IMPORTED)
/datum/design/stun_boomerang
diff --git a/code/modules/spells/spell_types/right_and_wrong.dm b/code/modules/spells/spell_types/right_and_wrong.dm
index 48422a26fdc9..7bf95a8d7054 100644
--- a/code/modules/spells/spell_types/right_and_wrong.dm
+++ b/code/modules/spells/spell_types/right_and_wrong.dm
@@ -27,7 +27,7 @@ GLOBAL_LIST_INIT(summoned_guns, list(
/obj/item/gun/ballistic/automatic/pistol/clandestine/fisher,
/obj/item/gun/ballistic/automatic/pistol/deagle,
/obj/item/gun/ballistic/automatic/pistol/deagle/regal,
- /obj/item/gun/ballistic/automatic/pistol/doorhickey,
+ /obj/item/gun/ballistic/automatic/pistol/doohickey,
/obj/item/gun/ballistic/automatic/pistol/m1911,
/obj/item/gun/ballistic/automatic/proto/unrestricted,
/obj/item/gun/ballistic/automatic/smartgun,
diff --git a/code/modules/unit_tests/door_access.dm b/code/modules/unit_tests/door_access.dm
index 595e68297ad1..78e663bb310b 100644
--- a/code/modules/unit_tests/door_access.dm
+++ b/code/modules/unit_tests/door_access.dm
@@ -173,3 +173,55 @@
door.req_access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
subject.Bump(door)
TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened access-locked airlock while hands blocked!")
+
+/// Checks that doors with the id wire cut are inaccessible
+/datum/unit_test/door_require_id_wire_cut
+
+/datum/unit_test/door_require_id_wire_cut/Run()
+ var/obj/machinery/door/airlock/instant/door = EASY_ALLOCATE()
+ door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE
+ door.req_access = list(ACCESS_ENGINEERING)
+
+ var/mob/living/carbon/human/subject = EASY_ALLOCATE()
+ subject.equipOutfit(/datum/outfit/job/assistant/consistent)
+ var/obj/item/card/id/advanced/keycard = subject.wear_id
+ keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
+
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock before id wire cut!")
+ door.close()
+ door.wires.pulse(WIRE_OPEN, subject)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Pulsing the door's open wire allowed the subject to open the access-locked airlock without cutting the id wire!")
+ door.wires.cut(WIRE_IDSCAN, subject)
+ subject.last_bumped = 0
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Subject opened access-locked airlock with id wire cut!")
+ door.wires.pulse(WIRE_OPEN, subject)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock with id wire cut and open wire pulsed!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN, subject) // mend
+ subject.last_bumped = 0
+ subject.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Subject failed to open access-locked airlock after mending id wire!")
+
+/// Same as above but testing throwing the item at the door
+/datum/unit_test/door_require_id_wire_cut/item_only
+
+/datum/unit_test/door_require_id_wire_cut/item_only/Run()
+ var/obj/machinery/door/airlock/instant/door = EASY_ALLOCATE()
+ door.interaction_flags_machine |= INTERACT_MACHINE_OFFLINE
+ door.req_access = list(ACCESS_ENGINEERING)
+
+ var/obj/item/card/id/advanced/keycard = EASY_ALLOCATE()
+ keycard.access = list(ACCESS_ENGINEERING, ACCESS_MAINT_TUNNELS)
+
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Throwing an ID at an access-locked airlock failed to open it before id wire cut!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN)
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, TRUE, "Throwing an ID at an access-locked airlock succeeded in opening it after id wire cut!")
+ door.close()
+ door.wires.cut(WIRE_IDSCAN) // mend
+ keycard.Bump(door)
+ TEST_ASSERT_EQUAL(door.density, FALSE, "Throwing an ID at an access-locked airlock failed to open it after mending the id wire!")
diff --git a/html/changelogs/AutoChangeLog-pr-96171.yml b/html/changelogs/AutoChangeLog-pr-96171.yml
deleted file mode 100644
index 82c646052f3a..000000000000
--- a/html/changelogs/AutoChangeLog-pr-96171.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "lelandkemble"
-delete-after: True
-changes:
- - balance: "Bitrunners can't back out of domains via the ladder while bitrunning glitches remain in them"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-96289.yml b/html/changelogs/AutoChangeLog-pr-96289.yml
deleted file mode 100644
index e7cea8825af1..000000000000
--- a/html/changelogs/AutoChangeLog-pr-96289.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Wisemonster"
-delete-after: True
-changes:
- - bugfix: "Fixed Nebula's boulder refinery being constantly spaced."
\ No newline at end of file
diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml
index 3aedd611db81..9f55dcb45881 100644
--- a/html/changelogs/archive/2026-06.yml
+++ b/html/changelogs/archive/2026-06.yml
@@ -14,3 +14,38 @@
lelandkemble:
- balance: The RNG instant kill via organ removal of the mech drill has been removed,
and replaced with a very high wounding chance
+2026-06-02:
+ Iajret:
+ - bugfix: Mending touch works once again
+ Melbert:
+ - bugfix: Cutting an airlock's ID wire prevents ID usage again (instead of changing
+ the door to all access)
+ - map: Roundstart nuclear operatives now spawn in the nuke base elevator, rather
+ than in the base itself. This change was done so we could reduce the lag from
+ loading the nuke base during round startup.
+ SmArtKar:
+ - bugfix: Fixed spilled flour not containing any flour
+ - balance: You can make toolboxes out of meat and paper again
+ - balance: Lavaland tendrils have been reworked into a full fledged miniboss instead
+ of a simple mob spawner.
+ - balance: Lavaland mobs minimum spawn separation distance has been lowered from
+ 12 to 7
+ TheRyeGuyWhoWillNowDie:
+ - bugfix: jaws of recovery are blacklisted from crafting the jaws of recovery
+ Wisemonster:
+ - bugfix: Fixed Nebula's boulder refinery being constantly spaced.
+ Y0SH1M4S73R:
+ - bugfix: Non-square paint canvases and Nanopaint projects correctly save to icons.
+ - bugfix: You can no longer enter empty filenames when saving Nanopaint files.
+ - spellcheck: User-provided modular computer filenames are now checked against the
+ pda IC filter.
+ lelandkemble:
+ - balance: Bitrunners can't back out of domains via the ladder while bitrunning
+ glitches remain in them
+ necromanceranne:
+ - spellcheck: Adds some additional deep lore to some of the pistols in the game.
+ FInd your favourite and see if I cared to add one.
+ - spellcheck: Rewrote the lore for contractor batons and stun batons.
+ - code_imp: Repathed Liberators. This has no bearing on their function.
+ - code_imp: Rechambers Regal Condors in .45 and 10mm Reaper rounds to .45 Reaper
+ rounds. This has no impact on their function.
diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi
index 3f5eb9cbd3b5..f338fab310ae 100644
Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ
diff --git a/icons/mob/simple/lavaland/tendril.dmi b/icons/mob/simple/lavaland/tendril.dmi
new file mode 100644
index 000000000000..0bbd0ccbe218
Binary files /dev/null and b/icons/mob/simple/lavaland/tendril.dmi differ
diff --git a/icons/mob/telegraphing/telegraph.dmi b/icons/mob/telegraphing/telegraph.dmi
index 6afddd04b62f..1483234c763b 100644
Binary files a/icons/mob/telegraphing/telegraph.dmi and b/icons/mob/telegraphing/telegraph.dmi differ
diff --git a/icons/obj/weapons/guns/ammo.dmi b/icons/obj/weapons/guns/ammo.dmi
index 168b55dfcb6b..06eb704f52dc 100644
Binary files a/icons/obj/weapons/guns/ammo.dmi and b/icons/obj/weapons/guns/ammo.dmi differ
diff --git a/icons/obj/weapons/guns/projectiles.dmi b/icons/obj/weapons/guns/projectiles.dmi
index 24aa777660d5..19bdf00bac92 100644
Binary files a/icons/obj/weapons/guns/projectiles.dmi and b/icons/obj/weapons/guns/projectiles.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 2acd31a9e325..d9914a272a00 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3089,7 +3089,6 @@
#include "code\game\objects\structures\icemoon\cave_entrance.dm"
#include "code\game\objects\structures\lavaland\geyser.dm"
#include "code\game\objects\structures\lavaland\gulag_vent.dm"
-#include "code\game\objects\structures\lavaland\necropolis_tendril.dm"
#include "code\game\objects\structures\lavaland\ore_vent.dm"
#include "code\game\objects\structures\plaques\_plaques.dm"
#include "code\game\objects\structures\plaques\static_plaques.dm"
@@ -5411,6 +5410,9 @@
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_egg.dm"
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_food_trough.dm"
#include "code\modules\mob\living\basic\lavaland\raptor\raptor_inheritance.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril_actions.dm"
+#include "code\modules\mob\living\basic\lavaland\tendril\tendril_ai.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_ai.dm"
#include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm"
diff --git a/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt b/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt
new file mode 100644
index 000000000000..8e3f4b313f01
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96056_pistols_and_stuff.txt
@@ -0,0 +1,4 @@
+/obj/item/gun/ballistic/automatic/pistol/doorhickey : /obj/item/gun/ballistic/automatic/pistol/doohickey{@OLD}
+/obj/item/ammo_box/magazine/r10mm : /obj/item/ammo_box/magazine/r45{@OLD}
+/obj/item/ammo_casing/c10mm/reaper : /obj/item/ammo_casing/c45/reaper{@OLD}
+/obj/projectile/bullet/c10mm/reaper : /obj/projectile/bullet/c45/reaper{@OLD}
diff --git a/tools/UpdatePaths/Scripts/96186_tendrils.txt b/tools/UpdatePaths/Scripts/96186_tendrils.txt
new file mode 100644
index 000000000000..eb2f45499c42
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96186_tendrils.txt
@@ -0,0 +1 @@
+/obj/structure/spawner/lavaland : /mob/living/basic/mining/tendril