diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_homestead.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_homestead.dmm
index 80e245fa11ba..b6f057ca94df 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_homestead.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_homestead.dmm
@@ -4,7 +4,7 @@
/turf/open/misc/asteroid/snow/icemoon,
/area/icemoon/surface/outdoors/nospawn)
"cP" = (
-/mob/living/simple_animal/hostile/asteroid/polarbear,
+/mob/living/basic/mining/polarbear,
/turf/open/floor/wood/large{
initial_gas_mix = "ICEMOON_ATMOS"
},
diff --git a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
index fad279c0710e..df600c338fab 100644
--- a/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
+++ b/_maps/RandomRuins/IceRuins/icemoon_underground_abandoned_plasma_facility.dmm
@@ -204,7 +204,7 @@
/turf/open/floor/iron,
/area/ruin/plasma_facility/operations)
"dB" = (
-/mob/living/simple_animal/hostile/asteroid/polarbear{
+/mob/living/basic/mining/polarbear{
name = "Baloo"
},
/turf/open/misc/asteroid/snow/icemoon,
diff --git a/_maps/RandomRuins/LavaRuins/lavaland_surface_cube.dmm b/_maps/RandomRuins/LavaRuins/lavaland_surface_cube.dmm
index 3e64a36fdb49..6c3818b43ecb 100644
--- a/_maps/RandomRuins/LavaRuins/lavaland_surface_cube.dmm
+++ b/_maps/RandomRuins/LavaRuins/lavaland_surface_cube.dmm
@@ -1,13 +1,13 @@
//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
"a" = (
-/turf/closed/mineral/volcanic/lava_land_surface,
+/turf/closed/mineral/volcanic/lava_land_surface/biome_replace,
/area/lavaland/surface/outdoors)
"b" = (
-/turf/open/misc/asteroid/basalt/lava_land_surface,
+/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace,
/area/lavaland/surface/outdoors)
"c" = (
/obj/effect/decal/remains/human,
-/turf/open/misc/asteroid/basalt/lava_land_surface,
+/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace,
/area/lavaland/surface/outdoors)
"d" = (
/turf/closed/indestructible/riveted,
@@ -15,7 +15,7 @@
"e" = (
/obj/machinery/wish_granter,
/obj/effect/mapping_helpers/no_lava,
-/turf/open/misc/asteroid/basalt/lava_land_surface,
+/turf/open/misc/asteroid/basalt/lava_land_surface/biome_replace,
/area/lavaland/surface/outdoors)
(1,1,1) = {"
diff --git a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
index 108f84c6ad6f..d8ca28b7b004 100644
--- a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
+++ b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
@@ -7042,7 +7042,7 @@
/obj/machinery/status_display/supply{
pixel_y = 32
},
-/obj/machinery/disposal/delivery_chute{
+/obj/structure/disposaloutlet{
dir = 4
},
/obj/structure/window/spawner/directional/south,
@@ -66111,10 +66111,6 @@
},
/turf/open/floor/iron/dark/textured_large,
/area/station/medical/medbay/central)
-"rDG" = (
-/obj/effect/turf_decal/tile/red,
-/turf/closed/wall/r_wall,
-/area/station/engineering/lobby)
"rDN" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
@@ -197861,7 +197857,7 @@ weC
lDI
kOk
kOk
-rDG
+kOk
kOk
kOk
mAH
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index c2482fe215de..6fb2cc5a05d5 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -17522,7 +17522,7 @@
/turf/open/floor/iron/white,
/area/station/medical/surgery/aft)
"eUO" = (
-/mob/living/simple_animal/hostile/asteroid/polarbear{
+/mob/living/basic/mining/polarbear{
move_force = 999;
name = "Dewey"
},
@@ -23565,7 +23565,7 @@
/turf/open/floor/iron/white,
/area/station/medical/treatment_center)
"gHA" = (
-/mob/living/simple_animal/hostile/asteroid/polarbear{
+/mob/living/basic/mining/polarbear{
move_force = 999;
name = "Louie"
},
@@ -56852,7 +56852,7 @@
/turf/open/floor/plating,
/area/station/maintenance/port/aft)
"pZY" = (
-/mob/living/simple_animal/hostile/asteroid/polarbear{
+/mob/living/basic/mining/polarbear{
move_force = 999;
name = "Huey"
},
diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm
index 660a4e3c82c7..21ed0c47aa18 100644
--- a/code/__DEFINES/ai/ai_blackboard.dm
+++ b/code/__DEFINES/ai/ai_blackboard.dm
@@ -10,6 +10,8 @@
#define BB_FOOD_TARGET "bb_food_target"
///How close a mob must be for us to select it as a target, if that is less than how far we can maintain it as a target
#define BB_AGGRO_RANGE "BB_aggro_range"
+///If defined, mobs will use this distance instead of default aggro range to locate new targets
+#define BB_AGGRO_GRAB_RANGE "BB_aggro_grab_range"
///are we hungry? determined by the udder component
#define BB_CHECK_HUNGRY "BB_check_hungry"
///are we ready to breed?
@@ -241,3 +243,8 @@
// Used to hold state without making bigass lists
/// For /datum/ai_behavior/find_potential_targets, what if any field are we using currently
#define BB_FIND_TARGETS_FIELD(type) "bb_find_targets_field_[type]"
+
+///Currently enraged
+#define BB_BASIC_MOB_ENRAGE "BB_enraged"
+///Previous melee cooldown
+#define BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN "BB_previous_melee_cooldown"
diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index 01bce2073f6c..81c37f60afa5 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -108,3 +108,8 @@
/// Global signal whenever a camera network broadcast is started/stopped/updated: (camera_net, is_show_active, announcement)
#define COMSIG_GLOB_NETWORK_BROADCAST_UPDATED "!network_broadcast_updated"
+///Global signal sent when the player list grows. Called by [mob/add_to_player_list] (mob/player)
+#define COMSIG_GLOB_PLAYER_LOGIN "!player_login"
+
+///Global signal sent when the player list shrinks. Called by [mob/remove_from_player_list] (mob/player)
+#define COMSIG_GLOB_PLAYER_LOGOUT "!player_logout"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index b44d42ceedec..aac692d55ca8 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -36,6 +36,8 @@
#define DOORCRUSH_NO_WOUND (1<<0)
///from base of mob/living/resist() (/mob/living)
#define COMSIG_LIVING_RESIST "living_resist"
+ // Block execute_resist()
+ #define COMPONENT_BLOCK_RESIST (1<<0)
///from base of mob/living/ignite_mob() (/mob/living)
#define COMSIG_LIVING_IGNITED "living_ignite"
///from base of mob/living/extinguish_mob() (/mob/living)
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
index 86381d4ec22f..5fab93418bb7 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
@@ -8,9 +8,6 @@
#define COMSIG_BORG_HUG_HANDLED 1
///called from /mob/living/silicon/attack_hand proc
#define COMSIG_MOB_PAT_BORG "mob_pat_borg"
-///called when an AI (malf or perhaps combat upgraded or some other circumstance that has them inhabit
-///an APC) enters an APC
-#define COMSIG_SILICON_AI_OCCUPY_APC "AI_occupy_apc"
///called when an AI vacates an APC
#define COMSIG_SILICON_AI_VACATE_APC "AI_vacate_apc"
///called when an AI's control is toggled
diff --git a/code/__DEFINES/icon_smoothing.dm b/code/__DEFINES/icon_smoothing.dm
index bdc6be75782c..42e539ed6a81 100644
--- a/code/__DEFINES/icon_smoothing.dm
+++ b/code/__DEFINES/icon_smoothing.dm
@@ -129,23 +129,24 @@ DEFINE_BITFIELD(smoothing_junction, list(
#define SMOOTH_GROUP_CARPET_SIMPLE_NEON_PINK_NODOTS S_TURF(51) //![turf/open/floor/carpet/neon/simple/pink/nodots]
#define SMOOTH_GROUP_BAMBOO_FLOOR S_TURF(52) //![/turf/open/floor/bamboo]
#define SMOOTH_GROUP_FLOOR_WATER_LAVALAND S_TURF(53) //![/turf/open/water/lavaland_atmos/basalt]
-#define SMOOTH_GROUP_FLOOR_SIDERITE S_TURF(54) ///turf/open/misc/grass
-#define SMOOTH_GROUP_FLOOR_SHALE S_TURF(55) ///turf/open/misc/grass
-
-#define SMOOTH_GROUP_CLOSED_TURFS S_TURF(56) ///turf/closed
-#define SMOOTH_GROUP_MATERIAL_WALLS S_TURF(57) ///turf/closed/wall/material
-#define SMOOTH_GROUP_SYNDICATE_WALLS S_TURF(58) ///turf/closed/wall/r_wall/plastitanium/syndicate, /turf/closed/indestructible/syndicate
-#define SMOOTH_GROUP_HOTEL_WALLS S_TURF(59) ///turf/closed/indestructible/hotelwall
-#define SMOOTH_GROUP_MINERAL_WALLS S_TURF(60) ///turf/closed/mineral, /turf/closed/indestructible
-#define SMOOTH_GROUP_RED_ROCK_WALLS S_TURF(61) ///turf/closed/mineral/asteroid, /turf/closed/mineral/random/stationside/asteroid
-#define SMOOTH_GROUP_SHALE_WALLS S_TURF(62) ///turf/closed/mineral/random/volcanic/shale
-#define SMOOTH_GROUP_BOSS_WALLS S_TURF(63) ///turf/closed/indestructible/riveted/boss
-#define SMOOTH_GROUP_SURVIVAL_TITANIUM_WALLS S_TURF(64) ///turf/closed/wall/mineral/titanium/survival
-#define SMOOTH_GROUP_TURF_OPEN_CLIFF S_TURF(65) ///turf/open/cliff
-#define SMOOTH_GROUP_HIEROPHANT S_TURF(66) ///turf/closed/indestructible/riveted/hierophant
-#define SMOOTH_GROUP_PLASTINUM_WALLS S_TURF(67) ///turf/closed/indestructible/riveted/plastinum
-
-#define MAX_S_TURF 67 //Always match this value with the one above it.
+#define SMOOTH_GROUP_FLOOR_SIDERITE S_TURF(54) //![/turf/open/misc/asteroid/basalt/smooth/siderite]
+#define SMOOTH_GROUP_FLOOR_SHALE S_TURF(55) //![/turf/open/misc/asteroid/basalt/smooth/shale]
+#define SMOOTH_GROUP_FLOOR_BASALT S_TURF(56) //![/turf/open/misc/asteroid/basalt]
+
+#define SMOOTH_GROUP_CLOSED_TURFS S_TURF(57) ///turf/closed
+#define SMOOTH_GROUP_MATERIAL_WALLS S_TURF(58) ///turf/closed/wall/material
+#define SMOOTH_GROUP_SYNDICATE_WALLS S_TURF(59) ///turf/closed/wall/r_wall/plastitanium/syndicate, /turf/closed/indestructible/syndicate
+#define SMOOTH_GROUP_HOTEL_WALLS S_TURF(60) ///turf/closed/indestructible/hotelwall
+#define SMOOTH_GROUP_MINERAL_WALLS S_TURF(61) ///turf/closed/mineral, /turf/closed/indestructible
+#define SMOOTH_GROUP_RED_ROCK_WALLS S_TURF(62) ///turf/closed/mineral/asteroid, /turf/closed/mineral/random/stationside/asteroid
+#define SMOOTH_GROUP_SHALE_WALLS S_TURF(63) ///turf/closed/mineral/random/volcanic/shale
+#define SMOOTH_GROUP_BOSS_WALLS S_TURF(64) ///turf/closed/indestructible/riveted/boss
+#define SMOOTH_GROUP_SURVIVAL_TITANIUM_WALLS S_TURF(65) ///turf/closed/wall/mineral/titanium/survival
+#define SMOOTH_GROUP_TURF_OPEN_CLIFF S_TURF(66) ///turf/open/cliff
+#define SMOOTH_GROUP_HIEROPHANT S_TURF(67) ///turf/closed/indestructible/riveted/hierophant
+#define SMOOTH_GROUP_PLASTINUM_WALLS S_TURF(68) ///turf/closed/indestructible/riveted/plastinum
+
+#define MAX_S_TURF 68 //Always match this value with the one above it.
#define S_OBJ(num) ("-" + #num + ",")
/* /obj included */
diff --git a/code/__DEFINES/keybinding.dm b/code/__DEFINES/keybinding.dm
index 73da7d1924c0..91ce64a5d4bb 100644
--- a/code/__DEFINES/keybinding.dm
+++ b/code/__DEFINES/keybinding.dm
@@ -63,6 +63,7 @@
#define COMSIG_KB_LIVING_HOLDTHROWMODE_DOWN "keybinding_living_holdthrowmode_down"
#define COMSIG_KB_LIVING_GIVEITEM_DOWN "keybinding_living_giveitem_down"
#define COMSIG_KB_LIVING_VIEW_PET_COMMANDS "keybinding_living_view_pet_commands"
+#define COMSIG_KB_LIVING_STOP_INTERACTIONS_DOWN "keybinding_living_stop_interactions_down"
//Mob
#define COMSIG_KB_MOB_FACENORTH_DOWN "keybinding_mob_facenorth_down"
diff --git a/code/__DEFINES/move_force.dm b/code/__DEFINES/move_force.dm
index d291e95579d3..64d1a8f1f1ba 100644
--- a/code/__DEFINES/move_force.dm
+++ b/code/__DEFINES/move_force.dm
@@ -18,3 +18,4 @@
#define MOVE_FORCE_WEAK (MOVE_FORCE_DEFAULT / 2)
#define MOVE_FORCE_VERY_WEAK ((MOVE_FORCE_DEFAULT / MOVE_FORCE_CRUSH_RATIO) + 1)
#define MOVE_FORCE_EXTREMELY_WEAK (MOVE_FORCE_DEFAULT / (MOVE_FORCE_CRUSH_RATIO * 3))
+#define MOVE_FORCE_NONE 0
diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm
index 877d06ed19f6..1fb72f050ed3 100644
--- a/code/__DEFINES/rust_g.dm
+++ b/code/__DEFINES/rust_g.dm
@@ -95,6 +95,32 @@
*/
#define rustg_acreplace_with_replacements(key, text, replacements) RUSTG_CALL(RUST_G, "acreplace_with_replacements")(key, text, json_encode(replacements))
+/**
+ * Generates a procedural dungeon map using BSP tree partitioning, prefab placement,
+ * MST corridor generation, and Cellular Automata smoothing.
+ *
+ * Returns a plain binary grid string (matching cellularnoise format):
+ * A width*height string of '0' (wall) and '1' (floor) characters in row-major order.
+ *
+ * Arguments:
+ * * width - Grid width
+ * * height - Grid height
+ * * prefabs_json - JSON array of prefab configs: [{"x":55,"y":65,"w":10,"h":10,"isEnclosed":true},...] (if none use "[]") x = bottom-left turf x, y = bottom-left turf y, w = prefab width, h = prefab height, isEnclosed = whether prefab should be treated like its wall or floor by the generation
+ * * min_bsp_size - Minimum BSP leaf dimension
+ * * max_ratio - Maximum aspect ratio for BSP splits
+ * * padding - Room edge padding within BSP leaf
+ * * room_fill_percent - How much of each BSP leaf a room fills, 1-100
+ * * corridor_width - Width of corridors between rooms
+ * * loop_percent - Chance to add extra MST edges for loops
+ * * noise_percent - Initial random floor density
+ * * ca_steps - Cellular Automata smoothing iterations
+ * * birth_limit - Neighbors to create floor (>=)
+ * * survival_limit - Neighbors to survive as floor (>=)
+ * * edge_is_alive - Whether out-of-bounds cells count as ALIVE (floor) for CA neighbor counts
+ */
+#define rustg_cave_system_generator_generate(width, height, prefabs_json, min_bsp_size, max_ratio, padding, room_fill_percent, corridor_width, loop_percent, noise_percent, ca_steps, birth_limit, survival_limit, edge_is_alive) \
+ RUSTG_CALL(RUST_G, "cave_system_generator_generate")(width, height, prefabs_json, min_bsp_size, max_ratio, padding, room_fill_percent, corridor_width, loop_percent, noise_percent, ca_steps, birth_limit, survival_limit, edge_is_alive)
+
/**
* This proc generates a cellular automata noise grid which can be used in procedural generation methods.
*
@@ -203,21 +229,70 @@
#define rustg_hash_string(algorithm, text) RUSTG_CALL(RUST_G, "hash_string")(algorithm, text)
#define rustg_hash_file(algorithm, fname) RUSTG_CALL(RUST_G, "hash_file")(algorithm, fname)
-#define rustg_hash_generate_totp(seed) RUSTG_CALL(RUST_G, "generate_totp")(seed)
-#define rustg_hash_generate_totp_tolerance(seed, tolerance) RUSTG_CALL(RUST_G, "generate_totp_tolerance")(seed, tolerance)
+
+/// Supported algorithms: RUSTG_HASH_SHA1, RUSTG_HASH_SHA256, RUSTG_HASH_SHA512
+/// Seed must be between 10 bytes to 64 bytes (padded or unpadded) of base32. 20 bytes is recommended. Use a CSPRNG.
+/// Refresh rate is fixed at 30sec and digit count is fixed at 6
+#define rustg_hash_generate_totp(algorithm, seed) RUSTG_CALL(RUST_G, "generate_totp")(algorithm, seed)
+/// Supported algorithms: RUSTG_HASH_SHA1, RUSTG_HASH_SHA256, RUSTG_HASH_SHA512
+/// Seed must be between 10 bytes to 64 bytes (padded or unpadded) of base32. 20 bytes is recommended. Use a CSPRNG.
+/// Refresh rate is fixed at 30sec and digit count is fixed at 6
+/// Tolerance is the number of codes +-30sec from the current one that are allowed.
+#define rustg_hash_generate_totp_tolerance(algorithm, seed, tolerance) RUSTG_CALL(RUST_G, "generate_totp_tolerance")(algorithm, seed, tolerance)
+
+/// Creates a cryptographically-secure pseudorandom number generator using the OS-level PRNG as a seed
+/// n_bytes is the number of bytes provided to the RNG, the length of the string output varies by format
+/// The output string length and characters contained in each format is as follows:
+/// RUSTG_RNG_FORMAT_HEX: n_bytes * 2, [a-z0-9]
+/// RUSTG_RNG_FORMAT_ALPHANUMERIC: n_bytes, [A-Za-z0-9]
+/// RUSTG_RNG_FORMAT_BASE32: ceil(n_bytes / 5 * 8) [A-Z2-7]
+/// RUSTG_RNG_FORMAT_BASE32_PADDED: ceil(n_bytes / 5) * 8 [A-Z2-7=]
+/// RUSTG_RNG_FORMAT_BASE64: 4 * ceil(n_bytes/3), [A-Za-z0-9+/=]
+/// Outputs "ERROR: [reason]" if the format string provided is invalid, or n_bytes is not a positive non-zero integer
+#define rustg_csprng_chacha20(format, n_bytes) RUSTG_CALL(RUST_G, "csprng_chacha20")(format, "[n_bytes]")
+
+/// Creates a seeded pseudorandom number generator using the SHA256 hash output bytes of the seed string
+/// Note that this function is NOT suitable for use in cryptography and is intended for high-quality **predictable** RNG
+/// Use rustg_csprng_chacha20 for a cryptographically-secure PRNG.
+/// n_bytes is the number of bytes provided to the RNG, the length of the string output varies by format
+/// The output string length and characters contained in each format is as follows:
+/// RUSTG_RNG_FORMAT_HEX: n_bytes * 2, [a-z0-9]
+/// RUSTG_RNG_FORMAT_ALPHANUMERIC: n_bytes, [A-Za-z0-9]
+/// RUSTG_RNG_FORMAT_BASE32: ceil(n_bytes / 5 * 8) [A-Z2-7]
+/// RUSTG_RNG_FORMAT_BASE32_PADDED: ceil(n_bytes / 5) * 8 [A-Z2-7=]
+/// RUSTG_RNG_FORMAT_BASE64: 4 * ceil(n_bytes/3), [A-Za-z0-9+/=]
+/// Outputs "ERROR: [reason]" if the format string provided is invalid, or n_bytes is not a positive non-zero integer
+#define rustg_prng_chacha20_seeded(format, n_bytes, seed) RUSTG_CALL(RUST_G, "prng_chacha20_seeded")(format, "[n_bytes]", seed)
+
+#define RUSTG_RNG_FORMAT_HEX "hex"
+#define RUSTG_RNG_FORMAT_ALPHANUMERIC "alphanumeric"
+#define RUSTG_RNG_FORMAT_BASE32 "base32_rfc4648"
+#define RUSTG_RNG_FORMAT_BASE32_PADDED "base32_rfc4648_pad"
+#define RUSTG_RNG_FORMAT_BASE64 "base64"
#define RUSTG_HASH_MD5 "md5"
#define RUSTG_HASH_SHA1 "sha1"
#define RUSTG_HASH_SHA256 "sha256"
#define RUSTG_HASH_SHA512 "sha512"
#define RUSTG_HASH_XXH64 "xxh64"
+#define RUSTG_HASH_BASE32 "base32_rfc4648"
+#define RUSTG_HASH_BASE32_PADDED "base32_rfc4648_pad"
#define RUSTG_HASH_BASE64 "base64"
/// Encode a given string into base64
#define rustg_encode_base64(str) rustg_hash_string(RUSTG_HASH_BASE64, str)
-/// Decode a given base64 string
+/// Decode a given base64 string. This expects padding.
+/// Returns a blank string if the string is not valid base64.
#define rustg_decode_base64(str) RUSTG_CALL(RUST_G, "decode_base64")(str)
+/// Encode a given string into base32 (RFC4648)
+/// If padding set to FALSE, will not output padding characters.
+#define rustg_encode_base32(str, padding) rustg_hash_string(padding ? RUSTG_HASH_BASE32_PADDED : RUSTG_HASH_BASE32, str)
+/// Decode a given base32 (RFC4648) string
+/// If padding set to FALSE, decoding will not support padding characters.
+/// Returns a blank string if the string is not valid base32.
+#define rustg_decode_base32(str, padding) RUSTG_CALL(RUST_G, "decode_base32")(str, "[padding ? 1 : 0]")
+
#ifdef RUSTG_OVERRIDE_BUILTINS
#define md5(thing) (isfile(thing) ? rustg_hash_file(RUSTG_HASH_MD5, "[thing]") : rustg_hash_string(RUSTG_HASH_MD5, thing))
#endif
@@ -501,3 +576,19 @@
#define url_decode(text) rustg_url_decode(text)
#endif
+/// Generates a version 4 UUID.
+/// See https://www.ietf.org/rfc/rfc9562.html#section-5.4 for specifics on version 4 UUIDs.
+#define rustg_generate_uuid_v4(...) RUSTG_CALL(RUST_G, "uuid_v4")()
+
+/// Generates a version 7 UUID, with the current time.
+/// See https://www.ietf.org/rfc/rfc9562.html#section-5.7 for specifics on version 7 UUIDs.
+#define rustg_generate_uuid_v7(...) RUSTG_CALL(RUST_G, "uuid_v7")()
+
+/// Generates a random version 2 CUID.
+/// See https://github.com/paralleldrive/cuid2 for specifics on version 2 CUIDs.
+#define rustg_generate_cuid2(...) RUSTG_CALL(RUST_G, "cuid2")()
+
+/// Generates a random version 2 CUID with the given length.
+/// See https://github.com/paralleldrive/cuid2 for specifics on version 2 CUIDs.
+#define rustg_generate_cuid2_length(length) RUSTG_CALL(RUST_G, "cuid2_len")("[length]")
+
diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm
index a1babe0ca797..d75aeee503fb 100644
--- a/code/__DEFINES/sound.dm
+++ b/code/__DEFINES/sound.dm
@@ -67,9 +67,9 @@
///The range deducted from sound range for things that are considered silent / sneaky
#define SILENCED_SOUND_EXTRARANGE -11
///Percentage of sound's range where no falloff is applied
-#define SOUND_DEFAULT_FALLOFF_DISTANCE 1 //For a normal sound this would be 1 tile of no falloff
+#define SOUND_DEFAULT_FALLOFF_DISTANCE 0 //Disabled because it doesn't actually have a nice effect, it just makes the jump to fall-off more shocking. maybe delete
///The default exponent of sound falloff
-#define SOUND_FALLOFF_EXPONENT 6
+#define SOUND_FALLOFF_EXPONENT 2.5
#define SOUND_MINIMUM_PRESSURE 10
diff --git a/code/__HELPERS/_dreamluau.dm b/code/__HELPERS/_dreamluau.dm
index 9436b95c5519..ab5e29eb34a7 100644
--- a/code/__HELPERS/_dreamluau.dm
+++ b/code/__HELPERS/_dreamluau.dm
@@ -294,10 +294,10 @@
* Hard deleting an object without clearing userdata corresponding to it leaves the userdata to become associated with
* the next DM object to receive the old object's reference ID, which may be undesirable behavior.
*
- * @param object the object to disassociate from userdata.
+ * @param args the objects to disassociate from userdata.
*
* @return null on success
*/
-#define DREAMLUAU_CLEAR_REF_USERDATA(object) DREAMLUAU_CALL(clear_ref_userdata)((object))
+#define DREAMLUAU_CLEAR_REF_USERDATA(args...) DREAMLUAU_CALL(clear_ref_userdata)(##args)
#endif
diff --git a/code/__HELPERS/icon_smoothing.dm b/code/__HELPERS/icon_smoothing.dm
index 733fd9e4f721..5101b02d31e3 100644
--- a/code/__HELPERS/icon_smoothing.dm
+++ b/code/__HELPERS/icon_smoothing.dm
@@ -378,6 +378,18 @@ xxx xxx xxx
handback |= WEST_JUNCTION | NORTHWEST_JUNCTION | SOUTHWEST_JUNCTION
return handback
+/// Takes a direction, turns it into all the junctions that it lines up with
+/proc/all_junctions_of_dir(dir)
+ if(dir == NORTH)
+ return NORTH_JUNCTION | NORTHEAST_JUNCTION | NORTHWEST_JUNCTION
+ if(dir == SOUTH)
+ return SOUTH_JUNCTION | SOUTHEAST_JUNCTION | SOUTHWEST_JUNCTION
+ if(dir == EAST)
+ return EAST_JUNCTION | SOUTHEAST_JUNCTION | NORTHEAST_JUNCTION
+ if(dir == WEST)
+ return WEST_JUNCTION | NORTHWEST_JUNCTION | SOUTHWEST_JUNCTION
+ return NONE
+
/proc/dir_to_junction(dir)
switch(dir)
if(NORTH)
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index cb1c70d04e3a..4afb312193c2 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -130,6 +130,12 @@
#define DISABLE_DREAMLUAU
#endif
+// Since 0.2.0, dreamluau depends on breaking changes made to byondapi in 1674.
+// Get rid of this when BYOND_MINOR is >= 1674 AND we don't have any alternate tests on <1674.
+#if DM_BUILD < 1674
+#define DISABLE_DREAMLUAU
+#endif
+
/// If this is uncommented, force our verb processing into just the 2% of a tick
/// We normally reserve for it
/// NEVER run this on live, it's for simulating highpop only
diff --git a/code/_onclick/click_ctrl.dm b/code/_onclick/click_ctrl.dm
index 19c86970aeee..e80f8fa05cb9 100644
--- a/code/_onclick/click_ctrl.dm
+++ b/code/_onclick/click_ctrl.dm
@@ -41,7 +41,7 @@
if(grab(target) != GRAB_SKIP)
changeNext_move(CLICK_CD_MELEE)
return
- start_pulling(target)
+ pulled(target)
/**
* Ctrl mouse wheel click
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index d90ffb54507e..92f7acb42598 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -73,7 +73,7 @@
return
if(W == A)
- W.attack_self(src)
+ W.attack_self(src, modifiers)
return
// cyborgs are prohibited from using storage items so we can I think safely remove (A.loc in contents)
diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
index 71a13ce43500..8631eaf46709 100644
--- a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
+++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm
@@ -1032,7 +1032,7 @@
candidate_role = "Slaughter Demon"
// preview_antag_datum = /datum/antagonist/slaughter // Doesn't actually have its own pref
midround_type = HEAVY_MIDROUND
- jobban_flag = ROLE_ALIEN
+ jobban_flag = ROLE_SENTIENCE
ruleset_flags = RULESET_INVADER
weight = 0
min_pop = 20
diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm
index ae01679b0943..efd23d83163c 100644
--- a/code/controllers/subsystem/garbage.dm
+++ b/code/controllers/subsystem/garbage.dm
@@ -374,7 +374,7 @@ SUBSYSTEM_DEF(garbage)
var/start_time = world.time
var/start_tick = world.tick_usage
SEND_SIGNAL(to_delete, COMSIG_QDELETING, force) // Let the (remaining) components know about the result of Destroy
- var/hint = to_delete.Destroy(force) // Let our friend know they're about to get fucked up.
+ var/hint = UNLINT(to_delete.Destroy(force)) // Let our friend know they're about to get fucked up.
if(world.time != start_time)
trash.slept_destroy++
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 8d98ff05bd5c..7b1b55341c51 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -17,6 +17,9 @@ SUBSYSTEM_DEF(mapping)
var/list/ruins_templates = list()
+ ///Assoc list of all ruins spawned, key center of ruin spawn -> value ruin instance
+ var/list/active_ruins = alist()
+
///List of ruins, separated by their theme
var/list/themed_ruins = list()
diff --git a/code/controllers/subsystem/processing/instruments.dm b/code/controllers/subsystem/processing/instruments.dm
index 197c16047b7f..a68a8ff77a61 100644
--- a/code/controllers/subsystem/processing/instruments.dm
+++ b/code/controllers/subsystem/processing/instruments.dm
@@ -52,6 +52,6 @@ PROCESSING_SUBSYSTEM_DEF(instruments)
/datum/controller/subsystem/processing/instruments/proc/reserve_instrument_channel(datum/instrument/I)
if(current_instrument_channels > max_instrument_channels)
return
- . = SSsounds.reserve_sound_channel(I)
+ . = SSsounds.reserve_sound_channel_for_datum(I)
if(!isnull(.))
current_instrument_channels++
diff --git a/code/controllers/subsystem/sound_tokens.dm b/code/controllers/subsystem/sound_tokens.dm
new file mode 100644
index 000000000000..dc4bd1600c38
--- /dev/null
+++ b/code/controllers/subsystem/sound_tokens.dm
@@ -0,0 +1,24 @@
+SUBSYSTEM_DEF(sound_tokens)
+ name = "Sound Tokens"
+ wait = 1
+ ss_flags = SS_TICKER | SS_BACKGROUND | SS_NO_INIT
+
+
+ var/list/clients_needing_update = list()
+ var/list/currentrun = list()
+
+/datum/controller/subsystem/sound_tokens/fire(resumed)
+ if(!resumed)
+ currentrun = clients_needing_update
+ clients_needing_update = list()
+ while(length(currentrun))
+ var/client/client = currentrun[currentrun.len]
+ currentrun.len--
+ var/mob/owned_mob = client.mob
+ if(!owned_mob)
+ continue
+ for(var/datum/sound_token/token in client.sound_tokens)
+ token.update_listener(owned_mob)
+ if(MC_TICK_CHECK)
+ break
+
diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm
index db3f94a080bd..360f8a883e53 100644
--- a/code/controllers/subsystem/sounds.dm
+++ b/code/controllers/subsystem/sounds.dm
@@ -114,10 +114,10 @@ SUBSYSTEM_DEF(sounds)
var/text_channel = num2text(channel)
var/using = using_channels[text_channel]
using_channels -= text_channel
- if(using != TRUE) // datum channel
+ if(using != DATUMLESS) // datum channel
using_channels_by_datum[using] -= channel
if(!length(using_channels_by_datum[using]))
- using_channels_by_datum -= using
+ stop_tracking_datum(using)
free_channel(channel)
/// Frees all the channels a datum is using.
@@ -128,14 +128,14 @@ SUBSYSTEM_DEF(sounds)
for(var/channel in L)
using_channels -= num2text(channel)
free_channel(channel)
- using_channels_by_datum -= D
+ stop_tracking_datum(D)
/// Frees all datumless channels
/datum/controller/subsystem/sounds/proc/free_datumless_channels()
free_datum_channels(DATUMLESS)
-/// NO AUTOMATIC CLEANUP - If you use this, you better manually free it later! Returns an integer for channel.
-/datum/controller/subsystem/sounds/proc/reserve_sound_channel_datumless()
+/// Reserve a sound channel. Free it later with free_sound_channel()
+/datum/controller/subsystem/sounds/proc/reserve_sound_channel()
. = reserve_channel()
if(!.) //oh no..
return FALSE
@@ -145,7 +145,7 @@ SUBSYSTEM_DEF(sounds)
using_channels_by_datum[DATUMLESS] += .
/// Reserves a channel for a datum. Automatic cleanup only when the datum is deleted. Returns an integer for channel.
-/datum/controller/subsystem/sounds/proc/reserve_sound_channel(datum/D)
+/datum/controller/subsystem/sounds/proc/reserve_sound_channel_for_datum(datum/D)
if(!D) //i don't like typechecks but someone will fuck it up
CRASH("Attempted to reserve sound channel without datum using the managed proc.")
.= reserve_channel()
@@ -156,6 +156,8 @@ SUBSYSTEM_DEF(sounds)
LAZYINITLIST(using_channels_by_datum[D])
using_channels_by_datum[D] += .
+ RegisterSignal(D, COMSIG_QDELETING, PROC_REF(tracked_datum_deleted))
+
/**
* Reserves a channel and updates the datastructure. Private proc.
*/
@@ -260,4 +262,19 @@ SUBSYSTEM_DEF(sounds)
if(!isnull(sfx.key))
GLOB.sfx_datum_by_key[sfx.key] = new sfx()
+
+///Call to free all channels reserved by a datum.
+/datum/controller/subsystem/sounds/proc/stop_tracking_datum(datum/D)
+ PRIVATE_PROC(TRUE)
+
+ using_channels_by_datum -= D
+ UnregisterSignal(D, COMSIG_QDELETING)
+
+/// Handles a tracked datum being deleted, automatically freeing the channels.
+/datum/controller/subsystem/sounds/proc/tracked_datum_deleted(datum/source)
+ SIGNAL_HANDLER
+ PRIVATE_PROC(TRUE)
+
+ free_datum_channels(source)
+
#undef DATUMLESS
diff --git a/code/datums/actions/mobs/charge.dm b/code/datums/actions/mobs/charge.dm
index 4dc016a66e67..dd80650065e0 100644
--- a/code/datums/actions/mobs/charge.dm
+++ b/code/datums/actions/mobs/charge.dm
@@ -212,18 +212,21 @@
if(!isliving(target))
source.visible_message(span_danger("[source] smashes into [target]!"))
- living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
+ if (recoil_duration >= 0) // Because 0 stun/knockdown is still a valid value
+ living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
return
var/mob/living/living_target = target
if(ishuman(living_target))
var/mob/living/carbon/human/human_target = living_target
if(human_target.check_block(source, 0, "\the [source]", attack_type = LEAP_ATTACK) && living_source)
- living_source.Stun(recoil_duration, ignore_canstun = TRUE)
+ if (recoil_duration >= 0)
+ living_source.Stun(recoil_duration, ignore_canstun = TRUE)
return
living_target.visible_message(span_danger("[source] charges into [living_target]!"), span_userdanger("[source] charges into you!"))
- living_target.Knockdown(knockdown_duration)
+ if (knockdown_duration >= 0)
+ living_target.Knockdown(knockdown_duration)
/datum/status_effect/tired_post_charge
id = "tired_post_charge"
diff --git a/code/datums/actions/mobs/dash.dm b/code/datums/actions/mobs/dash.dm
index ad87ab93f9a7..36db85531c1e 100644
--- a/code/datums/actions/mobs/dash.dm
+++ b/code/datums/actions/mobs/dash.dm
@@ -6,8 +6,6 @@
cooldown_time = 1.5 SECONDS
/// The range of the dash
var/dash_range = 4
- /// The distance you will be from the target after you dash
- var/pick_range = 5
/datum/action/cooldown/mob_cooldown/dash/Activate(atom/target_atom)
disable_cooldown_actions()
diff --git a/code/datums/actions/mobs/projectileattack.dm b/code/datums/actions/mobs/projectileattack.dm
index 183df71e03f1..bdbea403e2fe 100644
--- a/code/datums/actions/mobs/projectileattack.dm
+++ b/code/datums/actions/mobs/projectileattack.dm
@@ -78,7 +78,7 @@
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/attack_sequence(mob/living/firer, atom/target)
for(var/i in 1 to shot_count)
shoot_projectile(firer, target, null, firer, rand(-default_projectile_spread, default_projectile_spread), null)
- SLEEP_CHECK_DEATH(shot_delay, src)
+ SLEEP_CHECK_DEATH(shot_delay, firer)
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/direct
shot_count = 40
diff --git a/code/datums/actions/mobs/sequences/dash_attack.dm b/code/datums/actions/mobs/sequences/dash_attack.dm
index b5fff4cb4922..42486f8d9f37 100644
--- a/code/datums/actions/mobs/sequences/dash_attack.dm
+++ b/code/datums/actions/mobs/sequences/dash_attack.dm
@@ -6,6 +6,12 @@
cooldown_time = 3 SECONDS
shared_cooldown = MOB_SHARED_COOLDOWN_2
sequence_actions = list(
- /datum/action/cooldown/mob_cooldown/dash = 0.1 SECONDS,
- /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator = 0,
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = 0.22 SECONDS, // 0.1s windup + 0.12s max dash
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator = 0,
+ )
+
+/datum/action/cooldown/mob_cooldown/dash_attack/long_burst
+ sequence_actions = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = 0.22 SECONDS,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst = 0,
)
diff --git a/code/datums/actions/mobs/transform_weapon.dm b/code/datums/actions/mobs/transform_weapon.dm
deleted file mode 100644
index 6cb97e0c635c..000000000000
--- a/code/datums/actions/mobs/transform_weapon.dm
+++ /dev/null
@@ -1,26 +0,0 @@
-/datum/action/cooldown/mob_cooldown/transform_weapon
- name = "Transform Weapon"
- button_icon = 'icons/obj/mining_zones/artefacts.dmi'
- button_icon_state = "cleaving_saw"
- desc = "Transform weapon into a different state."
- cooldown_time = 5 SECONDS
- shared_cooldown = MOB_SHARED_COOLDOWN_2
- /// The max possible cooldown, cooldown is random between the default cooldown time and this
- var/max_cooldown_time = 10 SECONDS
-
-/datum/action/cooldown/mob_cooldown/transform_weapon/Activate(atom/target_atom)
- disable_cooldown_actions()
- do_transform()
- StartCooldown(rand(cooldown_time, max_cooldown_time), 0)
- enable_cooldown_actions()
- return TRUE
-
-/datum/action/cooldown/mob_cooldown/transform_weapon/proc/do_transform()
- if(!istype(owner, /mob/living/basic/boss/blood_drunk_miner))
- return
- var/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner = owner
- blood_drunk_miner.miner_saw.attack_self(owner)
- var/saw_open = HAS_TRAIT(blood_drunk_miner.miner_saw, TRAIT_TRANSFORM_ACTIVE)
- blood_drunk_miner.rapid_melee_hits = saw_open ? 3 : 5
- blood_drunk_miner.icon_state = "miner[saw_open ? "_transformed":""]"
- blood_drunk_miner.icon_living = "miner[saw_open ? "_transformed":""]"
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 b2aaaa235788..ce46d4c1ab98 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
var/vision_range = 9
/// Blackboard key for aggro range, uses vision range if not specified
var/aggro_range_key = BB_AGGRO_RANGE
+ /// Range in which we can acquire a new target
+ var/aggro_grab_range_key = BB_AGGRO_GRAB_RANGE
/// Blackboard key for the target priority strategy
var/priority_strategy_key = BB_TARGET_PRIORITY_STRATEGY
/// If we have a priority strategy set, how often do we refresh our target search?
@@ -34,7 +36,12 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
if((!priority_strategy || controller.blackboard[BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN] > world.time) && current_target && targeting_strategy.can_attack(living_mob, current_target, vision_range))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
- var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+ var/aggro_range = vision_range
+ if(isnull(current_target) && !isnull(controller.blackboard[aggro_grab_range_key]))
+ aggro_range = controller.blackboard[aggro_grab_range_key]
+ else if(!isnull(controller.blackboard[aggro_range_key]))
+ aggro_range = controller.blackboard[aggro_range_key]
+
controller.clear_blackboard_key(target_key)
// If we're using a field rn, just don't do anything yeah?
@@ -87,7 +94,12 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
- var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+ var/aggro_range = vision_range
+ if(!isnull(controller.blackboard[aggro_grab_range_key]))
+ aggro_range = controller.blackboard[aggro_grab_range_key]
+ else if(!isnull(controller.blackboard[aggro_range_key]))
+ aggro_range = controller.blackboard[aggro_range_key]
+
// takes the larger between our range() input and our implicit hearers() input (world.view)
aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2))
// Alright, here's the interesting bit
@@ -187,6 +199,3 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
if(length(priority_targets))
return ..(controller, priority_targets)
return ..()
-
-/datum/ai_behavior/find_potential_targets/bigger_range
- vision_range = 16
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm b/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm
new file mode 100644
index 000000000000..cf3a922a5a44
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/enrage.dm
@@ -0,0 +1,44 @@
+// Performs the enrage behavior when health is below given threshold, and calm down behavior if above that value afterwards
+/datum/ai_planning_subtree/enrage
+ var/health_threshold = 0.5
+ var/enrage_behavior = /datum/ai_behavior/enrage
+
+/datum/ai_planning_subtree/enrage/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!isbasicmob(controller.pawn))
+ return
+ var/mob/living/basic/basic_pawn = controller.pawn
+ var/low_health = (basic_pawn.health / basic_pawn.maxHealth) <= health_threshold
+
+ var/is_enraged = controller.blackboard_key_exists(BB_BASIC_MOB_ENRAGE)
+ if(low_health && !is_enraged)
+ controller.queue_behavior(enrage_behavior, FALSE)
+ else if(!low_health && is_enraged)
+ controller.queue_behavior(enrage_behavior, TRUE)
+
+
+/// Cuts down basic mob's melee attack cooldown in half
+/datum/ai_behavior/enrage
+
+/datum/ai_behavior/enrage/perform(seconds_per_tick, datum/ai_controller/controller, calm_down)
+ var/mob/living/basic/basic_pawn = controller.pawn
+ if(calm_down)
+ var/previous_delay = controller.blackboard[BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN]
+ // Technically something else could have modified the cooldown before/after but that requires further consideration so don't use this behavior in these scenarios
+ basic_pawn.melee_attack_cooldown = previous_delay
+ controller.clear_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN)
+ controller.clear_blackboard_key(BB_BASIC_MOB_ENRAGE)
+ return AI_BEHAVIOR_SUCCEEDED
+
+ var/current_cooldown = basic_pawn.melee_attack_cooldown
+ var/new_attack_cooldown = current_cooldown / 2
+
+ controller.set_blackboard_key(BB_BASIC_MOB_ENRAGE, TRUE)
+ controller.set_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN, current_cooldown)
+ basic_pawn.melee_attack_cooldown = new_attack_cooldown
+
+ if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ var/current_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look at [current_target]!"))
+ else
+ controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look!"))
+ return AI_BEHAVIOR_SUCCEEDED
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 9824d41f8238..57fb22b4a956 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
@@ -22,9 +22,5 @@
/datum/ai_planning_subtree/simple_find_target/to_flee
target_key = BB_BASIC_MOB_FLEE_TARGET
-/datum/ai_planning_subtree/simple_find_target/increased_range
- 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/components/jetpack.dm b/code/datums/components/jetpack.dm
index 3ebc9da823f2..1ca91a9cb2ea 100644
--- a/code/datums/components/jetpack.dm
+++ b/code/datums/components/jetpack.dm
@@ -136,13 +136,22 @@
if(source.client.intended_direction && check_on_move.Invoke(TRUE)) //You use jet when press keys. yes.
trail?.generate_effect()
+/// Handles all active 0g movement, including both manual (trying to move a direction in 0g) and automatic (drifting idly in 0g)
/datum/component/jetpack/proc/stabilize(mob/source, movement_dir, continuous_move, backup)
SIGNAL_HANDLER
- if(!continuous_move && movement_dir)
- return COMSIG_MOVABLE_STOP_SPACEMOVE
- // Check if we have the fuel to stop this. Do NOT consume any fuel, just check
- // This is done because things other then us can use our fuel
- if(stabilize && check_on_move.Invoke(FALSE))
+ /*
+ * Checks if we should stop any active movement
+ *
+ * Obviously we stop all forms of drifting if we have stabilizers active (allowing free space movement)
+ * Less obviously, we stop need to stop drift if we are trying to move *while passively drifting*
+ * (Without checking the latter, jetpacks will act very weird and jank. As you move in one direction,
+ * you will simultaneously drift in that direction, causing you to jump/skip a tile every so often.)
+ *
+ * Either way, we need to check that we have the "fuel" to stop this
+ * DO NOT CONSUME FUEL HERE, just check if we have it
+ * This is done because things other then us can use our fuel
+ */
+ if((stabilize || (!continuous_move && movement_dir)) && check_on_move.Invoke(FALSE))
return COMSIG_MOVABLE_STOP_SPACEMOVE
return NONE
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index 39c0f9ab284c..0b46fe66554b 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -87,7 +87,7 @@
/**
* Parent types.
- *
+ *
* Abstract-ness is a meta-property of a class that is used to indicate
* that the class is intended to be used as a base class for others, and
* should not (or cannot) be instantiated.
@@ -123,6 +123,7 @@
* Returns [QDEL_HINT_QUEUE]
*/
/datum/proc/Destroy(force = FALSE)
+ PROTECTED_PROC(TRUE)
SHOULD_CALL_PARENT(TRUE)
SHOULD_NOT_SLEEP(TRUE)
tag = null
@@ -160,9 +161,16 @@
//END: ECS SHIT
#ifndef DISABLE_DREAMLUAU
+ var/list/to_remove = list()
+ if(ismovable(src))
+ var/atom/movable/src_movable = src
+ to_remove += list(src_movable.vis_contents, src_movable.vis_locs)
if(!(datum_flags & DF_STATIC_OBJECT))
- DREAMLUAU_CLEAR_REF_USERDATA(vars) // vars ceases existing when src does, so we need to clear any lua refs to it that exist.
- DREAMLUAU_CLEAR_REF_USERDATA(src)
+ if(isatom(src))
+ var/atom/src_atom = src
+ to_remove += list(src_atom.contents, src_atom.filters, src_atom.underlays, src_atom.overlays)
+ to_remove += list(vars, src) // vars ceases existing when src does, so we need to clear any lua refs to it that exist.
+ DREAMLUAU_CLEAR_REF_USERDATA(arglist(to_remove))
#endif
return QDEL_HINT_QUEUE
diff --git a/code/datums/elements/move_force_on_death.dm b/code/datums/elements/move_force_on_death.dm
index af2560d000c3..481f93581337 100644
--- a/code/datums/elements/move_force_on_death.dm
+++ b/code/datums/elements/move_force_on_death.dm
@@ -39,11 +39,11 @@
if(!isnull(move_resist))
source.move_resist = move_resist
if(!isnull(pull_force))
- source.pull_force = pull_force
+ source.set_pull_force(pull_force)
/datum/element/change_force_on_death/proc/on_revive(mob/living/source)
SIGNAL_HANDLER
source.move_force = initial(source.move_force)
source.move_resist = initial(source.move_resist)
- source.pull_force = initial(source.pull_force)
+ source.set_pull_force(initial(source.pull_force))
diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm
index 090e559c1490..33d24177b547 100644
--- a/code/datums/keybinding/living.dm
+++ b/code/datums/keybinding/living.dm
@@ -227,3 +227,27 @@
if(!HAS_TRAIT(living_user, TRAIT_CAN_HOLD_ITEMS))
return
living_user.give()
+
+/datum/keybinding/living/view_pet_data
+ hotkey_keys = list("Shift")
+ name = "view_pet_commands"
+ full_name = "View Pet Commands"
+ description = "Hold down to see all the commands you can give your pets!"
+ keybind_signal = COMSIG_KB_LIVING_VIEW_PET_COMMANDS
+
+/datum/keybinding/living/cancel_interactions
+ name = "stop_interactions"
+ full_name = "Cancel Interactions"
+ description = "Cancels any ongoing interactions (such as using a tool, performing surgery, or climbing). \
+ Note that some interactions cannot be interrupted, and you can't cancel other player's interaction with this hotkey."
+ keybind_signal = COMSIG_KB_LIVING_STOP_INTERACTIONS_DOWN
+
+/datum/keybinding/living/cancel_interactions/down(client/user, turf/target, mousepos_x, mousepos_y)
+ . = ..()
+ if(.)
+ return
+ var/mob/living/mob_user = user.mob
+ if(!LAZYLEN(mob_user.do_afters) || HAS_TRAIT(mob_user, TRAIT_INCAPACITATED))
+ return
+ // this is currently the best way to stop all ongoing doafters
+ mob_user.incapacitate(0.1 SECONDS, ignore_canstun = TRUE)
diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm
index 6d25e6ab2c4d..a35937fe5559 100644
--- a/code/datums/keybinding/mob.dm
+++ b/code/datums/keybinding/mob.dm
@@ -246,10 +246,3 @@
if(.)
return
user.movement_locked = FALSE
-
-/datum/keybinding/living/view_pet_data
- hotkey_keys = list("Shift")
- name = "view_pet_commands"
- full_name = "View Pet Commands"
- description = "Hold down to see all the commands you can give your pets!"
- keybind_signal = COMSIG_KB_LIVING_VIEW_PET_COMMANDS
diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm
index 7080033c63f3..d4bd2f0ddf94 100644
--- a/code/datums/looping_sounds/_looping_sound.dm
+++ b/code/datums/looping_sounds/_looping_sound.dm
@@ -67,6 +67,10 @@
var/reserve_random_channel = FALSE
//If we reserve a random sound channel, store the channel number here so we can clean it up later.
var/reserved_channel
+ ///Whether this looping sound uses sound tokens. This should only be true for sounds that need to update as the source or listeners move. (Generally long or important sounds like grav-gen)
+ var/use_sound_tokens = FALSE
+ ///The sound token instance for this looping sound.
+ var/datum/sound_token/sound_token_instance
/datum/looping_sound/New(
_parent,
@@ -104,12 +108,12 @@
if(timer_id)
return
- if(!sound_channel && reserve_random_channel)
- sound_channel = SSsounds.reserve_sound_channel_datumless()
+ if(!use_sound_tokens && !sound_channel && reserve_random_channel)
+ sound_channel = SSsounds.reserve_sound_channel()
reserved_channel = sound_channel
-
on_start()
+
/**
* The proc to call to stop the sound loop.
*
@@ -171,6 +175,14 @@
* * volume_override - The volume we want to play the sound at, overriding the `volume` variable.
*/
/datum/looping_sound/proc/play(soundfile, volume_override)
+
+ if(use_sound_tokens)
+ if(sound_token_instance)
+ sound_token_instance.set_volume(volume_override || volume, FALSE) // Don't update, we'll do that after
+ sound_token_instance.update_sound(soundfile, TRUE)
+ else
+ sound_token_instance = new /datum/sound_token(parent, soundfile, SOUND_RANGE + extra_range, volume_override || volume, falloff_exponent, falloff_distance)
+ return
var/sound/sound_to_play = sound(soundfile)
sound_to_play.channel = sound_channel || SSsounds.random_available_channel()
sound_to_play.volume = volume_override || volume //Use volume as fallback if theres no override
@@ -248,6 +260,7 @@
/// Stops sound playing on current channel, if specified
/datum/looping_sound/proc/stop_current()
+ QDEL_NULL(sound_token_instance)
if(!sound_channel || !ismob(parent))
return
var/mob/mob_parent = parent
diff --git a/code/datums/looping_sounds/burning.dm b/code/datums/looping_sounds/burning.dm
index 191ae88db892..8673a9ab1dec 100644
--- a/code/datums/looping_sounds/burning.dm
+++ b/code/datums/looping_sounds/burning.dm
@@ -7,3 +7,4 @@
volume = 50
vary = TRUE
extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/changeling_absorb.dm b/code/datums/looping_sounds/changeling_absorb.dm
index 418c2a8dacf5..6b28c56ee47c 100644
--- a/code/datums/looping_sounds/changeling_absorb.dm
+++ b/code/datums/looping_sounds/changeling_absorb.dm
@@ -12,3 +12,4 @@
mid_length = 3 SECONDS
volume = 80
ignore_walls = FALSE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/choking.dm b/code/datums/looping_sounds/choking.dm
index 6d337b1c7d64..bf0222b038ec 100644
--- a/code/datums/looping_sounds/choking.dm
+++ b/code/datums/looping_sounds/choking.dm
@@ -10,3 +10,4 @@
vary = TRUE
// Same as above
ignore_walls = FALSE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/cyborg.dm b/code/datums/looping_sounds/cyborg.dm
index 0a01a4c7116b..1b23271ee331 100644
--- a/code/datums/looping_sounds/cyborg.dm
+++ b/code/datums/looping_sounds/cyborg.dm
@@ -8,3 +8,4 @@
end_sound = 'sound/mobs/non-humanoids/cyborg/wash_end.ogg'
vary = TRUE
extra_range = 5
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/drip.dm b/code/datums/looping_sounds/drip.dm
index 224d7850fc29..2c6d21e273a7 100644
--- a/code/datums/looping_sounds/drip.dm
+++ b/code/datums/looping_sounds/drip.dm
@@ -7,3 +7,4 @@
vary = TRUE
ignore_walls = FALSE
falloff_distance = 5
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm
index e5950566d4f3..517dc752944d 100644
--- a/code/datums/looping_sounds/item_sounds.dm
+++ b/code/datums/looping_sounds/item_sounds.dm
@@ -2,16 +2,19 @@
mid_sounds = list('sound/effects/clock_tick.ogg' = 1)
mid_length = 0.35 SECONDS
volume = 25
+ use_sound_tokens = TRUE
/datum/looping_sound/reverse_bear_trap_beep
mid_sounds = list('sound/machines/beep/beep.ogg' = 1)
mid_length = 6 SECONDS
volume = 10
+ use_sound_tokens = TRUE
/datum/looping_sound/siren
mid_sounds = list('sound/items/weeoo1.ogg' = 1)
mid_length = 1.5 SECONDS
volume = 20
+ use_sound_tokens = TRUE
/datum/looping_sound/tape_recorder_hiss
mid_sounds = list('sound/items/taperecorder/taperecorder_hiss_mid.ogg' = 1)
@@ -29,6 +32,7 @@
falloff_exponent = 10
falloff_distance = 1
volume = 5
+ use_sound_tokens = TRUE
/datum/looping_sound/chainsaw
start_sound = list('sound/items/weapons/chainsaw_start.ogg' = 1)
@@ -39,6 +43,7 @@
end_volume = 35
volume = 40
ignore_walls = FALSE
+ use_sound_tokens = TRUE
/datum/looping_sound/beesmoke
mid_sounds = list('sound/items/weapons/beesmoke.ogg' = 1)
@@ -59,3 +64,4 @@
end_volume = 15
ignore_walls = FALSE
reserve_random_channel = TRUE
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index 871a5ca7bf90..9ab93ae378e8 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -18,13 +18,15 @@
falloff_exponent = 10
falloff_distance = 5
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/destabilized_crystal
mid_sounds = list('sound/machines/sm/loops/delamming.ogg')
mid_length = 6 SECONDS
volume = 55
- extra_range = 15
+ extra_range = 35
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/hypertorus
mid_sounds = list('sound/machines/hypertorus/loops/hypertorus_nominal.ogg')
@@ -32,6 +34,7 @@
volume = 55
extra_range = 15
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/generator
start_sound = 'sound/machines/generator/generator_start.ogg'
@@ -52,9 +55,10 @@
'sound/machines/fryer/deep_fryer_1.ogg',
'sound/machines/fryer/deep_fryer_2.ogg',
)
- mid_length = 0.2 SECONDS
+ mid_length = 1 SECONDS
end_sound = 'sound/machines/fryer/deep_fryer_emerge.ogg'
volume = 15
+ use_sound_tokens = TRUE
/datum/looping_sound/clock
mid_sounds = list('sound/ambience/misc/ticking_clock.ogg')
@@ -66,6 +70,7 @@
mid_sounds = list('sound/machines/grill/grillsizzle.ogg')
mid_length = 18
volume = 50
+ use_sound_tokens = TRUE
/datum/looping_sound/oven
start_sound = 'sound/machines/oven/oven_loop_start.ogg' //my immersions
@@ -75,14 +80,7 @@
end_sound = 'sound/machines/oven/oven_loop_end.ogg'
volume = 100
falloff_exponent = 4
-
-/datum/looping_sound/deep_fryer
- mid_length = 0.2 SECONDS
- mid_sounds = list(
- 'sound/machines/fryer/deep_fryer_1.ogg',
- 'sound/machines/fryer/deep_fryer_2.ogg',
- )
- volume = 30
+ use_sound_tokens = TRUE
/datum/looping_sound/microwave
start_sound = 'sound/machines/microwave/microwave-start.ogg'
@@ -94,6 +92,7 @@
mid_length = 1 SECONDS
end_sound = 'sound/machines/microwave/microwave-end.ogg'
volume = 90
+ use_sound_tokens = TRUE
/datum/looping_sound/lathe_print
mid_sounds = list('sound/machines/lathe/lathe_print.ogg')
@@ -103,12 +102,14 @@
ignore_walls = FALSE
falloff_distance = 1
mid_length_vary = 1 SECONDS
+ use_sound_tokens = TRUE
/datum/looping_sound/jackpot
mid_length = 1.1 SECONDS
mid_sounds = list('sound/machines/roulette/roulettejackpot.ogg')
volume = 85
vary = TRUE
+ use_sound_tokens = TRUE
/datum/looping_sound/server
mid_sounds = list(
@@ -140,9 +141,10 @@
end_sound = 'sound/machines/computer/computer_end.ogg'
end_volume = 1 SECONDS
volume = SOUND_AUDIBLE_VOLUME_MIN
- falloff_exponent = 5 //Ultra quiet very fast
+ falloff_exponent = 4 //Ultra quiet very fast
extra_range = -12
- falloff_distance = 1 //Instant falloff after initial tile
+ falloff_distance = 0 //Instant falloff after initial tile
+ use_sound_tokens = TRUE
/datum/looping_sound/gravgen
start_sound = 'sound/machines/gravgen/grav_gen_start.ogg'
@@ -157,7 +159,8 @@
vary = TRUE
volume = 70
falloff_distance = 5
- falloff_exponent = 20
+ falloff_exponent = 10
+ use_sound_tokens = TRUE
/datum/looping_sound/firealarm
mid_sounds = list(
@@ -168,16 +171,19 @@
)
mid_length = 2.4 SECONDS
volume = 30
+ use_sound_tokens = TRUE
/datum/looping_sound/gravgen/kinesis
volume = 20
falloff_distance = 2
falloff_exponent = 5
+ use_sound_tokens = TRUE
/datum/looping_sound/boiling
mid_sounds = list('sound/effects/bubbles/bubbles2.ogg')
mid_length = 7 SECONDS
volume = 25
+ use_sound_tokens = TRUE
/datum/looping_sound/typing
mid_sounds = list(
@@ -205,7 +211,8 @@
end_sound = 'sound/effects/soup_boil/soup_boil_end.ogg'
end_volume = 60
extra_range = MEDIUM_RANGE_SOUND_EXTRARANGE
- falloff_exponent = 4
+ falloff_exponent = 3
+ use_sound_tokens = TRUE
/datum/looping_sound/soup/toxic
volume = 40
@@ -225,3 +232,4 @@
)
mid_length = 5 SECONDS
volume = 50
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/projectiles.dm b/code/datums/looping_sounds/projectiles.dm
index ca96df698e92..1c2dc9b19b72 100644
--- a/code/datums/looping_sounds/projectiles.dm
+++ b/code/datums/looping_sounds/projectiles.dm
@@ -2,3 +2,4 @@
mid_sounds = list('sound/effects/moon_parade_soundloop.ogg' = 1)
mid_length = 2 SECONDS
volume = 20
+ use_sound_tokens = TRUE
diff --git a/code/datums/looping_sounds/vents.dm b/code/datums/looping_sounds/vents.dm
index 183c337ef981..04e0246a4ee5 100644
--- a/code/datums/looping_sounds/vents.dm
+++ b/code/datums/looping_sounds/vents.dm
@@ -2,5 +2,6 @@
start_sound = 'sound/machines/fan/fan_start.ogg'
start_length = 1.5 SECONDS
end_sound = 'sound/machines/fan/fan_stop.ogg'
- end_sound = 1.5 SECONDS
mid_sounds = 'sound/machines/fan/fan_loop.ogg'
+ mid_length = 1.9 SECONDS
+ use_sound_tokens = TRUE
diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm
index 63b47165db47..316f9659e91d 100644
--- a/code/datums/mapgen/CaveGenerator.dm
+++ b/code/datums/mapgen/CaveGenerator.dm
@@ -86,15 +86,31 @@
/// Radius around megafauna within which we avoid spawning tendrils
var/megafauna_exclusion_radius = 7
- ///Chance of cells starting closed
- var/initial_closed_chance = 45
- ///Amount of smoothing iterations
- var/smoothing_iterations = 20
- ///How much neighbours does a dead cell need to become alive
- var/birth_limit = 4
- ///How little neighbours does a alive cell need to die
- var/death_limit = 3
+ ///Cave gen settings below!!
+
+ /// Minimum dimension of a BSP leaf in the generator. Raising this creates larger pockets but can end up making for big corridors
+ var/min_bsp_size = 25
+ /// Maximum aspect ratio for BSP splits for lavaland generator
+ var/max_ratio = 1.5
+ /// Room edge padding within BSP leaf for lavaland generator
+ var/padding = 1
+ /// How much of each BSP leaf is considered untouchable by the cellular automata. Raising this generally means bigger pockets
+ var/room_fill_percent = 30
+ /// Width of corridors between rooms for lavaland generator. Raising this just means corridors are AT LEAST this wide. but cellular automata can make them bigger
+ var/corridor_width = 1
+ /// Chance to add extra MST edges for loops for lavaland generator. This basically results in more corridors / mazier generation
+ var/loop_percent = 15
+ /// Initial random floor density for lavaland generator
+ var/noise_percent = 51
+ /// Cellular Automata smoothing iterations for lavaland generator
+ var/ca_steps = 8
+ /// Neighbors to create floor (>=) for lavaland generator
+ var/birth_limit = 6
+ /// Neighbors to survive as floor (>=) for lavaland generator
+ var/survival_limit = 4
+ ///Whether out-of-boudns counts as being alive. Setting this to FALSE results in the edges of the generator generating more closed. Default behavior tends to open up tunnels outside.
+ var/edges_are_alive = TRUE
/datum/map_generator/cave_generator/New()
. = ..()
@@ -138,10 +154,11 @@
return generate_terrain_with_biomes(turfs, generate_in)
var/start_time = REALTIMEOFDAY
- string_gen = rustg_cnoise_generate("[initial_closed_chance]", "[smoothing_iterations]", "[birth_limit]", "[death_limit]", "[world.maxx]", "[world.maxy]") //Generate the raw CA data
+
+ string_gen = generate_cave(generate_in)
for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them
- var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "0"
+ var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1"
var/turf/new_turf = pick(closed ? closed_turf_types : open_turf_types)
// The assumption is this will be faster then changeturf, and changeturf isn't required since by this point
@@ -190,7 +207,7 @@
heat_seed = rand(0, 50000)
var/start_time = REALTIMEOFDAY
- string_gen = rustg_cnoise_generate("[initial_closed_chance]", "[smoothing_iterations]", "[birth_limit]", "[death_limit]", "[world.maxx]", "[world.maxy]") //Generate the raw CA data
+ string_gen = generate_cave(generate_in)
var/humidity_gen = list()
humidity_gen[BIOME_HIGH_HUMIDITY] = rustg_dbp_generate("[humidity_seed]", "60", "[biome_stamp_size]", "[world.maxx]", "[high_heat_threshold]", "1.1")
@@ -210,7 +227,7 @@
var/list/to_generate = list()
for(var/turf/gen_turf as anything in turfs) //Go through all the turfs and generate them
- var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "0"
+ var/closed = string_gen[world.maxx * (gen_turf.y - 1) + gen_turf.x] != "1"
var/datum/biome/selected_biome
// Here comes the meat of the biome code.
@@ -405,6 +422,32 @@
to_chat(world, span_boldannounce("[message]"), MESSAGE_TYPE_DEBUG)
log_world(message)
+
+///Generates the cave shape using Rust-G
+/datum/map_generator/cave_generator/proc/generate_cave(area/generate_in)
+
+ ///Loop through all the active ruins for this z-level and make a json format out of it so we can send it to the generator
+ var/list/active_ruins_list = list()
+
+ for(var/turf/bottom_left_turf in SSmapping.active_ruins)
+ var/datum/map_template/ruin/active_ruin = SSmapping.active_ruins[bottom_left_turf]
+ if(bottom_left_turf.z != generate_in.z)
+ continue
+ active_ruins_list += list(list(
+ "x" = max(1, bottom_left_turf.x - active_ruin.terrain_padding),
+ "y" = max(1, bottom_left_turf.y - active_ruin.terrain_padding),
+ "w" = active_ruin.width + active_ruin.terrain_padding * 2,
+ "h" = active_ruin.height + active_ruin.terrain_padding * 2,
+ "isEnclosed" = active_ruin.enclosed_for_terrain,
+ ))
+
+ var/active_ruin_string = json_encode(active_ruins_list)
+
+ var/string_gen = rustg_cave_system_generator_generate("[world.maxx]", "[world.maxy]", active_ruin_string, "[min_bsp_size]","[max_ratio]", "[padding]", "[room_fill_percent]", "[corridor_width]","[loop_percent]", "[noise_percent]", "[ca_steps]", "[birth_limit]", "[survival_limit]", "[edges_are_alive]")
+
+ return string_gen
+
+
/datum/map_generator/cave_generator/jungle
possible_biomes = list(
BIOME_LOW_HEAT = list(
diff --git a/code/datums/mapgen/Cavegens/IcemoonCaves.dm b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
index 153b0c22e804..45f429c0fe80 100644
--- a/code/datums/mapgen/Cavegens/IcemoonCaves.dm
+++ b/code/datums/mapgen/Cavegens/IcemoonCaves.dm
@@ -11,7 +11,7 @@
/mob/living/basic/mining/lobstrosity = 15,
/mob/living/basic/mining/wolf = 50,
/obj/effect/spawner/random/lavaland_mob/raptor = 15,
- /mob/living/simple_animal/hostile/asteroid/polarbear = 30,
+ /mob/living/basic/mining/polarbear = 30,
/obj/structure/spawner/ice_moon = 3,
/obj/structure/spawner/ice_moon/polarbear = 3,
)
@@ -39,10 +39,7 @@
weighted_open_turf_types = list(/turf/open/misc/asteroid/snow/icemoon = 1)
flora_spawn_chance = 60
weighted_mob_spawn_list = null
- initial_closed_chance = 0
- birth_limit = 5
- death_limit = 4
- smoothing_iterations = 10
+ noise_percent = 100 // Full floor
feature_spawn_chance = 0.15
weighted_feature_spawn_list = list(
@@ -62,7 +59,7 @@
/// Surface snow generator variant for forested station trait, WITH FORESTSSSS
/datum/map_generator/cave_generator/icemoon/surface/forested
- initial_closed_chance = 10
+ noise_percent = 65 //Few small rocks, but mostly open floor for the trees to spawn on
flora_spawn_chance = 80
weighted_flora_spawn_list = list(
@@ -80,7 +77,6 @@
weighted_mob_spawn_list = list(/mob/living/basic/deer/ice = 99, /mob/living/basic/tree = 1, /obj/effect/spawner/random/lavaland_mob/raptor = 15)
/datum/map_generator/cave_generator/icemoon/surface/rocky
- initial_closed_chance = 53
mob_spawn_chance = 0.5
/datum/map_generator/cave_generator/icemoon/surface/noruins //use this for when you don't want ruins to spawn in a certain area
diff --git a/code/datums/mapgen/Cavegens/LavalandGenerator.dm b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
index 40c25b51c289..b40ea70beda8 100644
--- a/code/datums/mapgen/Cavegens/LavalandGenerator.dm
+++ b/code/datums/mapgen/Cavegens/LavalandGenerator.dm
@@ -23,8 +23,7 @@
high_heat_threshold = 0.15
high_humidity_threshold = 0.15
biome_stamp_size = 60
- smoothing_iterations = 50
-
+
/datum/map_generator/cave_generator/lavaland/ruin_version
biome_population = FALSE
weighted_open_turf_types = list(/turf/open/misc/asteroid/basalt/lava_land_surface/no_ruins = 1)
diff --git a/code/datums/mood.dm b/code/datums/mood.dm
index afc7824380f8..d9c031151671 100644
--- a/code/datums/mood.dm
+++ b/code/datums/mood.dm
@@ -495,6 +495,9 @@
else
msg += "• [span_grey("I don't have much of a reaction to anything right now.")]
"
+ if(LAZYLEN(mob_parent.personalities))
+ msg += span_notice("You know yourself to be [mob_parent.get_parsonality_string()].
")
+
if(LAZYLEN(mob_parent.quirks))
msg += span_notice("You have these quirks: [mob_parent.get_quirk_string(FALSE, CAT_QUIRK_ALL)].")
diff --git a/code/datums/quirks/_quirk.dm b/code/datums/quirks/_quirk.dm
index 46be5ac25a59..5fd9ad64dd20 100644
--- a/code/datums/quirks/_quirk.dm
+++ b/code/datums/quirks/_quirk.dm
@@ -248,45 +248,3 @@
to_chat(quirk_holder, chat_string)
where_items_spawned = null
-
-/**
- * get_quirk_string() is used to get a printable string of all the quirk traits someone has for certain criteria
- *
- * Arguments:
- * * Medical- If we want the long, fancy descriptions that show up in medical records, or if not, just the name
- * * Category- Which types of quirks we want to print out. Defaults to everything
- * * from_scan- If the source of this call is like a health analyzer or HUD, in which case QUIRK_HIDE_FROM_MEDICAL hides the quirk.
- */
-/mob/living/proc/get_quirk_string(medical = FALSE, category = CAT_QUIRK_ALL, from_scan = FALSE)
- var/list/dat = list()
- for(var/datum/quirk/candidate as anything in quirks)
- if(from_scan && (candidate.quirk_flags & QUIRK_HIDE_FROM_SCAN))
- continue
- switch(category)
- if(CAT_QUIRK_MAJOR_DISABILITY)
- if(candidate.value >= -4)
- continue
- if(CAT_QUIRK_MINOR_DISABILITY)
- if(!ISINRANGE(candidate.value, -4, -1))
- continue
- if(CAT_QUIRK_NOTES)
- if(candidate.value < 0)
- continue
- dat += medical ? candidate.medical_record_text : candidate.name
-
- if(!length(dat))
- return medical ? "No issues have been declared." : "None"
- return medical ? dat.Join("
") : dat.Join(", ")
-
-/mob/living/proc/cleanse_quirk_datums() //removes all trait datums
- QDEL_LAZYLIST(quirks)
-
-/mob/living/proc/transfer_quirk_datums(mob/living/to_mob)
- // We could be done before the client was moved or after the client was moved
- var/datum/preferences/to_pass = client || to_mob.client
-
- for(var/datum/quirk/quirk as anything in quirks)
- if(quirk.quirk_flags & QUIRK_NO_TRANSFER)
- continue
- quirk.remove_from_current_holder(quirk_transfer = TRUE)
- quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass)
diff --git a/code/datums/ruins.dm b/code/datums/ruins.dm
index 703991f596e9..5741904e4906 100644
--- a/code/datums/ruins.dm
+++ b/code/datums/ruins.dm
@@ -28,6 +28,10 @@
var/suffix = null
///What flavor or ruin is this? eg ZTRAIT_SPACE_RUINS
var/ruin_type = null
+ ///is this ruin "enclosed" by walls. This is relevant for terrain gen with cellular automata to know whether this ruin will spawn inside of walls, or should spawn in the open.
+ var/enclosed_for_terrain = FALSE
+ ///Padding to be used to ensure extra space around the ruin for terrain gen purposes. If a ruin is NOT enclosed and this is set to 1; there will be at least one layer of open terrain around the ruin. If a ruin IS enclosed and this is set to 1; there will be at least one layer of wall terrain around the ruin.
+ var/terrain_padding = 0
/datum/map_template/ruin/New()
if(!name && id)
diff --git a/code/datums/ruins/icemoon.dm b/code/datums/ruins/icemoon.dm
index 5e293a70747a..35d1856d8ace 100644
--- a/code/datums/ruins/icemoon.dm
+++ b/code/datums/ruins/icemoon.dm
@@ -23,6 +23,7 @@
id = "lust"
description = "Not exactly what you expected."
suffix = "icemoon_surface_lust.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/asteroid
name = "Ice-Ruin Asteroid Site"
@@ -103,6 +104,7 @@
suffix = "icemoon_surface_mining_site.dmm"
always_place = TRUE
always_spawn_with = list(/datum/map_template/ruin/icemoon/underground/mining_site_below = PLACE_BELOW)
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/mining_site_below
name = "Ice-Ruin Mining Site Underground"
@@ -111,6 +113,7 @@
suffix = "icemoon_underground_mining_site.dmm"
has_ceiling = FALSE
unpickable = TRUE
+ enclosed_for_terrain = TRUE
// below ground only
@@ -124,18 +127,21 @@
id = "abandonedvillage"
description = "Who knows what lies within?"
suffix = "icemoon_underground_abandoned_village.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/library
name = "Ice-Ruin Buried Library"
id = "buriedlibrary"
description = "A once grand library, now lost to the confines of the Ice Moon."
suffix = "icemoon_underground_library.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/wrath
name = "Ice-Ruin Ruin of Wrath"
id = "wrath"
description = "You'll fight and fight and just keep fighting."
suffix = "icemoon_underground_wrath.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/hermit
name = "Ice-Ruin Frozen Shack"
@@ -148,6 +154,7 @@
id = "lavalandsite"
description = "I guess we never really left you huh?"
suffix = "icemoon_underground_lavaland.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/puzzle
name = "Ice-Ruin Ancient Puzzle"
@@ -180,6 +187,7 @@
id = "mailroom"
description = "This is where all of your paychecks went. Signed, the management."
suffix = "icemoon_underground_mailroom.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/biodome
name = "Ice-Ruin Syndicate Bio-Dome"
@@ -204,6 +212,7 @@
id = "syndie_lab"
description = "A small laboratory and living space for Syndicate agents."
suffix = "icemoon_underground_syndielab.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/o31
name = "Ice-Ruin Outpost 31"
@@ -224,6 +233,7 @@
id = "hotsprings"
description = "Just relax and take a dip, nothing will go wrong, I swear!"
suffix = "icemoon_underground_hotsprings.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/icemoon/underground/vent
name = "Ice-Ruin Icemoon Ore Vent"
diff --git a/code/datums/ruins/lavaland.dm b/code/datums/ruins/lavaland.dm
index cd3c1065ec4d..206158c15d1f 100644
--- a/code/datums/ruins/lavaland.dm
+++ b/code/datums/ruins/lavaland.dm
@@ -15,6 +15,7 @@
description = "Seemingly plucked from a tropical destination, this beach is calm and cool, with the salty waves roaring softly in the background. \
Comes with a rustic wooden bar and suicidal bartender."
suffix = "lavaland_biodome_beach.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/biodome/winter
name = "Lava-Ruin Biodome Winter"
@@ -43,6 +44,7 @@
suffix = "lavaland_surface_cube.dmm"
cost = 10
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/seed_vault
name = "Lava-Ruin Seed Vault"
@@ -52,6 +54,7 @@
suffix = "lavaland_surface_seed_vault.dmm"
cost = 10
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/ash_walker
name = "Lava-Ruin Ash Walker Nest"
@@ -61,6 +64,7 @@
suffix = "lavaland_surface_ash_walker1.dmm"
cost = 20
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/syndicate_base
name = "Lava-Ruin Syndicate Lava Base"
@@ -87,6 +91,7 @@
cost = 5
suffix = "lavaland_surface_gaia.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/sin
cost = 10
@@ -124,6 +129,7 @@
suffix = "lavaland_surface_sloth.dmm"
// Generates nothing but atmos runtimes and salt
cost = 0
+ terrain_padding = 2
/datum/map_template/ruin/lavaland/ratvar
name = "Lava-Ruin Dead God"
@@ -140,6 +146,7 @@
suffix = "lavaland_surface_hierophant.dmm"
always_place = TRUE
allow_duplicates = FALSE
+ terrain_padding = 2
/datum/map_template/ruin/lavaland/blood_drunk_miner
name = "Lava-Ruin Blood-Drunk Miner"
@@ -148,14 +155,17 @@
suffix = "lavaland_surface_blooddrunk1.dmm"
cost = 0
allow_duplicates = FALSE //will only spawn one variant of the ruin
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/guidance
name = "Lava-Ruin Blood-Drunk Miner (Guidance)"
suffix = "lavaland_surface_blooddrunk2.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/hunter
name = "Lava-Ruin Blood-Drunk Miner (Hunter)"
suffix = "lavaland_surface_blooddrunk3.dmm"
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/blood_drunk_miner/random
name = "Lava-Ruin Blood-Drunk Miner (Random)"
@@ -172,6 +182,7 @@
description = "Turns out that keeping your abductees unconscious is really important. Who knew?"
suffix = "lavaland_surface_ufo_crash.dmm"
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/xeno_nest
name = "Lava-Ruin Xenomorph Nest"
@@ -180,6 +191,7 @@
Quality memes."
suffix = "lavaland_surface_xeno_nest.dmm"
cost = 20
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/fountain
name = "Lava-Ruin Fountain Hall"
@@ -227,6 +239,7 @@
suffix = "lavaland_surface_random_ripley.dmm"
allow_duplicates = FALSE
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/dark_wizards
name = "Lava-Ruin Dark Wizard Altar"
@@ -234,6 +247,7 @@
description = "A ruin with dark wizards. What secret do they guard?"
suffix = "lavaland_surface_wizard.dmm"
cost = 5
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/strong_stone
name = "Lava-Ruin Strong Stone"
@@ -266,6 +280,7 @@
suffix = "lavaland_surface_elephant_graveyard.dmm"
allow_duplicates = FALSE
cost = 10
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/bileworm_nest
name = "Lava-Ruin Bileworm Nest"
@@ -274,6 +289,7 @@
cost = 5
suffix = "lavaland_surface_bileworm_nest.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/lava_phonebooth
name = "Lava-Ruin Phonebooth"
@@ -323,6 +339,8 @@
description = "Not every shuttle makes it back to CentCom."
suffix = "lavaland_surface_shuttle_wreckage.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
+
/datum/map_template/ruin/lavaland/crashsite
name = "Lava-Ruin Pod Crashsite"
@@ -330,6 +348,7 @@
description = "They launched too early"
suffix = "lavaland_surface_crashsite.dmm"
allow_duplicates = FALSE
+ enclosed_for_terrain = TRUE
/datum/map_template/ruin/lavaland/shoe_facotry
name = "Lava-Ruin Shoe Factory"
diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm
new file mode 100644
index 000000000000..d9ad9903c283
--- /dev/null
+++ b/code/datums/sound_token.dm
@@ -0,0 +1,284 @@
+// Sound tokens, a datumized handler for spatial sound.
+// Uses the spatial grid to track clients in range and add them as listeners
+// Updated by the SSsound_tokens subsystem every tick when requested by client so that if the source or listener moves, the sound updates accordingly.
+/datum/sound_token
+ /// The atom playing the sound.
+ var/atom/source
+ /// k:v list of mob : sound status
+ var/list/listeners = list()
+
+ /// Sound maximum range
+ var/range
+ /// Sound volume
+ var/volume
+ /// Sound falloff
+ var/falloff_exponent
+ /// Sound falloff distance
+ var/falloff_distance
+
+ /// The master copy of the playing sound.
+ var/sound/sound
+ /// Null sound for cancelling the sound entirely.
+ var/sound/null_sound
+
+ /// Status of the playing sound
+ var/sound_status = NONE
+ /// The channel being used.
+ var/sound_channel
+ /// world.time when the sound started (or when the sound file was last changed). Used to calculate playback offset for new listeners.
+ var/start_time
+ /// Duration of the current sound file in deciseconds. Used to wrap offset for looping sounds.
+ var/sound_duration
+ /// Cell tracker managing spatial grid cells within range of the source. The wizards say this is the fastest.
+ var/datum/cell_tracker/cell_tracker
+
+/datum/sound_token/New(atom/_source, _sound, _range = 10, _volume = 50, _falloff_exponent = SOUND_FALLOFF_EXPONENT, _falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE)
+ source = _source
+ RegisterSignal(source, COMSIG_QDELETING, PROC_REF(source_deleted))
+ RegisterSignal(source, COMSIG_MOVABLE_MOVED, PROC_REF(source_moved))
+ RegisterSignal(source, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
+
+ range = _range
+ volume = _volume
+ falloff_exponent = _falloff_exponent
+ falloff_distance = _falloff_distance
+
+ update_sound(_sound)
+
+ null_sound = sound(channel = sound_channel)
+
+ cell_tracker = new /datum/cell_tracker(range, range)
+ update_tracked_cells()
+
+ RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGIN, PROC_REF(player_login))
+ RegisterSignal(SSdcs, COMSIG_GLOB_PLAYER_LOGOUT, PROC_REF(player_logout))
+
+/datum/sound_token/Destroy(force, ...)
+ for(var/listener in listeners)
+ remove_listener(listener)
+
+ listeners = null
+ source = null
+ return ..()
+
+///Lets us update the sound to a new one.
+/datum/sound_token/proc/update_sound(_sound, start_playing = FALSE)
+ sound = sound(_sound)
+ if(!sound_channel)
+ sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
+ sound.channel = sound_channel
+ sound_duration = SSsounds.get_sound_length(_sound)
+ start_time = REALTIMEOFDAY
+ if(start_playing)
+ force_update_all_listeners(FALSE)
+
+/// Updates the data of a listener, or adds them if they are not present.
+/datum/sound_token/proc/add_or_update_listener(mob/listener_mob)
+ if(isnull(listeners[listener_mob]))
+ add_listener(listener_mob)
+ else
+ update_listener(listener_mob)
+
+/// Adds a listener to the sound.
+/datum/sound_token/proc/add_listener(mob/listener_mob)
+ if(!isnull(listeners[listener_mob]))
+ return FALSE
+
+ if(!listener_mob.client || isnewplayer(listener_mob))
+ return
+
+ listeners[listener_mob] = NONE
+ listener_mob.client.sound_tokens += src
+ RegisterSignal(listener_mob, COMSIG_QDELETING, PROC_REF(listener_deleted))
+ RegisterSignals(listener_mob, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deafness_update))
+ update_listener(listener_mob, FALSE)
+ return TRUE
+
+/// Remove a listener from the sound.
+/datum/sound_token/proc/remove_listener(mob/listener_mob)
+
+ listeners -= listener_mob
+
+ if(listener_mob.client)
+ listener_mob.client.sound_tokens -= src
+
+ UnregisterSignal(listener_mob, list(COMSIG_QDELETING, SIGNAL_ADDTRAIT(TRAIT_DEAF),SIGNAL_REMOVETRAIT(TRAIT_DEAF)))
+ SEND_SOUND(listener_mob, null_sound)
+
+/datum/sound_token/proc/update_listener(mob/listener_mob, update_sound = TRUE)
+ if(QDELETED(src))
+ return
+ if(isnull(listeners[listener_mob]))
+ return
+
+ var/turf/source_turf = get_turf(source)
+ var/turf/listener_turf = get_turf(listener_mob)
+
+ if(!source_turf || !listener_turf)
+ return
+
+ var/is_muted = listeners[listener_mob] & SOUND_MUTE
+ var/should_be_muted = FALSE
+
+ if(source_turf.z != listener_turf.z)
+ should_be_muted = TRUE
+
+ var/distance = get_dist(source_turf, listener_turf)
+ if(distance > range)
+ should_be_muted = TRUE
+ if(should_be_muted && is_muted)
+ return
+
+ should_be_muted ||= HAS_TRAIT(listener_mob, TRAIT_DEAF)
+ if(should_be_muted && is_muted)
+ return
+
+ set_listener_status(listener_mob, should_be_muted ? SOUND_MUTE : NONE)
+ send_listener_sound(listener_mob, update_sound)
+
+/datum/sound_token/proc/send_listener_sound(mob/listener_mob, update_sound)
+ PRIVATE_PROC(TRUE)
+
+ sound.status = SOUND_STREAM|sound_status|listeners[listener_mob]
+ if(update_sound)
+ sound.status |= SOUND_UPDATE
+ else
+ sound.offset = calculate_offset()
+
+ if(sound.status & SOUND_MUTE)
+ SEND_SOUND(listener_mob, sound)
+ return
+
+ if(!listener_mob.playsound_local(get_turf(source), vol = volume, falloff_exponent = falloff_exponent, channel = sound_channel, sound_to_use = sound, max_distance = range, falloff_distance = falloff_distance, use_reverb = TRUE))
+ sound.status = SOUND_UPDATE|SOUND_MUTE
+ SEND_SOUND(listener_mob, sound)
+ sound.offset = null
+
+/datum/sound_token/proc/update_all_listeners()
+ for(var/mob/listener_mob in listeners)
+ if(listener_mob.client)
+ SSsound_tokens.clients_needing_update[listener_mob.client] = TRUE
+
+/datum/sound_token/proc/force_update_all_listeners(update_sound = TRUE)
+ for(var/mob/listener_mob in listeners)
+ if(listener_mob.client)
+ update_listener(listener_mob, update_sound)
+
+/// Setter for volume
+/datum/sound_token/proc/set_volume(new_volume, update_listeners = TRUE)
+ volume = new_volume
+ if(update_listeners)
+ update_all_listeners()
+
+/// Set the status of a listener. Does not update the sound.
+/datum/sound_token/proc/set_listener_status(mob/listener_mob, new_status)
+ if(isnull(listeners[listener_mob]))
+ return
+
+ listeners[listener_mob] = new_status
+
+/// Respond to TRAIT_DEAF addition/removal
+/datum/sound_token/proc/listener_deafness_update(atom/movable/source)
+ SIGNAL_HANDLER
+ update_listener(source)
+
+/datum/sound_token/proc/listener_deleted(datum/source)
+ SIGNAL_HANDLER
+ remove_listener(source)
+
+/// Respond to any mob in the world being logged into. Only adds if the mob is within range.
+/datum/sound_token/proc/player_login(datum/source, mob/player)
+ SIGNAL_HANDLER
+ var/turf/player_turf = get_turf(player)
+ var/turf/source_turf = get_turf(src.source)
+ if(!player_turf || !source_turf)
+ return
+ if(player_turf.z != source_turf.z)
+ return
+ if(get_dist(source_turf, player_turf) > range)
+ return
+ add_or_update_listener(player)
+
+/// Respond to any cliented mob becoming uncliented
+/datum/sound_token/proc/player_logout(datum/source, mob/player)
+ SIGNAL_HANDLER
+ remove_listener(player)
+
+/// If the sound source moves, update tracked cells then refresh all listener positions.
+/datum/sound_token/proc/source_moved()
+ SIGNAL_HANDLER
+ update_tracked_cells()
+ update_all_listeners()
+
+/datum/sound_token/proc/source_deleted()
+ SIGNAL_HANDLER
+
+ qdel(src)
+
+///Update env when source is entering new area
+/datum/sound_token/proc/on_enter_area(datum/source, area/area_to_register)
+ SIGNAL_HANDLER
+ set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
+
+/datum/sound_token/proc/set_new_environment(new_env)
+ if(sound.environment == new_env)
+ return
+ sound.environment = new_env
+ update_all_listeners()
+
+///Calculates the offset to give the sound for people who start hearing it mid-play
+/datum/sound_token/proc/calculate_offset()
+ var/elapsed = REALTIMEOFDAY - start_time
+ var/freq_factor = (sound.frequency || 100) / 100
+ var/pitch_factor = (sound.pitch || 100) / 100
+ var/offset = elapsed * freq_factor * pitch_factor
+ return offset
+
+///Update tracked cells; happens on movement. We need to check if anyone is now out of cell range and kick them out.
+/datum/sound_token/proc/update_tracked_cells()
+ if(!get_turf(source))
+ return
+
+ var/list/new_and_old = cell_tracker.recalculate_cells(get_turf(source))
+ var/list/datum/spatial_grid_cell/added_cells = new_and_old[1]
+ var/list/datum/spatial_grid_cell/removed_cells = new_and_old[2]
+
+ for(var/datum/spatial_grid_cell/cell as anything in removed_cells)
+ UnregisterSignal(cell, list(SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS),))
+
+ // Remove listeners whose mob is no longer in any remaining member cell
+ if(removed_cells.len)
+ for(var/mob/listener_mob as anything in listeners)
+ var/still_in_range = FALSE
+ for(var/datum/spatial_grid_cell/cell as anything in cell_tracker.member_cells)
+ if(listener_mob in cell.client_contents)
+ still_in_range = TRUE
+ break
+ if(!still_in_range)
+ remove_listener(listener_mob)
+
+ for(var/datum/spatial_grid_cell/cell as anything in added_cells)
+ RegisterSignal(cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_entered))
+ RegisterSignal(cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), PROC_REF(on_cell_client_exited))
+ for(var/mob/listener_mob as anything in cell.client_contents)
+ add_or_update_listener(listener_mob)
+
+/// Signal handler for SPATIAL_GRID_CELL_ENTERED on tracked cells. Adds newly arriving mobs as listeners.
+/datum/sound_token/proc/on_cell_client_entered(datum/source, list/entering_mobs)
+ SIGNAL_HANDLER
+
+ for(var/mob/listener_mob as anything in entering_mobs)
+ if(!isnull(listeners[listener_mob])) // already added
+ continue
+ add_or_update_listener(listener_mob)
+
+/// Signal handler for SPATIAL_GRID_CELL_EXITED on tracked cells. Removes mobs who have left all member cells.
+/datum/sound_token/proc/on_cell_client_exited(datum/source, list/exiting_mobs)
+ SIGNAL_HANDLER
+ for(var/mob/listener_mob as anything in exiting_mobs)
+ var/still_in_range = FALSE
+ if(SSspatial_grid.get_cell_of(listener_mob) in cell_tracker.member_cells)
+ still_in_range = TRUE
+
+ if(!still_in_range)
+ remove_listener(listener_mob)
diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm
index 76c5744c0f98..ad246f5ff825 100644
--- a/code/game/atom/_atom.dm
+++ b/code/game/atom/_atom.dm
@@ -192,15 +192,6 @@
if(smoothing_flags & SMOOTH_QUEUED)
SSicon_smooth.remove_from_queues(src)
-#ifndef DISABLE_DREAMLUAU
- // These lists cease existing when src does, so we need to clear any lua refs to them that exist.
- if(!(datum_flags & DF_STATIC_OBJECT))
- DREAMLUAU_CLEAR_REF_USERDATA(contents)
- DREAMLUAU_CLEAR_REF_USERDATA(filters)
- DREAMLUAU_CLEAR_REF_USERDATA(overlays)
- DREAMLUAU_CLEAR_REF_USERDATA(underlays)
-#endif
-
return ..()
/atom/proc/handle_ricochet(obj/projectile/ricocheting_projectile)
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index c6c604e60208..403af00e60fc 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -252,12 +252,6 @@
LAZYNULL(client_mobs_in_contents)
-#ifndef DISABLE_DREAMLUAU
- // These lists cease existing when src does, so we need to clear any lua refs to them that exist.
- DREAMLUAU_CLEAR_REF_USERDATA(vis_contents)
- DREAMLUAU_CLEAR_REF_USERDATA(vis_locs)
-#endif
-
. = ..()
for(var/movable_content in contents)
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index fbc72b11eb85..13d2f6f4aa35 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1331,6 +1331,27 @@
/obj/item/stack/sheet/glass = 1)
needs_anchored = FALSE
+/obj/item/circuitboard/machine/hydroponics/proc/changeindicators(mob/living/user, obj/item/I)
+ if(build_path == /obj/machinery/hydroponics/constructable/oldstyle)
+ name = "Hydroponics Tray"
+ build_path = /obj/machinery/hydroponics/constructable
+ balloon_alert(user, "defaulting indicator location.")
+ else
+ name = "Old-Designed Hydropoincs Tray"
+ build_path = /obj/machinery/hydroponics/constructable/oldstyle
+ balloon_alert(user, "moving the indicators...")
+ return TRUE
+
+/obj/item/circuitboard/machine/hydroponics/item_interaction(mob/living/user, obj/item/I, list/modifiers)
+ if(istype(I, /obj/item/plant_analyzer))
+ changeindicators(user)
+ else
+ return ..()
+
+/obj/item/circuitboard/machine/hydroponics/screwdriver_act(mob/living/user, obj/item/tool)
+ src.changeindicators(user)
+ return
+
/obj/item/circuitboard/machine/hydroponics/fullupgrade
build_path = /obj/machinery/hydroponics/constructable/fullupgrade
specific_parts = TRUE
diff --git a/code/game/objects/items/food/pastries.dm b/code/game/objects/items/food/pastries.dm
index 041dc116bcff..aedcbdf7c474 100644
--- a/code/game/objects/items/food/pastries.dm
+++ b/code/game/objects/items/food/pastries.dm
@@ -599,3 +599,18 @@
food_flags = FOOD_FINGER_FOOD
w_class = WEIGHT_CLASS_SMALL
crafting_complexity = FOOD_COMPLEXITY_3
+
+/obj/item/food/apple_fritter
+ name = "apple fritter"
+ desc = "For something that looks like a pile of glazed dirt, it's suprisingly tart. It smells sweet enough to knock you unconscious."
+ icon_state = "apple_fritter"
+ food_reagents = list(
+ /datum/reagent/consumable/nutriment = 3,
+ /datum/reagent/consumable/nutriment/vitamin = 1,
+ /datum/reagent/consumable/sugar = 1,
+ /datum/reagent/consumable/applejuice = 1,
+ )
+ tastes = list("apple" = 1, "glaze" = 1)
+ foodtypes = GRAIN|FRUIT|FRIED|DAIRY|BREAKFAST
+ w_class = WEIGHT_CLASS_SMALL
+ crafting_complexity = FOOD_COMPLEXITY_3
diff --git a/code/game/objects/items/robot/items/hypo.dm b/code/game/objects/items/robot/items/hypo.dm
index 2a759a275a8b..3119037f0153 100644
--- a/code/game/objects/items/robot/items/hypo.dm
+++ b/code/game/objects/items/robot/items/hypo.dm
@@ -50,6 +50,7 @@
/datum/reagent/medicine/morphine,\
/datum/reagent/medicine/potass_iodide,\
/datum/reagent/medicine/syndicate_nanites,\
+ /datum/reagent/medicine/atropine,\
)
#define BASE_SERVICE_REAGENTS list(/datum/reagent/consumable/applejuice, /datum/reagent/consumable/banana,\
/datum/reagent/consumable/berryjuice, /datum/reagent/consumable/cherryjelly, /datum/reagent/consumable/coffee,\
diff --git a/code/game/objects/items/robot/items/tools.dm b/code/game/objects/items/robot/items/tools.dm
index 23fe0c97edf4..351949715451 100644
--- a/code/game/objects/items/robot/items/tools.dm
+++ b/code/game/objects/items/robot/items/tools.dm
@@ -252,7 +252,7 @@
return tool
-/obj/item/borg/cyborg_omnitool/attack_self(mob/user)
+/obj/item/borg/cyborg_omnitool/attack_self(mob/user, modifiers)
//build the radial menu options
var/list/radial_menu_options = list()
var/list/tool_map = list()
@@ -277,9 +277,8 @@
return ..()
var/mob/living/silicon/robot/user = usr
if (!(src in user.held_items))
- attack_self(user)
- . = ..()
- user.select_module(user.held_items.Find(src))
+ attack_self(user, modifiers)
+ return ..()
/obj/item/borg/cyborg_omnitool/update_icon_state()
if (reference)
@@ -334,19 +333,6 @@
. = ..()
RegisterSignal(src, COMSIG_SILICON_MODULE_ACTIVATION, PROC_REF(welder_toggle))
-/obj/item/borg/cyborg_omnitool/engineering/add_context(atom/source, list/context, obj/item/held_item, mob/user)
- . = ..()
- if (!issilicon(user) || tool_behaviour != TOOL_WELDER)
- return
-
- context[SCREENTIP_CONTEXT_CTRL_LMB] = "Toggle welder"
- return CONTEXTUAL_SCREENTIP_SET
-
-/obj/item/borg/cyborg_omnitool/engineering/examine(mob/user)
- . = ..()
- if(tool_behaviour == TOOL_WELDER)
- . += span_notice("Use [EXAMINE_HINT("Ctrl Click")] to toggle welder")
-
/obj/item/borg/cyborg_omnitool/engineering/update_overlays()
. = ..()
if(tool_behaviour == TOOL_WELDER)
@@ -354,24 +340,19 @@
if(tool?.welding)
. |= tool.update_overlays()
-/obj/item/borg/cyborg_omnitool/engineering/set_internal_tool(obj/item/tool)
- if(tool_behaviour == TOOL_WELDER)
- welder_toggle(src, FALSE)
+/obj/item/borg/cyborg_omnitool/engineering/attack_self(mob/user, modifiers)
+ if(tool_behaviour == TOOL_WELDER && LAZYACCESS(modifiers, LEFT_CLICK))
+ welder_toggle(src, null, user)
- . = ..()
+ return NONE
- if(tool_behaviour == TOOL_WELDER)
- welder_toggle(src, TRUE)
+ return ..()
-/obj/item/borg/cyborg_omnitool/engineering/item_ctrl_click(mob/user)
- . = NONE
+/obj/item/borg/cyborg_omnitool/engineering/set_internal_tool(obj/item/tool)
if(tool_behaviour == TOOL_WELDER)
- var/mob/living/silicon/robot/borgy = loc
- if(!istype(borgy) || borgy.module_active != src)
- return
+ welder_toggle(src, FALSE)
- welder_toggle(src, null)
- return CLICK_ACTION_SUCCESS
+ return ..()
///Reflects internal welder icon onto the omnitool
/obj/item/borg/cyborg_omnitool/engineering/proc/welder_update(source)
@@ -381,7 +362,7 @@
update_appearance(UPDATE_OVERLAYS)
///Toggles welder on/off when module slot is selected/deselected
-/obj/item/borg/cyborg_omnitool/engineering/proc/welder_toggle(datum/omnitool, state)
+/obj/item/borg/cyborg_omnitool/engineering/proc/welder_toggle(datum/omnitool, state, mob/self_user)
PRIVATE_PROC(TRUE)
SIGNAL_HANDLER
@@ -393,8 +374,9 @@
return
if(state)
- RegisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(welder_update))
- tool.switched_on(usr)
+ RegisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(welder_update), override = TRUE)
+ if(self_user)
+ tool.switched_on(self_user)
else
tool.switched_off()
UnregisterSignal(tool, COMSIG_ATOM_UPDATE_APPEARANCE)
diff --git a/code/game/objects/items/weaponry/melee/energy.dm b/code/game/objects/items/weaponry/melee/energy.dm
index ca7f75e93a90..93571d4d710a 100644
--- a/code/game/objects/items/weaponry/melee/energy.dm
+++ b/code/game/objects/items/weaponry/melee/energy.dm
@@ -222,48 +222,6 @@
return ..()
-/obj/item/melee/energy/sword/cyborg
- name = "cyborg energy sword"
- sword_color_icon = "red"
- /// The cell cost of hitting something.
- var/hitcost = 0.05 * STANDARD_CELL_CHARGE
-
-/obj/item/melee/energy/sword/cyborg/attack(mob/target, mob/living/silicon/robot/user)
- if(!user.cell)
- return
-
- var/obj/item/stock_parts/power_store/our_cell = user.cell
- if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) && !(our_cell.use(hitcost)))
- attack_self(user)
- to_chat(user, span_notice("It's out of charge!"))
- return
- return ..()
-
-/obj/item/melee/energy/sword/cyborg/cyborg_unequip(mob/user)
- if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
- return
- attack_self(user)
-
-/obj/item/melee/energy/sword/cyborg/saw //Used by medical Syndicate cyborgs
- name = "energy saw"
- desc = "For heavy duty cutting. It has a carbon-fiber blade in addition to a toggleable hard-light edge to dramatically increase sharpness."
- icon = 'icons/obj/medical/surgery_tools.dmi'
- icon_state = "esaw"
- hitsound = 'sound/items/weapons/circsawhit.ogg'
- force = 18
- hitcost = 0.075 * STANDARD_CELL_CHARGE // Costs more than a standard cyborg esword.
- w_class = WEIGHT_CLASS_NORMAL
- sharpness = SHARP_EDGED
- light_color = LIGHT_COLOR_LIGHT_CYAN
- tool_behaviour = TOOL_SAW
- toolspeed = 0.7 // Faster than a normal saw.
-
- active_force = 30
- sword_color_icon = null // Stops icon from breaking when turned on.
-
-/obj/item/melee/energy/sword/cyborg/saw/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
- return FALSE
-
// The colored energy swords we all know and love.
/obj/item/melee/energy/sword/saber
/// Assoc list of all possible saber colors to color define. If you add a new color, make sure to update /obj/item/toy/sword too!
@@ -325,6 +283,51 @@
to_chat(user, span_warning("RNBW_ENGAGE"))
update_appearance(UPDATE_ICON_STATE)
+/obj/item/melee/energy/sword/saber/cyborg
+ name = "cyborg energy sword"
+ hacked = TRUE
+ sword_color_icon = "rainbow"
+ /// The cell cost of hitting something.
+ var/hitcost = 0.05 * STANDARD_CELL_CHARGE
+
+/obj/item/melee/energy/sword/saber/cyborg/Initialize(mapload)
+ . = ..()
+ set_light_range(5) //Cyborgs don't have inhand sprites, so we compensate by making it glow brightly.
+
+/obj/item/melee/energy/sword/saber/cyborg/attack(mob/target, mob/living/silicon/robot/user)
+ if(!user.cell)
+ return
+
+ var/obj/item/stock_parts/power_store/our_cell = user.cell
+ if(HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) && !(our_cell.use(hitcost)))
+ attack_self(user)
+ to_chat(user, span_notice("It's out of charge!"))
+ return
+ return ..()
+
+/obj/item/melee/energy/sword/saber/cyborg/cyborg_unequip(mob/user)
+ if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
+ return
+ attack_self(user)
+
+/obj/item/melee/energy/sword/saber/cyborg/saw //Used by medical Syndicate cyborgs
+ name = "energy saw"
+ desc = "For heavy duty cutting. It has a carbon-fiber blade in addition to a toggleable hard-light edge to dramatically increase sharpness."
+ icon = 'icons/obj/medical/surgery_tools.dmi'
+ icon_state = "esaw"
+ hitsound = 'sound/items/weapons/circsawhit.ogg'
+ force = 18
+ hitcost = 0.075 * STANDARD_CELL_CHARGE // Costs more than a standard cyborg esword.
+ w_class = WEIGHT_CLASS_NORMAL
+ sharpness = SHARP_EDGED
+ light_color = LIGHT_COLOR_LIGHT_CYAN
+ tool_behaviour = TOOL_SAW
+ toolspeed = 0.7 // Faster than a normal saw.
+ hacked = FALSE
+ active_force = 30
+ block_chance = 0 //Unlike assault cyborgs, syndicate medical cyborgs don't get any blocking capabilities
+ sword_color_icon = null // Stops icon from breaking when turned on.
+
/obj/item/melee/energy/sword/pirate
name = "energy cutlass"
desc = "Arrrr matey."
diff --git a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
index 66f7e2780e56..83981f1beddf 100644
--- a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
+++ b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm
@@ -91,18 +91,20 @@
playsound(loc, 'sound/machines/chime.ogg', 50, FALSE, -5)
/// Does the MGS ! animation
-/atom/proc/do_alert_animation()
+/atom/proc/do_alert_animation(duration = 1 SECONDS)
var/mutable_appearance/alert = mutable_appearance('icons/obj/storage/closet.dmi', "cardboard_special")
SET_PLANE_EXPLICIT(alert, ABOVE_LIGHTING_PLANE, src)
- var/atom/movable/flick_visual/exclamation = flick_overlay_view(alert, 1 SECONDS)
+ var/atom/movable/flick_visual/exclamation = flick_overlay_view(alert, duration)
exclamation.alpha = 0
exclamation.pixel_x = -pixel_x
- animate(exclamation, pixel_z = 32, alpha = 255, time = 0.5 SECONDS, easing = ELASTIC_EASING)
+ animate(exclamation, pixel_z = 32, alpha = 255, time = duration * 0.5, easing = ELASTIC_EASING)
+ animate(time = duration * 0.35)
+ animate(pixel_z = 64, alpha = 0, time = duration * 0.15, easing = SINE_EASING)
// We use this list to update plane values on parent z change, which is why we need the timer too
// I'm sorry :(
LAZYADD(update_on_z, exclamation)
// Intentionally less time then the flick so we don't get weird shit
- addtimer(CALLBACK(src, PROC_REF(forget_alert), exclamation), 0.8 SECONDS, TIMER_CLIENT_TIME)
+ addtimer(CALLBACK(src, PROC_REF(forget_alert), exclamation), max(0.05 SECONDS, duration - 0.1 SECONDS), TIMER_CLIENT_TIME)
/atom/proc/forget_alert(atom/movable/flick_visual/exclamation)
LAZYREMOVE(update_on_z, exclamation)
diff --git a/code/game/objects/structures/icemoon/cave_entrance.dm b/code/game/objects/structures/icemoon/cave_entrance.dm
index 408e409e5571..b58fff6bb705 100644
--- a/code/game/objects/structures/icemoon/cave_entrance.dm
+++ b/code/game/objects/structures/icemoon/cave_entrance.dm
@@ -66,7 +66,7 @@ GLOBAL_LIST_INIT(ore_probability, list(
/obj/structure/spawner/ice_moon/polarbear
max_mobs = 1
spawn_time = 60 SECONDS
- mob_types = list(/mob/living/simple_animal/hostile/asteroid/polarbear)
+ mob_types = list(/mob/living/basic/mining/polarbear)
mob_gps_id = "BR" // bear
/obj/structure/spawner/ice_moon/polarbear/clear_rock()
diff --git a/code/game/objects/structures/lavaland/ore_vent.dm b/code/game/objects/structures/lavaland/ore_vent.dm
index 244e88102be5..3ab9542f6301 100644
--- a/code/game/objects/structures/lavaland/ore_vent.dm
+++ b/code/game/objects/structures/lavaland/ore_vent.dm
@@ -677,7 +677,7 @@
/mob/living/basic/mining/lobstrosity,
/mob/living/basic/mining/legion/snow/spawner_made,
/mob/living/basic/mining/wolf,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
+ /mob/living/basic/mining/polarbear,
)
ore_vent_options = list(
SMALL_VENT_TYPE,
@@ -690,7 +690,7 @@
/mob/living/basic/mining/legion/snow/spawner_made,
/mob/living/basic/mining/ice_demon,
/mob/living/basic/mining/wolf,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
+ /mob/living/basic/mining/polarbear,
)
ore_vent_options = list(
SMALL_VENT_TYPE = 3,
diff --git a/code/game/objects/structures/mystery_box.dm b/code/game/objects/structures/mystery_box.dm
index 28700113381e..0e153b328131 100644
--- a/code/game/objects/structures/mystery_box.dm
+++ b/code/game/objects/structures/mystery_box.dm
@@ -189,7 +189,7 @@ GLOBAL_LIST_INIT(mystery_fishing, list(
presented_item.vis_flags = VIS_INHERIT_PLANE
vis_contents += presented_item
presented_item.start_animation(src)
- current_sound_channel = SSsounds.reserve_sound_channel(src)
+ current_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
playsound(src, open_sound, 70, FALSE, channel = current_sound_channel, falloff_exponent = 10)
playsound(src, crate_open_sound, 80)
if(user.mind)
diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm
index 451ee6d60a49..92b2e6f19e9b 100644
--- a/code/game/sound/sound.dm
+++ b/code/game/sound/sound.dm
@@ -145,7 +145,7 @@
//End Atmosphere affecting sound
if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
- return //No sound
+ return FALSE
var/dx = turf_source.x - turf_loc.x // Hearing from the right/left
sound_to_use.x = dx * distance_multiplier
@@ -175,12 +175,13 @@
var/client_volume_modifier = client.prefs.read_preference(volume_preference)
sound_to_use.volume *= (client_volume_modifier / 100)
if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN)
- return
+ return FALSE
if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED))
to_chat(src, span_admin("Max Range-[max_distance] Distance-[distance] Vol-[round(sound_to_use.volume, 0.01)] Sound-[sound_to_use.file]"))
SEND_SOUND(src, sound_to_use)
+ return TRUE
/proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S, datum/preference/numeric/volume/volume_preference = null)
if(!S)
diff --git a/code/game/turfs/open/asteroid.dm b/code/game/turfs/open/asteroid.dm
index 9ac571c2e6dd..c441871caa82 100644
--- a/code/game/turfs/open/asteroid.dm
+++ b/code/game/turfs/open/asteroid.dm
@@ -140,6 +140,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
base_icon_state = "basalt"
floor_variance = 15
dig_result = /obj/item/stack/ore/glass/basalt
+ smoothing_groups = SMOOTH_GROUP_FLOOR_BASALT
/turf/open/misc/asteroid/basalt/getDug()
. = ..()
@@ -267,7 +268,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
base_icon_state = "siderite"
layer = HIGH_TURF_LAYER
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_SIDERITE
- canSmoothWith = SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_CLOSED_TURFS
dig_result = /obj/item/stack/ore/glass/siderite
/turf/open/misc/asteroid/basalt/smooth/siderite/lava_land_surface
@@ -287,7 +288,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt)
icon_state = "shale-255"
base_icon_state = "shale"
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_SHALE
- canSmoothWith = SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_CLOSED_TURFS
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_CLOSED_TURFS
/turf/open/misc/asteroid/basalt/smooth/shale/lava_land_surface
initial_gas_mix = LAVALAND_DEFAULT_ATMOS
diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm
index a931748a577c..63a4e1e5045d 100644
--- a/code/game/turfs/open/lava.dm
+++ b/code/game/turfs/open/lava.dm
@@ -147,11 +147,6 @@
return result
-/turf/open/lava/smooth_icon()
- . = ..()
- mask_state = icon_state
- update_appearance(~UPDATE_SMOOTHING)
-
/turf/open/lava/ex_act(severity, target)
if(fish_source)
GLOB.preset_fish_sources[fish_source].spawn_reward_from_explosion(src, severity)
@@ -375,10 +370,59 @@
base_icon_state = "lava"
smoothing_flags = SMOOTH_BITMASK | SMOOTH_BORDER
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_LAVA
- canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA
- underfloor_accessibility = 2 //This avoids strangeness when routing pipes / wires along catwalks over lava
+ canSmoothWith = SMOOTH_GROUP_FLOOR_LAVA + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS + SMOOTH_GROUP_RED_ROCK_WALLS + SMOOTH_GROUP_SHALE_WALLS
+ underfloor_accessibility = 2 // This avoids strangeness when routing pipes / wires along catwalks over lava
+ /// *Inverse* smoothing bitflag for basalt overlays
+ var/basalt_junction = NONE
+ /// *Inverse* smoothing bitflag for siderite overlays
+ var/siderite_junction = NONE
+ /// *Inverse* smoothing bitflag for shale overlays
+ var/shale_junction = NONE
+
+/turf/open/lava/smooth/bitmask_smooth()
+ . = ..()
+ basalt_junction = ALL_SMOOTHING_JUNCTIONS
+ siderite_junction = ALL_SMOOTHING_JUNCTIONS
+ shale_junction = ALL_SMOOTHING_JUNCTIONS
+ // We need to convert basalt/siderite/shale groups into a readable format
+ var/static/basalt_group = null
+ var/static/siderite_group = null
+ var/static/shale_group = null
+ if (isnull(basalt_group))
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS, basalt_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_RED_ROCK_WALLS, siderite_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_SHALE_WALLS, shale_group)
+ // After smoothing normally we can check our smoothed directions for possible basalt/siderite/shale tiles
+ for (var/check_dir in GLOB.alldirs)
+ var/junction = dir_to_junction(check_dir) | all_junctions_of_dir(check_dir)
+ if (!(junction & smoothing_junction))
+ continue
+ var/turf/to_smooth = get_step(src, check_dir)
+ if (!istype(to_smooth) || !to_smooth.smoothing_groups)
+ continue
+ for(var/key, group in to_smooth.smoothing_groups)
+ if (group & basalt_group[key])
+ basalt_junction &= ~junction
+ else if (group & siderite_group[key])
+ siderite_junction &= ~junction
+ else if (group & shale_group[key])
+ shale_junction &= ~junction
+
+/turf/open/lava/smooth/smooth_icon()
+ . = ..()
+ mask_state = "lava-[smoothing_junction & (basalt_junction & siderite_junction & shale_junction)]"
+ update_appearance(~UPDATE_SMOOTHING)
-/// Smooth lava needs to take after basalt in order to blend better. If you make a /turf/open/lava/smooth subtype for an area NOT surrounded by basalt; you should override this proc.
+/turf/open/lava/smooth/update_overlays()
+ . = ..()
+ if (basalt_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/basalt_outline.dmi', "basalt_outline-[basalt_junction]")
+ if (siderite_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/siderite_outline.dmi', "siderite_outline-[siderite_junction]")
+ if (shale_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/shale_outline.dmi', "shale_outline-[shale_junction]")
+
+/// Smooth lava needs to take after basalt in order to blend better.
/turf/open/lava/smooth/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
underlay_appearance.icon = /turf/open/misc/asteroid/basalt::icon
underlay_appearance.icon_state = /turf/open/misc/asteroid/basalt::icon_state
diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm
index ecf75011189c..21fe6b5b01b6 100644
--- a/code/game/turfs/open/water.dm
+++ b/code/game/turfs/open/water.dm
@@ -144,8 +144,56 @@
base_icon_state = "water_lavaland"
smoothing_flags = SMOOTH_BITMASK | SMOOTH_BORDER
smoothing_groups = SMOOTH_GROUP_TURF_OPEN + SMOOTH_GROUP_FLOOR_WATER_LAVALAND
- canSmoothWith = SMOOTH_GROUP_FLOOR_WATER_LAVALAND
+ canSmoothWith = SMOOTH_GROUP_FLOOR_WATER_LAVALAND + SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS + SMOOTH_GROUP_RED_ROCK_WALLS + SMOOTH_GROUP_SHALE_WALLS
fishing_datum = /datum/fish_source/ocean
+ /// *Inverse* smoothing bitflag for basalt overlays
+ var/basalt_junction = NONE
+ /// *Inverse* smoothing bitflag for siderite overlays
+ var/siderite_junction = NONE
+ /// *Inverse* smoothing bitflag for shale overlays
+ var/shale_junction = NONE
+
+/turf/open/water/lavaland_atmos/basalt/bitmask_smooth()
+ . = ..()
+ basalt_junction = ALL_SMOOTHING_JUNCTIONS
+ siderite_junction = ALL_SMOOTHING_JUNCTIONS
+ shale_junction = ALL_SMOOTHING_JUNCTIONS
+ // We need to convert basalt/siderite/shale groups into a readable format
+ var/static/basalt_group = null
+ var/static/siderite_group = null
+ var/static/shale_group = null
+ if (isnull(basalt_group))
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_BASALT + SMOOTH_GROUP_MINERAL_WALLS, basalt_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SIDERITE + SMOOTH_GROUP_RED_ROCK_WALLS, siderite_group)
+ SET_SMOOTHING_GROUPS(SMOOTH_GROUP_FLOOR_SHALE + SMOOTH_GROUP_SHALE_WALLS, shale_group)
+ // After smoothing normally we can check our smoothed directions for possible basalt/siderite/shale tiles
+ for (var/check_dir in GLOB.alldirs)
+ var/junction = dir_to_junction(check_dir) | all_junctions_of_dir(check_dir)
+ if (!(junction & smoothing_junction))
+ continue
+ var/turf/to_smooth = get_step(src, check_dir)
+ if (!istype(to_smooth) || !to_smooth.smoothing_groups)
+ continue
+ for(var/key, group in to_smooth.smoothing_groups)
+ if (group & basalt_group[key])
+ basalt_junction &= ~junction
+ else if (group & siderite_group[key])
+ siderite_junction &= ~junction
+ else if (group & shale_group[key])
+ shale_junction &= ~junction
+
+/turf/open/water/lavaland_atmos/basalt/smooth_icon()
+ . = ..()
+ update_appearance(~UPDATE_SMOOTHING)
+
+/turf/open/water/lavaland_atmos/basalt/update_overlays()
+ . = ..()
+ if (basalt_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/basalt_outline.dmi', "basalt_outline-[basalt_junction]")
+ if (siderite_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/siderite_outline.dmi', "siderite_outline-[siderite_junction]")
+ if (shale_junction != ALL_SMOOTHING_JUNCTIONS)
+ . += mutable_appearance('icons/turf/floors/shale_outline.dmi', "shale_outline-[shale_junction]")
/turf/open/water/lavaland_atmos/basalt/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir)
underlay_appearance.icon = /turf/open/misc/asteroid/basalt::icon
diff --git a/code/modules/admin/spawn_panel/spawn_panel.dm b/code/modules/admin/spawn_panel/spawn_panel.dm
index cdde4d0b9024..449fb8c0469a 100644
--- a/code/modules/admin/spawn_panel/spawn_panel.dm
+++ b/code/modules/admin/spawn_panel/spawn_panel.dm
@@ -131,7 +131,7 @@
var/list/spawn_params = list(
"selected_atom" = selected_atom,
"offset" = params["offset"],
- "atom_dir" = text2num(params["dir"]) || 1,
+ "atom_dir" = text2num(params["atom_dir"]) || 1,
"atom_amount" = text2num(params["atom_amount"]) || 1,
"atom_name" = params["atom_name"],
"where_target_type" = params["where_target_type"] || WHERE_FLOOR_BELOW_MOB,
diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm
index d45b0f0a37c7..ed7f8ffeec24 100644
--- a/code/modules/antagonists/_common/antag_spawner.dm
+++ b/code/modules/antagonists/_common/antag_spawner.dm
@@ -272,7 +272,7 @@
return
if(used)
return
- var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_ALIEN, role = ROLE_ALIEN, poll_time = 5 SECONDS, checked_target = src, alert_pic = demon_type, jump_target = src, role_name_text = initial(demon_type.name))
+ var/mob/chosen_one = SSpolling.poll_ghosts_for_target(check_jobban = ROLE_SENTIENCE, role = ROLE_SENTIENCE, poll_time = 5 SECONDS, checked_target = src, alert_pic = demon_type, jump_target = src, role_name_text = initial(demon_type.name))
if(chosen_one)
if(used || QDELETED(src))
return
diff --git a/code/modules/antagonists/wizard/slaughter_antag.dm b/code/modules/antagonists/wizard/slaughter_antag.dm
index effd5d2abf63..bce27599c8cd 100644
--- a/code/modules/antagonists/wizard/slaughter_antag.dm
+++ b/code/modules/antagonists/wizard/slaughter_antag.dm
@@ -3,7 +3,7 @@
roundend_category = "demons"
show_name_in_check_antagonists = TRUE
ui_name = "AntagInfoDemon"
- pref_flag = ROLE_ALIEN
+ pref_flag = ROLE_SENTIENCE
show_in_antagpanel = FALSE
show_to_ghosts = TRUE
antagpanel_category = ANTAG_GROUP_WIZARDS
diff --git a/code/modules/cargo/exports/lavaland.dm b/code/modules/cargo/exports/lavaland.dm
index 78f98cb04da6..576ca4d8c95a 100644
--- a/code/modules/cargo/exports/lavaland.dm
+++ b/code/modules/cargo/exports/lavaland.dm
@@ -45,6 +45,7 @@
/obj/item/jacobs_ladder,
/obj/item/borg/upgrade/modkit/lifesteal,
/obj/item/clockwork_alloy,
+ /obj/item/gun/energy/recharge/kinetic_accelerator/bdm,
)
/datum/export/lavaland/major //valuable chest/ruin loot, minor megafauna loot
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index a49169b90846..84dd50428ef1 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -79,6 +79,9 @@
//SOUND STUFF//
///////////////
+ /// Sound tokens currently playing for this client. Managed by /datum/sound_token and the soundtoken subsystem!! SOUND TOKENS 2026
+ var/list/datum/sound_token/sound_tokens = list()
+
////////////
//SECURITY//
////////////
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 2f0f10646e61..ff0077c7251d 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -635,6 +635,8 @@ GLOBAL_LIST_INIT(unrecommended_builds, list(
SSambience.remove_ambience_client(src)
SSmouse_entered.hovers -= src
SSping.currentrun -= src
+ SSsound_tokens.clients_needing_update -= src
+ SSsound_tokens.currentrun -= src
QDEL_NULL(view_size)
QDEL_NULL(void)
QDEL_NULL(tooltips)
diff --git a/code/modules/clothing/outfits/standard.dm b/code/modules/clothing/outfits/standard.dm
index 9dcb90b0e3d1..ba6fae4d6aa6 100644
--- a/code/modules/clothing/outfits/standard.dm
+++ b/code/modules/clothing/outfits/standard.dm
@@ -421,7 +421,7 @@
back = /obj/item/mod/control/pre_equipped/debug
backpack_contents = list(
/obj/item/melee/energy/axe = 1,
- /obj/item/storage/part_replacer/bluespace/tier4 = 1,
+ /obj/item/storage/part_replacer/bluespace/AdminDebug = 1,
/obj/item/gun/magic/wand/resurrection/debug = 1,
/obj/item/gun/magic/wand/death/debug = 1,
/obj/item/debug/human_spawner = 1,
@@ -452,7 +452,7 @@
back = /obj/item/mod/control/pre_equipped/administrative
backpack_contents = list(
/obj/item/melee/energy/axe = 1,
- /obj/item/storage/part_replacer/bluespace/tier4 = 1,
+ /obj/item/storage/part_replacer/bluespace/AdminDebug = 1,
/obj/item/gun/magic/wand/resurrection/debug = 1,
/obj/item/gun/magic/wand/death/debug = 1,
/obj/item/debug/human_spawner = 1,
diff --git a/code/modules/flufftext/dreaming.dm b/code/modules/flufftext/dreaming.dm
index 73520a1625c2..a97054affe09 100644
--- a/code/modules/flufftext/dreaming.dm
+++ b/code/modules/flufftext/dreaming.dm
@@ -183,7 +183,7 @@ GLOBAL_LIST_INIT(dreams, populate_dream_list())
addtimer(CALLBACK(src, PROC_REF(StopSound), dreamer), 5 SECONDS)
/datum/dream/hear_something/proc/ReserveSoundChannel()
- reserved_sound_channel = SSsounds.reserve_sound_channel(src)
+ reserved_sound_channel = SSsounds.reserve_sound_channel_for_datum(src)
UnregisterSignal(SSsounds, COMSIG_SUBSYSTEM_POST_INITIALIZE)
/datum/dream/hear_something/proc/PlayRandomSound(mob/living/carbon/dreamer)
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
index 47b0b834a1ab..4d9f358a6f6d 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_pastry.dm
@@ -760,3 +760,14 @@
)
result = /obj/item/food/cookie/macaron
dish_category = DISH_COOKIE
+
+/datum/crafting_recipe/food/apple_fritter
+ name = "Apple fritter"
+ reqs = list(
+ /obj/item/food/pastrybase = 1,
+ /obj/item/food/appleslice = 1,
+ )
+ result = /obj/item/food/apple_fritter
+ added_foodtypes = GRAIN|FRUIT|FRIED|BREAKFAST
+ dish_category = DISH_PASTRY
+ meal_category = MEAL_BREAKFAST
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 4701c0026733..2e9b24675f17 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -1,4 +1,3 @@
-
/obj/machinery/hydroponics
name = "hydroponics tray"
desc = "A basin used to grow plants in."
@@ -53,6 +52,8 @@
var/datum/weakref/lastuser
///If the tray generates nutrients and water on its own
var/self_sustaining = FALSE
+ ///If the tray is currently able to self-sustain
+ var/can_self_sustain = TRUE
///The icon state for the overlay used to represent that this tray is self-sustaining.
var/self_sustaining_overlay_icon_state = "gaia_blessing"
///Whether the plant is currently being pollinated or polinating the nearby plants
@@ -65,7 +66,12 @@
var/tray_flags = HYDROPONIC
///How many extra px to offset the plant sprite on the y axis, gets passed to the seed and added to the seeds offset
var/plant_offset_y = 0
-
+ ///Suffix things
+ var/alt_tray = FALSE
+ var/indicatorsuffix = ""
+ ///Soil things
+ var/obj/machinery/hydroponics/soil/current_soil = null
+ var/current_soil_overlay = null
/obj/machinery/hydroponics/Initialize(mapload)
//ALRIGHT YOU DEGENERATES. YOU HAD REAGENT HOLDERS FOR AT LEAST 4 YEARS AND NONE OF YOU MADE HYDROPONICS TRAYS HOLD NUTRIENT CHEMS INSTEAD OF USING "Points".
@@ -167,6 +173,12 @@
icon = 'icons/obj/service/hydroponics/equipment.dmi'
icon_state = "hydrotray3"
+/obj/machinery/hydroponics/constructable/oldstyle
+ name = "hydroponics tray"
+ icon = 'icons/obj/service/hydroponics/equipment.dmi'
+ icon_state = "hydrotray3-alt"
+ alt_tray = TRUE
+
/obj/machinery/hydroponics/constructable/fullupgrade
name = "deluxe hydroponics tray"
desc = "A basin used to grown plants in, packed full of cutting-edge technology."
@@ -179,6 +191,9 @@
AddComponent(/datum/component/usb_port, typecacheof(list(/obj/item/circuit_component/hydroponics), only_root_path = TRUE))
AddComponent(/datum/component/fishing_spot, /datum/fish_source/hydro_tray)
+/obj/machinery/hydroponics/constructable/on_deconstruction(disassembled)
+ current_soil?.forceMove(drop_location())
+
/obj/machinery/hydroponics/constructable/RefreshParts()
. = ..()
var/tmp_capacity = 0
@@ -186,8 +201,12 @@
tmp_capacity += matter_bin.tier
for (var/datum/stock_part/servo/servo in component_parts)
rating = servo.tier
- maxwater = tmp_capacity * 50 // Up to 300
- maxnutri = (tmp_capacity * 5) + STATIC_NUTRIENT_CAPACITY // Up to 50 Maximum
+ if(current_soil)
+ maxwater = current_soil.maxwater + ((tmp_capacity - 2)*50)
+ maxnutri = current_soil.maxnutri + ((tmp_capacity - 2)*5)
+ else
+ maxwater = tmp_capacity * 50
+ maxnutri = (tmp_capacity * 5) + STATIC_NUTRIENT_CAPACITY
reagents.maximum_volume = maxnutri
nutridrain = 1/rating
@@ -464,21 +483,32 @@
. += myseed.get_tray_overlay(age, plant_status, plant_offset_y)
. += update_status_light_overlays()
+ if(current_soil && current_soil_overlay)
+ . += mutable_appearance(icon, current_soil_overlay, OBJ_LAYER + 0.001)
+
if(self_sustaining && self_sustaining_overlay_icon_state)
- . += mutable_appearance(icon, self_sustaining_overlay_icon_state)
+ . += mutable_appearance(icon, self_sustaining_overlay_icon_state, OBJ_LAYER + 0.002)
+ . += emissive_appearance(icon, self_sustaining_overlay_icon_state, src, OBJ_LAYER + 0.002)
/obj/machinery/hydroponics/proc/update_status_light_overlays()
. = list()
+ if(alt_tray)
+ indicatorsuffix = "-alt"
if(waterlevel <= 10)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lowwater3[indicatorsuffix]", src, alpha = src.alpha)
if(reagents.total_volume <= 2)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lownutri3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lownutri3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lownutri3[indicatorsuffix]", src, alpha = src.alpha)
if(plant_health <= (myseed.endurance / 2))
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowhealth3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowhealth3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_lowhealth3[indicatorsuffix]", src, alpha = src.alpha)
if(weedlevel >= 5 || pestlevel >= 5 || toxic >= 40)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_alert3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_alert3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_alert3[indicatorsuffix]", src, alpha = src.alpha)
if(plant_status == HYDROTRAY_PLANT_HARVESTABLE)
- . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_harvest3")
+ . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_harvest3[indicatorsuffix]")
+ . += emissive_appearance(icon, "over_harvest3[indicatorsuffix]", src, alpha = src.alpha)
///Sets a new value for the myseed variable, which is the seed of the plant that's growing inside the tray.
/obj/machinery/hydroponics/proc/set_seed(obj/item/seeds/new_seed, delete_old_seed = TRUE)
@@ -1063,6 +1093,37 @@
flowergun.update_appearance()
to_chat(user, span_notice("[myseed.plantname]'s mutation was set to [locked_mutation], depleting [flowergun]'s cell!"))
return
+ else if(istype(O, /obj/item/soil_sack))
+ var/obj/item/soil_sack/oursoil = O
+
+ if(plant_status != HYDROTRAY_NO_PLANT)
+ balloon_alert(user, "remove the plants first!")
+ return
+
+ if(!isnull(current_soil))
+ balloon_alert(user, "tray is full")
+ return
+
+ balloon_alert(user, "filling the tray...")
+ if(!do_after(user, 2 SECONDS, src))
+ return
+
+ if(!oursoil.stored_soil)
+ balloon_alert(user, "sack is empty!")
+ return
+
+ current_soil = new oursoil.stored_soil(src)
+
+ RefreshParts()
+ tray_flags = current_soil.tray_flags
+ current_soil_overlay = "[current_soil.icon_state]_tray"
+ name = "botanic tray"
+ desc = "A basin used to grow plants in. Filled with [current_soil.name]."
+
+ qdel(oursoil)
+ update_appearance()
+ return
+
else
return ..()
@@ -1105,6 +1166,10 @@
update_use_power(NO_POWER_USE)
return CLICK_ACTION_BLOCKING
+ if(!can_self_sustain)
+ balloon_alert(user, "no self-sustain mode!")
+ return CLICK_ACTION_BLOCKING
+
set_self_sustaining(!self_sustaining)
to_chat(user, span_notice("You [self_sustaining ? "activate" : "deactivated"] [src]'s autogrow function[self_sustaining ? ", maintaining the tray's health while using high amounts of power" : ""]."))
return CLICK_ACTION_SUCCESS
diff --git a/code/modules/instruments/songs/_song.dm b/code/modules/instruments/songs/_song.dm
index f0df050b5c46..04786785bde6 100644
--- a/code/modules/instruments/songs/_song.dm
+++ b/code/modules/instruments/songs/_song.dm
@@ -120,6 +120,10 @@
var/cached_exponential_dropoff = 1.045
/////////////////////////////////////////////////////////////////////////
+
+ ///Rate at which volume goes down to 0. Not controlled in menu.
+ var/exponential_falloff = 4
+
/datum/song/New(atom/parent, list/instrument_ids, new_range)
SSinstruments.on_song_new(src)
lines = list()
diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm
index bb60d63c3b54..8513b8582170 100644
--- a/code/modules/instruments/songs/play_legacy.dm
+++ b/code/modules/instruments/songs/play_legacy.dm
@@ -88,5 +88,5 @@
var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments)
if(!pref_volume)
continue
- M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played)
+ M.playsound_local(source, null, volume * using_instrument.volume_multiplier * (pref_volume/100), sound_to_use = music_played, falloff_exponent = exponential_falloff)
// Could do environment and echo later but not for now
diff --git a/code/modules/instruments/songs/play_synthesized.dm b/code/modules/instruments/songs/play_synthesized.dm
index 6f9cf2280b28..92ae4d369c58 100644
--- a/code/modules/instruments/songs/play_synthesized.dm
+++ b/code/modules/instruments/songs/play_synthesized.dm
@@ -67,7 +67,7 @@
var/pref_volume = M?.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_instruments)
if(!pref_volume)
continue
- M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, null, channel, null, copy)
+ M.playsound_local(get_turf(parent), null, volume * (pref_volume/100), FALSE, K.frequency, exponential_falloff, channel, null, copy)
// Could do environment and echo later but not for now
/**
diff --git a/code/modules/lootpanel/_lootpanel.dm b/code/modules/lootpanel/_lootpanel.dm
index f7cfab8d8cd4..cc67696d6f22 100644
--- a/code/modules/lootpanel/_lootpanel.dm
+++ b/code/modules/lootpanel/_lootpanel.dm
@@ -23,6 +23,8 @@
/datum/lootpanel/Destroy(force)
+ SSlooting.backlog -= src
+ SSlooting.processing -= src
reset_contents()
owner = null
source_turf = null
diff --git a/code/modules/lootpanel/ss_looting.dm b/code/modules/lootpanel/ss_looting.dm
index eb8cff36e696..6f2481844a16 100644
--- a/code/modules/lootpanel/ss_looting.dm
+++ b/code/modules/lootpanel/ss_looting.dm
@@ -18,7 +18,7 @@ SUBSYSTEM_DEF(looting)
/datum/controller/subsystem/looting/fire(resumed)
- if(!length(backlog))
+ if(!length(backlog) && !length(processing))
return
if(!resumed)
diff --git a/code/modules/mapping/ruins.dm b/code/modules/mapping/ruins.dm
index 94d5dd6195c2..c6ce163abf52 100644
--- a/code/modules/mapping/ruins.dm
+++ b/code/modules/mapping/ruins.dm
@@ -212,6 +212,9 @@
var/top_right_y = bottom_left_y + current_pick.height - 1
log_mapping("Successfully placed [current_pick.name] ruin ([bottom_left_x],[bottom_left_y],[placed_turf.z] to [top_right_x],[top_right_y],[placed_turf.z]).")
+ ///Keep track of the active ruins so we can take it in account for map generation. Using bottom lef turf as key
+ SSmapping.active_ruins[locate(bottom_left_x, bottom_left_y, placed_turf.z)] = current_pick
+
//Update the available list
for(var/datum/map_template/ruin/R in ruins_available)
if(R.cost > budget || R.mineral_cost > mineral_budget)
diff --git a/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm b/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
index 5c69886c3711..09d15bef6ba2 100644
--- a/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
+++ b/code/modules/mining/equipment/kinetic_crusher/trophies_megafauna.dm
@@ -38,7 +38,7 @@
living_target.adjust_fire_loss(bonus_value, forced = TRUE)
/obj/item/crusher_trophy/tail_spike/proc/pushback(mob/living/target, mob/living/user)
- if(!QDELETED(target) && !QDELETED(user) && (!target.anchored || ismegafauna(target))) //megafauna will always be pushed
+ if(!QDELETED(target) && !QDELETED(user) && (!target.anchored || ismegafauna(target)) && target.move_resist < INFINITY) //megafauna will always be pushed
step(target, get_dir(user, target))
//bubblegum
diff --git a/code/modules/mob/living/basic/blob_minions/blob_mob.dm b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
index ef7184d09ac0..b13e9f693451 100644
--- a/code/modules/mob/living/basic/blob_minions/blob_mob.dm
+++ b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
@@ -21,7 +21,7 @@
initial_language_holder = /datum/language_holder/empty
can_buckle_to = FALSE
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
/// Size of cloud produced from a dying spore
var/death_cloud_size = BLOBMOB_CLOUD_NONE
var/loot = /obj/item/food/spore_sack
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
index 7462974381e0..0e9e734a21de 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/_blood_drunk_miner.dm
@@ -12,16 +12,17 @@ Difficulty: Medium
/mob/living/basic/boss/blood_drunk_miner
name = "blood-drunk miner"
desc = "A miner destined to wander forever, engaged in an endless hunt."
- health = 900
- maxHealth = 900
+ health = 1300
+ maxHealth = 1300
icon_state = "miner"
icon_living = "miner"
+ base_icon_state = "miner"
icon = 'icons/mob/simple/broadMobs.dmi'
health_doll_icon = "miner"
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_SPECIAL|MOB_MINING
light_color = COLOR_LIGHT_GRAYISH_RED
speak_emote = list("roars")
- speed = 3
+ speed = 2.5
pixel_x = -16
base_pixel_x = -16
basic_mob_flags = DEL_ON_DEATH
@@ -43,7 +44,7 @@ Difficulty: Medium
victor_memory_type = /datum/memory/megafauna_slayer
crusher_loot = list(/obj/item/crusher_trophy/miner_eye, /obj/item/knife/hunting/wildhunter)
- regular_loot = list(/obj/item/melee/cleaving_saw, /obj/item/gun/energy/recharge/kinetic_accelerator)
+ regular_loot = list(/obj/item/melee/cleaving_saw, /obj/item/gun/energy/recharge/kinetic_accelerator/bdm)
/// Their little saw
var/obj/item/melee/cleaving_saw/miner/miner_saw
@@ -87,9 +88,9 @@ Difficulty: Medium
/// Returns a list of innate actions for the blood-drunk miner.
/mob/living/basic/boss/blood_drunk_miner/proc/get_innate_actions()
- var/static/list/innate_abilities = list(
- /datum/action/cooldown/mob_cooldown/dash = BB_BDM_DASH_ABILITY,
- /datum/action/cooldown/mob_cooldown/projectile_attack/kinetic_accelerator = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
+ var/list/innate_abilities = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = BB_BDM_DASH_ABILITY,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
/datum/action/cooldown/mob_cooldown/dash_attack = BB_BDM_DASH_ATTACK_ABILITY,
/datum/action/cooldown/mob_cooldown/transform_weapon = BB_BDM_TRANSFORM_WEAPON_ABILITY,
)
@@ -101,6 +102,13 @@ Difficulty: Medium
INVOKE_ASYNC(ai_controller.blackboard[BB_BDM_TRANSFORM_WEAPON_ABILITY], TYPE_PROC_REF(/datum/action, Trigger), src, NONE)
+/mob/living/basic/boss/blood_drunk_miner/proc/transform_saw()
+ miner_saw.attack_self(src)
+ var/saw_open = HAS_TRAIT(miner_saw, TRAIT_TRANSFORM_ACTIVE)
+ rapid_melee_hits = saw_open ? 3 : 5
+ icon_state = "[base_icon_state][saw_open ? "_transformed":""]"
+ icon_living = "[base_icon_state][saw_open ? "_transformed":""]"
+
/mob/living/basic/boss/blood_drunk_miner/ex_act(severity, target)
var/datum/action/cooldown/mob_cooldown/dash_ability = ai_controller.blackboard[BB_BDM_DASH_ABILITY]
if(dash_ability.Trigger(target = target))
@@ -145,28 +153,58 @@ Difficulty: Medium
/// Namely, we just use the miner saw to rapidly hit the target multiple times
/mob/living/basic/boss/blood_drunk_miner/proc/attack_override(mob/living/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(!istype(target, /mob/living))
+
+ if(!isliving(target))
return
var/mob/living/victim = target
- if(should_devour(target))
- devour(target)
+ if(should_devour(victim))
+ devour(victim)
return COMPONENT_HOSTILE_NO_ATTACK
+ do_chain_attack(victim, modifiers)
+ return COMPONENT_HOSTILE_NO_ATTACK
+
+/mob/living/basic/boss/blood_drunk_miner/proc/do_chain_attack(mob/living/victim, modifiers, sequence_hit = 1)
+ if (!Adjacent(victim))
+ post_attack_effects(victim, modifiers)
+ return
+
changeNext_move(CLICK_CD_MELEE)
victim.visible_message(
span_danger("[src] slashes at [victim] with [p_their()] cleaving saw!"),
span_userdanger("You are slashed at by [src]'s cleaving saw!"),
)
- var/datum/callback/melee_callback = CALLBACK(miner_saw, TYPE_PROC_REF(/obj/item/melee/cleaving_saw/miner, melee_attack_chain), src, victim, modifiers)
- var/delay = 0.2 SECONDS
- for(var/i in 1 to rapid_melee_hits)
- addtimer(melee_callback, (i - 1) * delay)
+ var/delay = HAS_TRAIT(miner_saw, TRAIT_TRANSFORM_ACTIVE) ? 0.5 SECONDS : 0.3 SECONDS
+ apply_status_effect(/datum/status_effect/saw_slashes_slowdown, delay)
+ INVOKE_ASYNC(miner_saw, TYPE_PROC_REF(/obj/item/melee/cleaving_saw/miner, melee_attack_chain), src, victim, modifiers)
- post_attack_effects(victim, modifiers)
+ if (sequence_hit >= rapid_melee_hits)
+ post_attack_effects(victim, modifiers)
+ return
- return COMPONENT_HOSTILE_NO_ATTACK
+ addtimer(CALLBACK(src, PROC_REF(do_chain_attack), victim, modifiers, sequence_hit + 1), delay)
+
+/datum/status_effect/saw_slashes_slowdown
+ id = "saw_slashes_slowdown"
+ alert_type = null
+ status_type = STATUS_EFFECT_REFRESH
+
+/datum/status_effect/saw_slashes_slowdown/on_creation(mob/living/new_owner, new_duration)
+ duration = new_duration
+ return ..()
+
+/datum/status_effect/saw_slashes_slowdown/refresh(effect, new_duration)
+ duration = new_duration
+
+/datum/status_effect/saw_slashes_slowdown/on_apply()
+ . = ..()
+ owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/saw_slashes_slowdown)
+
+/datum/status_effect/saw_slashes_slowdown/on_remove()
+ . = ..()
+ owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/saw_slashes_slowdown)
/// Hook for potential additional behaviors after attacking
/mob/living/basic/boss/blood_drunk_miner/proc/post_attack_effects(mob/living/victim, list/modifiers)
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm
new file mode 100644
index 000000000000..468d01ce3a98
--- /dev/null
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_actions.dm
@@ -0,0 +1,82 @@
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner
+ cooldown_time = 1.5 SECONDS
+ charge_delay = 0.1 SECONDS
+ shake_duration = 0.2 SECONDS // A bit longer so he shakes during the dash too
+ charge_distance = 6
+ // Don't stun ourselves or the target
+ recoil_duration = -1
+ knockdown_duration = -1
+ destroy_objects = FALSE
+ charge_damage = 0
+ charge_speed = 0.3
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner/hit_target(atom/movable/source, atom/target, damage_dealt)
+ . = ..()
+ if(!isbasicmob(source) || !isliving(target))
+ return
+ var/mob/living/basic/basic_source = source
+ basic_source.melee_attack(target, ignore_cooldown = TRUE)
+
+/datum/action/cooldown/mob_cooldown/transform_weapon
+ name = "Transform Weapon"
+ button_icon = 'icons/obj/mining_zones/artefacts.dmi'
+ button_icon_state = "cleaving_saw"
+ desc = "Transform weapon into a different state."
+ cooldown_time = 5 SECONDS
+ shared_cooldown = MOB_SHARED_COOLDOWN_2
+ /// The max possible cooldown, cooldown is random between the default cooldown time and this
+ var/max_cooldown_time = 10 SECONDS
+
+/datum/action/cooldown/mob_cooldown/transform_weapon/Activate(atom/target_atom)
+ disable_cooldown_actions()
+ do_transform()
+ StartCooldown(rand(cooldown_time, max_cooldown_time), 0)
+ enable_cooldown_actions()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/transform_weapon/proc/do_transform()
+ if(!istype(owner, /mob/living/basic/boss/blood_drunk_miner))
+ return
+ var/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner = owner
+ blood_drunk_miner.transform_saw()
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator
+ name = "Fire Kinetic Accelerator"
+ desc = "Fires a kinetic accelerator projectile at the target."
+ button_icon = 'icons/obj/weapons/guns/energy.dmi'
+ button_icon_state = "kineticgun"
+ cooldown_time = 1.5 SECONDS
+ projectile_type = /obj/projectile/kinetic/miner
+ projectile_sound = 'sound/items/weapons/kinetic_accel.ogg'
+ shot_count = 3
+ shot_delay = 0.15 SECONDS
+ default_projectile_spread = 10
+ can_move = FALSE
+ /// Delay for the alert
+ var/alert_delay = 0.5 SECONDS
+ /// Delay before we start shooting during which we cannot move
+ var/prefire_delay = 0.2 SECONDS
+ /// Delay before the user can move or act again after firing
+ var/reload_delay = 0.1 SECONDS
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/Activate(atom/target_atom)
+ owner.visible_message(span_danger("[owner] fires the proto-kinetic accelerator!"))
+ owner.face_atom(target_atom)
+ owner.do_alert_animation(alert_delay + (shot_count - 1) * shot_delay)
+ disable_cooldown_actions()
+ if (alert_delay > prefire_delay) // As to delay movement blocking
+ SLEEP_CHECK_DEATH(alert_delay - prefire_delay, owner)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/attack_sequence(mob/living/firer, atom/target)
+ SLEEP_CHECK_DEATH(prefire_delay, firer)
+ . = ..()
+ sleep(reload_delay)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/shoot_projectile(atom/origin, atom/target, set_angle, mob/firer, projectile_spread, speed_multiplier, override_projectile_type, override_homing)
+ . = ..()
+ new /obj/effect/temp_visual/dir_setting/firing_effect(get_turf(firer), firer.dir)
+
+/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst
+ shot_count = 5
+ shot_delay = 0.1 SECONDS
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
index f41400aa52a0..64efdb0b8a96 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_ai.dm
@@ -12,7 +12,7 @@
BB_BDM_RANGED_ATTACK_COOLDOWN = 0,
)
- movement_delay = 0.3 SECONDS
+ movement_delay = 0.25 SECONDS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
@@ -63,4 +63,4 @@
return TRUE
/datum/ai_controller/blood_drunk_miner/doom
- movement_delay = 0.8 SECONDS
+ movement_delay = 0.5 SECONDS
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
index 81b49ea593a7..df5028557705 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_objects.dm
@@ -1,15 +1,10 @@
/// A slightly nerfed saw as the normal one is much too murdery.
/obj/item/melee/cleaving_saw/miner
- force = 6
- open_force = 10
-
-/obj/item/melee/cleaving_saw/miner/attack(mob/living/target, mob/living/carbon/human/user)
- target.add_stun_absorption(source = "miner", duration = 1 SECONDS, priority = INFINITY)
- return ..()
+ force = 8
+ open_force = 12
/obj/projectile/kinetic/miner
- damage = 20
- speed = 1.1
+ damage = 18
icon_state = "ka_tracer"
range = 4
diff --git a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
index a9e6c4d50b11..414fafe2b49f 100644
--- a/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
+++ b/code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_subtypes.dm
@@ -1,6 +1,6 @@
#define HUNTER_DASH_PROBABILITY 12
-/// heals slightly on melee hits
+/// Heals slightly on melee hits
/mob/living/basic/boss/blood_drunk_miner/guidance
/mob/living/basic/boss/blood_drunk_miner/guidance/attack_override(mob/living/source, atom/target, proximity, modifiers)
@@ -19,10 +19,11 @@
if(!isnull(dash_attack))
INVOKE_ASYNC(dash_attack, TYPE_PROC_REF(/datum/action, Trigger), src, NONE, victim)
+/// Slow but constantly dashes and has longer barrages
/mob/living/basic/boss/blood_drunk_miner/doom
name = "hostile-environment miner"
desc = "A miner destined to hop across dimensions for all eternity, hunting anomalous creatures."
- speed = 8
+ speed = 5
ranged_attack_cooldown_duration = 0.8 SECONDS
ai_controller = /datum/ai_controller/blood_drunk_miner/doom
@@ -32,4 +33,17 @@
if(!isnull(dash_ability))
dash_ability.cooldown_time = 0.8 SECONDS
+ var/datum/action/cooldown/dash_attack = ai_controller.blackboard[BB_BDM_DASH_ATTACK_ABILITY]
+ if(!isnull(dash_attack))
+ dash_attack.cooldown_time = 4 SECONDS
+
+/mob/living/basic/boss/blood_drunk_miner/doom/get_innate_actions()
+ var/list/innate_abilities = list(
+ /datum/action/cooldown/mob_cooldown/charge/basic_charge/blood_drunk_miner = BB_BDM_DASH_ABILITY,
+ /datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/kinetic_accelerator/long_burst = BB_BDM_KINETIC_ACCELERATOR_ABILITY,
+ /datum/action/cooldown/mob_cooldown/dash_attack = BB_BDM_DASH_ATTACK_ABILITY,
+ /datum/action/cooldown/mob_cooldown/transform_weapon = BB_BDM_TRANSFORM_WEAPON_ABILITY,
+ )
+ return innate_abilities
+
#undef HUNTER_DASH_PROBABILITY
diff --git a/code/modules/mob/living/basic/boss/thing/thing_ai.dm b/code/modules/mob/living/basic/boss/thing/thing_ai.dm
index be6033ec2b26..e8956cac2b13 100644
--- a/code/modules/mob/living/basic/boss/thing/thing_ai.dm
+++ b/code/modules/mob/living/basic/boss/thing/thing_ai.dm
@@ -5,14 +5,15 @@
BB_THETHING_ATTACKMODE = TRUE, //Whether we are using our melee abilities right now
BB_THETHING_NOAOE = TRUE, // Restricts us to only melee abilities
BB_THETHING_LASTAOE = null, // Last AOE ability key executed
- BB_AGGRO_RANGE = 6, //lets not execute hearers for a 16 tile radius
+ BB_AGGRO_RANGE = 16,
+ BB_AGGRO_GRAB_RANGE = 6,
)
ai_movement = /datum/ai_movement/basic_avoidance // dont need anything better because the arena is a square lol
idle_behavior = null
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
- /datum/ai_planning_subtree/simple_find_target/increased_range, //aggros at 6, sees 16 tiles
+ /datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/thing_boss_aoe,
/datum/ai_planning_subtree/thing_boss_melee,
)
diff --git a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
index 66cd498ccd60..c3a1c5d91395 100644
--- a/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
+++ b/code/modules/mob/living/basic/farm_animals/bee/_bee.dm
@@ -199,7 +199,7 @@
/// Picks a random toxin and assigns it to the bee
/mob/living/basic/bee/proc/assign_random_toxin_reagent()
- assign_reagent(get_random_reagent_id(whitelist = subtypesof(/datum/reagent/toxin)))
+ assign_reagent(GLOB.chemical_reagents_list[get_random_reagent_id(whitelist = subtypesof(/datum/reagent/toxin))])
/mob/living/basic/bee/mutate()
. = ..()
diff --git a/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm b/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm
new file mode 100644
index 000000000000..b1dc69d06bf0
--- /dev/null
+++ b/code/modules/mob/living/basic/icemoon/polar_bear/polar_bear.dm
@@ -0,0 +1,72 @@
+// The taxonomy of polar vs space bear left to imagination of the reader
+/mob/living/basic/mining/polarbear
+ name = "polar bear"
+ desc = "An aggressive animal that defends its territory with incredible power. These beasts don't run from their enemies."
+ icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
+ icon_state = "polarbear"
+ icon_living = "polarbear"
+ icon_dead = "polarbear_dead"
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST|MOB_MINING
+
+ friendly_verb_continuous = "growls at"
+ friendly_verb_simple = "growl at"
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ attack_verb_continuous = "claws"
+ attack_verb_simple = "claw"
+ attack_sound = 'sound/items/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_CLAW
+
+ habitable_atmos = null
+ speed = 2
+ maxHealth = 300
+ health = 300
+ obj_damage = 40
+ melee_damage_lower = 25
+ melee_damage_upper = 25
+ wound_bonus = -5
+ exposed_wound_bonus = 10
+ sharpness = SHARP_EDGED
+
+ move_force = MOVE_FORCE_VERY_STRONG
+ move_resist = MOVE_FORCE_VERY_STRONG
+ pull_force = MOVE_FORCE_VERY_STRONG
+ butcher_results = list(/obj/item/food/meat/slab/bear = 3, /obj/item/stack/sheet/bone = 2)
+ guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/goliath_hide/polar_bear_hide = 1)
+ crusher_loot = /obj/item/crusher_trophy/bear_paw
+
+ ai_controller = /datum/ai_controller/basic_controller/polar
+
+/mob/living/basic/mining/polarbear/Initialize(mapload)
+ . = ..()
+
+ add_traits(list(TRAIT_SPACEWALK, TRAIT_SWIMMER, TRAIT_FENCE_CLIMBER, TRAIT_SNOWSTORM_IMMUNE), INNATE_TRAIT)
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BEAR, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+ AddElement(/datum/element/change_force_on_death, move_force = MOVE_FORCE_DEFAULT, move_resist = MOVE_RESIST_DEFAULT, pull_force = PULL_FORCE_DEFAULT)
+ AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_CLAW)
+
+/mob/living/basic/mining/polarbear/lesser
+ name = "magic polar bear"
+ desc = "It seems sentient somehow."
+ faction = list(FACTION_NEUTRAL)
+
+/datum/ai_controller/basic_controller/polar
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_AGGRO_RANGE = 2,
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/enrage,
+ /datum/ai_planning_subtree/escape_captivity,
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/random_speech/bear,
+ )
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
index ba4bb3b219d8..1a2272fd85b7 100644
--- a/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_actions.dm
@@ -266,6 +266,8 @@
cooldown_time = 10 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
+ /// Range in which we create spikes
+ var/spike_range = 7
/// Health threshold at which we reduce the amount of empty spots on the ground
var/health_threshold = 0.3
@@ -288,7 +290,7 @@
if (as_living.health / as_living.maxHealth <= health_threshold)
reduced_spawns = TRUE
- for (var/turf/open/target_turf in oview(7, owner_turf))
+ for (var/turf/open/target_turf in oview(spike_range, 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
diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
index 77da7086dfa6..5da4fcedf285 100644
--- a/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
+++ b/code/modules/mob/living/basic/lavaland/tendril/tendril_ai.dm
@@ -2,6 +2,8 @@
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
+ BB_AGGRO_RANGE = 9, // Keeps an eye on you even if you flee
+ BB_AGGRO_GRAB_RANGE = 5, // Only aggros if you get real close and personal
)
planning_subtrees = list(
@@ -22,7 +24,8 @@
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])
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ if (isnull(target) || get_dist(controller.pawn, target) > /obj/projectile/tentacle_lash::range)
return FALSE
return ..()
@@ -30,6 +33,8 @@
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])
+ var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
+ var/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/ability = controller.blackboard[ability_key]
+ if (isnull(target) || !istype(ability) || get_dist(controller.pawn, target) > ability.spike_range)
return FALSE
return ..()
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie.dm b/code/modules/mob/living/basic/pets/orbie/orbie.dm
index fbbdfd89d22d..a75f1f5b0a2c 100644
--- a/code/modules/mob/living/basic/pets/orbie/orbie.dm
+++ b/code/modules/mob/living/basic/pets/orbie/orbie.dm
@@ -21,7 +21,7 @@
pass_flags = PASSMOB
move_force = 0
move_resist = 0
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
minimum_survivable_temperature = TCMB
maximum_survivable_temperature = INFINITY
death_message = "fades out of existence!"
diff --git a/code/modules/mob/living/basic/space_fauna/lightgeist.dm b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
index 50d59efbf2a3..b86d50e2efba 100644
--- a/code/modules/mob/living/basic/space_fauna/lightgeist.dm
+++ b/code/modules/mob/living/basic/space_fauna/lightgeist.dm
@@ -41,7 +41,7 @@
minimum_survivable_temperature = 0
maximum_survivable_temperature = 1500
obj_damage = 0
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
environment_smash = ENVIRONMENT_SMASH_NONE
ai_controller = /datum/ai_controller/basic_controller/lightgeist
diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm
index 34579f82174b..39525bd46ab2 100644
--- a/code/modules/mob/living/init_signals.dm
+++ b/code/modules/mob/living/init_signals.dm
@@ -186,11 +186,13 @@
mobility_flags &= ~(MOBILITY_PULL)
if(pulling)
stop_pulling()
+ pull_force_change()
/// Called when [TRAIT_PULL_BLOCKED] is removed from the mob.
/mob/living/proc/on_pull_blocked_trait_loss(datum/source)
SIGNAL_HANDLER
mobility_flags |= MOBILITY_PULL
+ pull_force_change()
/// Called when [TRAIT_INCAPACITATED] is added to the mob.
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 17c77733e224..83561f37f9f4 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1134,7 +1134,9 @@
return
changeNext_move(CLICK_CD_RESIST)
- SEND_SIGNAL(src, COMSIG_LIVING_RESIST, src)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_RESIST) & COMPONENT_BLOCK_RESIST)
+ return
+
//resisting grabs (as if it helps anyone...)
if(!HAS_TRAIT(src, TRAIT_RESTRAINED) && pulledby)
log_combat(src, pulledby, "resisted grab")
@@ -2093,10 +2095,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
update_transform(var_value/current_size)
. = TRUE
if(NAMEOF(src, pull_force))
- if(var_value == 0) //no more pulling
- remove_verb(src, /mob/living/verb/pulled)
- else
- add_verb(src, /mob/living/verb/pulled)
+ set_pull_force(var_value)
. = TRUE
@@ -3041,3 +3040,15 @@ GLOBAL_LIST_EMPTY(fire_appearances)
if(HAS_TRAIT(src, TRAIT_ANALGESIA) && !force)
return
INVOKE_ASYNC(src, PROC_REF(emote), "scream")
+
+/mob/living/proc/set_pull_force(new_pull_force)
+ if(pull_force == new_pull_force)
+ return
+ pull_force = new_pull_force
+ pull_force_change()
+
+/mob/living/proc/pull_force_change()
+ if(!pull_force || HAS_TRAIT(src, TRAIT_PULL_BLOCKED))
+ remove_verb(src, /mob/living/verb/pulled)
+ else
+ add_verb(src, /mob/living/verb/pulled)
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index b3c899b25421..ab191bf71069 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -214,6 +214,18 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return NONE
+//Checks blockchance of any items in any module slots
+/mob/living/silicon/robot/check_block(atom/hit_by, damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration = 0, damage_type = BRUTE)
+ . = ..()
+ if(. == SUCCESSFUL_BLOCK)
+ return SUCCESSFUL_BLOCK
+
+ var/block_chance_modifier = round(damage / -3)
+ for(var/obj/item/module in held_items)
+ var/final_block_chance = module.block_chance - (clamp((armour_penetration - module.armour_penetration) / 2, 0, 100)) + block_chance_modifier
+ if(module.hit_reaction(src, hit_by, attack_text, final_block_chance, damage, attack_type, damage_type))
+ return SUCCESSFUL_BLOCK
+
// This has to go at the very end of interaction so we don't block every interaction with ID-like items
/mob/living/silicon/robot/base_item_interaction(mob/living/user, obj/item/tool, list/modifiers)
. = ..()
@@ -522,6 +534,8 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
return TRUE
/mob/living/silicon/robot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
+ if(check_block(hitting_projectile, hitting_projectile.damage, "\the [hitting_projectile]", PROJECTILE_ATTACK, hitting_projectile.armour_penetration, hitting_projectile.damage_type))
+ return ..(hitting_projectile, def_zone, piercing_hit, 100)
. = ..()
if(prob(25) || . != BULLET_ACT_HIT)
return
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index deee0d543053..af9b1e6d49fa 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -903,7 +903,7 @@
name = "Syndicate Assault"
basic_modules = list(
/obj/item/assembly/flash/cyborg,
- /obj/item/melee/energy/sword/cyborg,
+ /obj/item/melee/energy/sword/saber/cyborg,
/obj/item/gun/energy/printer,
/obj/item/gun/ballistic/revolver/grenadelauncher/cyborg,
/obj/item/card/emag,
@@ -936,7 +936,7 @@
/obj/item/borg/cyborg_omnitool/medical,
/obj/item/borg/cyborg_omnitool/medical,
/obj/item/blood_filter,
- /obj/item/melee/energy/sword/cyborg/saw,
+ /obj/item/melee/energy/sword/saber/cyborg/saw,
/obj/item/emergency_bed/silicon,
/obj/item/crowbar/cyborg,
/obj/item/extinguisher/mini,
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
deleted file mode 100644
index 8a73d1465966..000000000000
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/polarbear.dm
+++ /dev/null
@@ -1,67 +0,0 @@
-/mob/living/simple_animal/hostile/asteroid/polarbear
- name = "polar bear"
- desc = "An aggressive animal that defends its territory with incredible power. These beasts don't run from their enemies."
- icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
- icon_state = "polarbear"
- icon_living = "polarbear"
- icon_dead = "polarbear_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST|MOB_MINING
- mouse_opacity = MOUSE_OPACITY_ICON
- friendly_verb_continuous = "growls at"
- friendly_verb_simple = "growl at"
- speak_emote = list("growls")
- speed = 3
- move_to_delay = 8
- maxHealth = 300
- health = 300
- obj_damage = 40
- melee_damage_lower = 25
- melee_damage_upper = 25
- attack_verb_continuous = "claws"
- attack_verb_simple = "claw"
- attack_sound = 'sound/items/weapons/bladeslice.ogg'
- attack_vis_effect = ATTACK_EFFECT_CLAW
- vision_range = 2 // don't aggro unless you basically antagonize it, though they will kill you worse than a goliath will
- aggro_vision_range = 9
- move_force = MOVE_FORCE_VERY_STRONG
- move_resist = MOVE_FORCE_VERY_STRONG
- pull_force = MOVE_FORCE_VERY_STRONG
- butcher_results = list(/obj/item/food/meat/slab/bear = 3, /obj/item/stack/sheet/bone = 2)
- guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/goliath_hide/polar_bear_hide = 1)
- loot = list()
- crusher_loot = /obj/item/crusher_trophy/bear_paw
- stat_attack = HARD_CRIT
- robust_searching = TRUE
- footstep_type = FOOTSTEP_MOB_CLAW
- /// Message for when the polar bear starts to attack faster
- var/aggressive_message_said = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/Initialize(mapload)
- . = ..()
- AddElement(\
- /datum/element/change_force_on_death,\
- move_force = MOVE_FORCE_DEFAULT,\
- move_resist = MOVE_RESIST_DEFAULT,\
- pull_force = PULL_FORCE_DEFAULT,\
- )
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- . = ..()
- if(health > maxHealth*0.5)
- rapid_melee = initial(rapid_melee)
- return
- if(!aggressive_message_said && target)
- visible_message(span_danger("\The [src] gets an enraged look at [target]!"))
- aggressive_message_said = TRUE
- rapid_melee = 2
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/Life(seconds_per_tick = SSMOBS_DT)
- . = ..()
- if(!. || target)
- return
- aggressive_message_said = FALSE
-
-/mob/living/simple_animal/hostile/asteroid/polarbear/lesser
- name = "magic polar bear"
- desc = "It seems sentient somehow."
- faction = list(FACTION_NEUTRAL)
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index 2c5f0af4451a..50a15e3ed984 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -489,6 +489,49 @@
return quirk
return null
+/**
+ * get_quirk_string() is used to get a printable string of all the quirk traits someone has for certain criteria
+ *
+ * Arguments:
+ * * Medical- If we want the long, fancy descriptions that show up in medical records, or if not, just the name
+ * * Category- Which types of quirks we want to print out. Defaults to everything
+ * * from_scan- If the source of this call is like a health analyzer or HUD, in which case QUIRK_HIDE_FROM_MEDICAL hides the quirk.
+ */
+/mob/living/proc/get_quirk_string(medical = FALSE, category = CAT_QUIRK_ALL, from_scan = FALSE)
+ var/list/dat = list()
+ for(var/datum/quirk/candidate as anything in quirks)
+ if(from_scan && (candidate.quirk_flags & QUIRK_HIDE_FROM_SCAN))
+ continue
+ switch(category)
+ if(CAT_QUIRK_MAJOR_DISABILITY)
+ if(candidate.value >= -4)
+ continue
+ if(CAT_QUIRK_MINOR_DISABILITY)
+ if(!ISINRANGE(candidate.value, -4, -1))
+ continue
+ if(CAT_QUIRK_NOTES)
+ if(candidate.value < 0)
+ continue
+ dat += medical ? candidate.medical_record_text : candidate.name
+
+ if(!length(dat))
+ return medical ? "No issues have been declared." : "None"
+ return medical ? dat.Join("
") : dat.Join(", ")
+
+/mob/living/proc/cleanse_quirk_datums() //removes all trait datums
+ QDEL_LAZYLIST(quirks)
+
+/mob/living/proc/transfer_quirk_datums(mob/living/to_mob)
+ // We could be done before the client was moved or after the client was moved
+ var/datum/preferences/to_pass = client || to_mob.client
+
+ for(var/datum/quirk/quirk as anything in quirks)
+ if(quirk.quirk_flags & QUIRK_NO_TRANSFER)
+ continue
+ quirk.remove_from_current_holder(quirk_transfer = TRUE)
+ quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass)
+
+
/// Helper to easily add a personality by a typepath
/mob/living/proc/add_personality(personality_type)
var/datum/personality/personality = SSpersonalities.personalities_by_type[personality_type]
@@ -509,6 +552,14 @@
for(var/personality_type in personalities)
remove_personality(personality_type)
+/// Returns a string with the names of the personalities of this mob, and their description as tooltip
+/mob/living/proc/get_parsonality_string()
+ var/list/return_list = list()
+ for(var/personality_type in personalities)
+ var/datum/personality/personality = SSpersonalities.personalities_by_type[personality_type]
+ return_list += span_tooltip(personality.desc, personality.name)
+ return english_list(return_list)
+
/mob/living/proc/cure_husk(source)
REMOVE_TRAIT(src, TRAIT_HUSK, source)
if(HAS_TRAIT(src, TRAIT_HUSK))
diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm
index f47f316c078c..d020f9df0f2c 100644
--- a/code/modules/mob/mob_lists.dm
+++ b/code/modules/mob/mob_lists.dm
@@ -46,21 +46,26 @@
///Adds the cliented mob reference to the list of all player-mobs, besides to either the of dead or alive player-mob lists, as appropriate. Called on Login().
/mob/proc/add_to_player_list()
SHOULD_CALL_PARENT(TRUE)
+
+
GLOB.player_list |= src
if(stat == DEAD)
add_to_current_dead_players()
else
add_to_current_living_players()
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGIN, src)
///Removes the mob reference from the list of all player-mobs, besides from either the of dead or alive player-mob lists, as appropriate. Called on Logout().
/mob/proc/remove_from_player_list()
SHOULD_CALL_PARENT(TRUE)
+
GLOB.player_list -= src
GLOB.keyloop_list -= src
if(stat == DEAD)
remove_from_current_dead_players()
else
remove_from_current_living_players()
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAYER_LOGOUT, src)
///Adds the cliented mob reference to either the list of dead player-mobs or to the list of observers, depending on how they joined the game.
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 9d5bfd2a5b6c..93c9dce974ea 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -599,3 +599,8 @@
if(new_turf && (istype(new_turf, /turf/cordon/secret) || is_secret_level(new_turf.z)) && !client?.holder)
return
return ..()
+
+/mob/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if(client?.sound_tokens.len)
+ SSsound_tokens.clients_needing_update[client] = TRUE
diff --git a/code/modules/movespeed/modifiers/status_effects.dm b/code/modules/movespeed/modifiers/status_effects.dm
index df114aca4070..265362f7b8cf 100644
--- a/code/modules/movespeed/modifiers/status_effects.dm
+++ b/code/modules/movespeed/modifiers/status_effects.dm
@@ -41,6 +41,9 @@
/datum/movespeed_modifier/status_effect/tired_post_charge/lesser
multiplicative_slowdown = 2
+/datum/movespeed_modifier/status_effect/saw_slashes_slowdown
+ multiplicative_slowdown = 0.5
+
/// Get slower the more gold is in your system.
/datum/movespeed_modifier/status_effect/midas_blight
id = MOVESPEED_ID_MIDAS_BLIGHT
diff --git a/code/modules/pai/pai.dm b/code/modules/pai/pai.dm
index 2573d75f5e8c..37c9e943debf 100644
--- a/code/modules/pai/pai.dm
+++ b/code/modules/pai/pai.dm
@@ -25,7 +25,7 @@
move_resist = 0
name = "pAI"
pass_flags = PASSTABLE | PASSMOB
- pull_force = 0
+ pull_force = MOVE_FORCE_NONE
radio = /obj/item/radio/headset/silicon/pai
worn_slot_flags = ITEM_SLOT_HEAD
diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm
index eb0526e32ca1..8cdcc3584590 100644
--- a/code/modules/power/apc/apc_malf.dm
+++ b/code/modules/power/apc/apc_malf.dm
@@ -56,8 +56,6 @@
disk_pinpointers.switch_mode_to(TRACK_MALF_AI) //Pinpointer will track the shunted AI
var/datum/action/innate/core_return/return_action = new
return_action.Grant(occupier)
- SEND_SIGNAL(src, COMSIG_SILICON_AI_OCCUPY_APC, occupier)
- SEND_SIGNAL(occupier, COMSIG_SILICON_AI_OCCUPY_APC, occupier)
occupier.cancel_camera()
/obj/machinery/power/apc/proc/malfvacate(forced)
@@ -71,6 +69,12 @@
occupier.gib(DROP_ALL_REMAINS)
occupier = null
return
+
+ if(!occupier.nuking) //Pinpointers go back to tracking the nuke disk, as long as the AI (somehow) isn't mid-nuking.
+ for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list)
+ disk_pinpointers.switch_mode_to(TRACK_NUKE_DISK)
+ disk_pinpointers.alert = FALSE
+
if(occupier.linked_core)
occupier.shunted = FALSE
occupier.resolve_core_link()
@@ -78,10 +82,6 @@
else
stack_trace("An AI: [occupier] has vacated an APC with no linked core and without being gibbed.")
- if(!occupier.nuking) //Pinpointers go back to tracking the nuke disk, as long as the AI (somehow) isn't mid-nuking.
- for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list)
- disk_pinpointers.switch_mode_to(TRACK_NUKE_DISK)
- disk_pinpointers.alert = FALSE
/obj/machinery/power/apc/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
. = ..()
diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm
index 52184d2c9592..545855c30bfd 100644
--- a/code/modules/power/lighting/light.dm
+++ b/code/modules/power/lighting/light.dm
@@ -677,17 +677,19 @@
var/obj/item/light/light_tube = drop_light_tube()
return light_tube.attack_tk(user)
-// break the light and make sparks if was on
+// break the light and make sparks if was on, state is mutated BEFORE firing side-effects to prevent re-entrancy loops from synchronous signals.
/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = FALSE)
if(status == LIGHT_EMPTY || status == LIGHT_BROKEN)
return
+ var/was_ok = (status == LIGHT_OK || status == LIGHT_BURNED)
+ status = LIGHT_BROKEN
+
if(!skip_sound_and_sparks)
- if(status == LIGHT_OK || status == LIGHT_BURNED)
+ if(was_ok)
playsound(loc, 'sound/effects/glass/glasshit.ogg', 75, TRUE)
if(on)
do_sparks(3, TRUE, src)
- status = LIGHT_BROKEN
update()
/obj/machinery/light/proc/fix()
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index 80d9aa26d849..c91985e71ea5 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -80,7 +80,8 @@
to_chat(user, span_notice("You pry all the modifications out."))
I.play_tool_sound(src, 100)
for(var/obj/item/borg/upgrade/modkit/modkit_upgrade as anything in modkits)
- modkit_upgrade.forceMove(drop_location()) //uninstallation handled in Exited(), or /mob/living/silicon/robot/remove_from_upgrades() for borgs
+ if (modkit_upgrade.removable)
+ modkit_upgrade.forceMove(drop_location()) //uninstallation handled in Exited(), or /mob/living/silicon/robot/remove_from_upgrades() for borgs
else
to_chat(user, span_notice("There are no modifications currently installed."))
@@ -98,7 +99,9 @@
var/list/display_names = list()
var/list/items = list()
for(var/modkits_length in 1 to length(modkits))
- var/obj/item/thing = modkits[modkits_length]
+ var/obj/item/borg/upgrade/modkit/thing = modkits[modkits_length]
+ if (!thing.removable)
+ continue
display_names["[thing.name] ([modkits_length])"] = REF(thing)
var/image/item_image = image(icon = thing.icon, icon_state = thing.icon_state)
if(length(thing.overlays))
@@ -156,6 +159,16 @@
for(var/obj/item/borg/upgrade/modkit/modkit_upgrade as anything in modkits)
modkit_upgrade.modify_projectile(kinetic_projectile)
+/obj/item/gun/energy/recharge/kinetic_accelerator/bdm
+ name = "infernal proto-kinetic accelerator"
+ icon_state = "kineticgun_evil"
+ inhand_icon_state = "kineticgun_evil"
+
+/obj/item/gun/energy/recharge/kinetic_accelerator/bdm/Initialize(mapload)
+ . = ..()
+ var/obj/item/borg/upgrade/modkit/cooldown/repeater/bdm/repeater_mod = new()
+ repeater_mod.install(src)
+
/obj/item/gun/energy/recharge/kinetic_accelerator/cyborg
icon_state = "kineticgun_b"
holds_charge = TRUE
@@ -299,6 +312,8 @@
var/modifier = 1 //For use in any mod kit that has numerical modifiers
var/minebot_upgrade = TRUE
var/minebot_exclusive = FALSE
+ /// Can it be removed?
+ var/removable = TRUE
/obj/item/borg/upgrade/modkit/examine(mob/user)
. = ..()
@@ -320,10 +335,12 @@
. = TRUE
if(minebot_upgrade)
if(minebot_exclusive && !istype(KA.loc, /mob/living/basic/mining_drone))
- to_chat(user, span_notice("The modkit you're trying to install is only rated for minebot use."))
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install is only rated for minebot use."))
return FALSE
else if(istype(KA.loc, /mob/living/basic/mining_drone))
- to_chat(user, span_notice("The modkit you're trying to install is not rated for minebot use."))
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install is not rated for minebot use."))
return FALSE
var/type_to_limit = denied_type
@@ -337,21 +354,24 @@
if(istype(modkit_upgrade, type_to_limit))
number_of_denied++
if(maximum_of_type && number_of_denied >= maximum_of_type || !maximum_of_type && number_of_denied) //if we denied a type, or we have a maximum to reach, break
- . = FALSE
- break
-
- if(KA.get_remaining_mod_capacity() >= cost)
- if(.)
- if(transfer_to_loc && !user.transferItemToLoc(src, KA))
- return
- to_chat(user, span_notice("You install the modkit."))
- playsound(loc, 'sound/items/tools/screwdriver.ogg', 100, TRUE)
- KA.modkits |= src
- else
- to_chat(user, span_notice("The modkit you're trying to install would conflict with an already installed modkit. Remove existing modkits first."))
- else
+ if (user)
+ to_chat(user, span_notice("The modkit you're trying to install would conflict with an already installed modkit. Remove existing modkits first."))
+ return FALSE
+
+ if(KA.get_remaining_mod_capacity() < cost)
to_chat(user, span_notice("You don't have room([KA.get_remaining_mod_capacity()]% remaining, [cost]% needed) to install this modkit. Use a crowbar or right click with an empty hand to remove existing modkits."))
- . = FALSE
+ return FALSE
+
+ if(transfer_to_loc)
+ if (user && !user.transferItemToLoc(src, KA))
+ return FALSE
+ else if (!user)
+ forceMove(KA)
+
+ if (user)
+ to_chat(user, span_notice("You install the modkit."))
+ playsound(loc, 'sound/items/tools/screwdriver.ogg', 100, TRUE)
+ KA.modkits |= src
/obj/item/borg/upgrade/modkit/deactivate(mob/living/silicon/robot/R, user = usr)
. = ..()
@@ -554,6 +574,12 @@
KA.cell.use(KA.cell.charge)
KA.attempt_reload(KA.recharge_time * 0.25) //If you hit, the cooldown drops to 0.75 seconds.
+/obj/item/borg/upgrade/modkit/cooldown/repeater/bdm
+ name = "infernal repeater"
+ removable = FALSE
+ cost = 30
+ modifier = -10
+
/obj/item/borg/upgrade/modkit/lifesteal
name = "lifesteal crystal"
desc = "Causes kinetic accelerator shots to slightly heal the firer on striking a living target."
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 0f8f7b35177c..89e74b81a32e 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -154,9 +154,11 @@
/datum/reagent/medicine/cryoxadone/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
. = ..()
metabolization_rate = REAGENTS_METABOLISM * (0.00001 * (affected_mob.bodytemperature ** 2) + 0.5)
- if(affected_mob.bodytemperature >= T0C || !HAS_TRAIT(affected_mob, TRAIT_KNOCKEDOUT))
+ if(affected_mob.bodytemperature >= T0C)
return
var/power = -0.00003 * (affected_mob.bodytemperature ** 2) + 3
+ if(HAS_TRAIT(affected_mob, TRAIT_KNOCKEDOUT)) //Significantly more effective when unconscious
+ power *= 2
var/need_mob_update
need_mob_update = affected_mob.adjust_oxy_loss(-1.5 * power * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype, required_respiration_type = affected_respiration_type)
need_mob_update += affected_mob.adjust_brute_loss(-0.5 * power * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index b052473d55f8..efc98614861d 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -35,6 +35,8 @@
var/last_rigger = ""
/// is it climbable? some of our wall-mounted dispensers should not have this
var/climbable = FALSE
+ /// Flags passed to the reagents datum upon creation
+ var/reagent_flags = DRAINABLE | AMOUNT_VISIBLE
// This check is necessary for assemblies to automatically detect that we are compatible
/obj/structure/reagent_dispensers/IsSpecialAssembly()
@@ -152,7 +154,7 @@
UnregisterSignal(src, COMSIG_IGNITER_ACTIVATE)
/obj/structure/reagent_dispensers/Initialize(mapload)
- create_reagents(tank_volume, DRAINABLE | AMOUNT_VISIBLE)
+ create_reagents(tank_volume, reagent_flags)
if(reagent_id)
reagents.add_reagent(reagent_id, tank_volume)
. = ..()
@@ -358,6 +360,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/reagent_dispensers/wall/peppertank, 3
desc = "A machine that cools and dispenses liquids to drink. The 'hot' handle doesn't seem to do anything."
icon_state = "water_cooler"
anchored = TRUE
+ reagent_flags = DRAINABLE | TRANSPARENT
tank_volume = 200
can_be_tanked = FALSE
max_integrity = 150
diff --git a/code/modules/research/part_replacer.dm b/code/modules/research/part_replacer.dm
index 581c1d6e28be..3b022850f654 100644
--- a/code/modules/research/part_replacer.dm
+++ b/code/modules/research/part_replacer.dm
@@ -156,6 +156,16 @@
new /obj/item/stock_parts/power_store/battery/bluespace(src)
new /obj/item/stack/cable_coil/thirty(src)
+/obj/item/storage/part_replacer/bluespace/AdminDebug/PopulateContents()
+ for(var/i in 1 to 40)
+ new /obj/item/stock_parts/capacitor/quadratic(src)
+ new /obj/item/stock_parts/scanning_module/triphasic(src)
+ new /obj/item/stock_parts/servo/femto(src)
+ new /obj/item/stock_parts/micro_laser/quadultra(src)
+ new /obj/item/stock_parts/matter_bin/bluespace(src)
+ new /obj/item/stock_parts/power_store/cell/bluespace(src)
+ new /obj/item/stack/cable_coil/thirty(src)
+
//used in a cargo crate
/obj/item/storage/part_replacer/cargo/PopulateContents()
for(var/i in 1 to 10)
diff --git a/code/modules/spells/spell_types/shapeshift/polar_bear.dm b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
index 93e1f6cea917..65a7342054fe 100644
--- a/code/modules/spells/spell_types/shapeshift/polar_bear.dm
+++ b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
@@ -6,4 +6,4 @@
invocation_type = INVOCATION_EMOTE
spell_requirements = NONE
- possible_shapes = list(/mob/living/simple_animal/hostile/asteroid/polarbear/lesser)
+ possible_shapes = list(/mob/living/basic/mining/polarbear/lesser)
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 72e120160300..5b1794ef33bf 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -18,8 +18,6 @@
/mob/living/simple_animal/hostile/asteroid/elite/legionnaire,
/mob/living/simple_animal/hostile/asteroid/elite/legionnairehead,
/mob/living/simple_animal/hostile/asteroid/elite/pandora,
- /mob/living/simple_animal/hostile/asteroid/polarbear,
- /mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/megafauna,
/mob/living/simple_animal/hostile/megafauna/bubblegum,
/mob/living/simple_animal/hostile/megafauna/bubblegum/hallucination,
diff --git a/dependencies.sh b/dependencies.sh
index 2869f8c53d9d..bf5a66f6e7c7 100644
--- a/dependencies.sh
+++ b/dependencies.sh
@@ -8,7 +8,7 @@ export BYOND_MAJOR=516
export BYOND_MINOR=1659
#rust_g git tag
-export RUST_G_VERSION=4.2.0
+export RUST_G_VERSION=6.2.0
# node version
export NODE_VERSION_LTS=22.11.0
@@ -26,11 +26,11 @@ export PYTHON_VERSION=3.11.0
export DREAMLUAU_REPO="tgstation/dreamluau"
#dreamluau git tag
-export DREAMLUAU_VERSION=0.1.2
+export DREAMLUAU_VERSION=0.2.1
#hypnagogic repo
export CUTTER_REPO=spacestation13/hypnagogic
#hypnagogic git tag
-export CUTTER_VERSION=v5.0.0
+export CUTTER_VERSION=v5.0.1
diff --git a/dreamluau.dll b/dreamluau.dll
index cc2d56d2d1e4..6975fad39969 100644
Binary files a/dreamluau.dll and b/dreamluau.dll differ
diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml
index 576d4d9390e5..ca7ab1a64ccc 100644
--- a/html/changelogs/archive/2026-06.yml
+++ b/html/changelogs/archive/2026-06.yml
@@ -70,6 +70,8 @@
timothymtorres:
- spellcheck: Add security misspelling checks to CI
2026-06-04:
+ AnturK:
+ - balance: polar bears are tiny bit faster
Arturlang:
- bugfix: moonatics switching bodies (including slimeperson swap) giving you duplicate
action buttons
@@ -77,6 +79,17 @@
Frfor17:
- bugfix: fixed NT shop printing infiniti papers, now its linked to computer's(laptop,
pda, any) paper storage
+ JohnFulpWillard:
+ - qol: People who are handcuffed/resting/anyone who can't pull people won't be prompted
+ to pull things in the dropdown menu.
+ MelokG000:
+ - rscadd: added botanic trays - hydroponic trays with soil instead of water, just
+ fill a tray with any soil and done!
+ - image: resprited auto-growth mode and added emissives to it
+ - image: resprited hydroponic trays
+ - rscadd: added old-designed mode for hydroponic trays - use a screwdriver or a
+ plant analyzer on the circuit!
+ - image: added emissives to hydroponic tray indicators
QuiteLiterallyAnything:
- bugfix: Made discounts from toxins papers show up in research UIs properly.
- refactor: Refactored research techwebs such that toxins discounts are now stored
@@ -85,6 +98,14 @@
- bugfix: Fixed brimdust overlay on wide mobs leaking onto other objects
- bugfix: Drake swoop no longer misleadingly claims you fall into lava if it fails
to spawn said lava
+ - bugfix: Tendrils no longer try to attack you when you're out of their reach
+ - code_imp: Cleaned up The Thing's aggro code
+ - bugfix: Drake tail trophy no longer pushes immovable mobs
+ TheRyeGuyWhoWillNowDie:
+ - balance: instead of the hard requirement for sleeping, cryox's healing power is
+ doubled by being asleep instead
+ - bugfix: on default settings a cryo tube filled with cryoxadone can no longer softlock
+ you by outdamaging its own healing potential
Wisemonster:
- code_imp: Wizard Apprentices now use the mid-round wizard pref, instead of the
round start wizard pref.
@@ -92,3 +113,65 @@
- bugfix: Material spears no longer partially avoid the force malus from being unwielded
- balance: Smartfridges no longer block atmos when broken & no longer let creatures
pass until broken
+ - bugfix: Toxin bees get toxin
+2026-06-05:
+ Ghommie:
+ - qol: You can now review your character's personality from the mood message box
+ (by clicking the mood hud element).
+ Melbert:
+ - qol: Adds "cancel interaction" keybinding, unbound by default, it cancels (most)
+ interactions you're doing without needing to move or swap items.
+ SmArtKar:
+ - refactor: Implemented overlay-based smoothing for lavaland lava and water
+ - rscadd: Blood-drunk miner now drops an infernal PKA with an in-built improved
+ rapid repeater modification
+ - balance: Blood-drunk miner has been reworked with new attacks and patterns
+ - bugfix: Blood-drunk miner's combos are no longer guaranteed to land even if you
+ move away from it
+ UgoManzo:
+ - bugfix: Fixed a server crash caused by igniting plasma-covered floor lights.
+ Wisemonster:
+ - code_imp: Slaughter/laughter demons now use the sentient creature pref, instead
+ of the xenomorph pref
+ lelandkemble:
+ - map: Catwalk disposals outputs correctly.
+2026-06-06:
+ CabinetOnFire:
+ - refactor: Lavaland/Icemoon now uses a smarter algorithm to generate caves, making
+ them more interconnected and making ruins easier to locate.
+ - bugfix: wishgranter ruin now has /biome_replace subtypes on its floors/walls,
+ ensuring it spawns in correctly (if enabled in config)
+ CabinetOnFire, Kapu:
+ - sound: Changed our falloff to be less intense, as it was punching volume down
+ way too fast.
+ - refactor: Implements a system for spatial audio to improve our looping sounds
+ Floyd:
+ - code_imp: Rust-G has been updated to 6.2.0
+ Frfor17:
+ - qol: now Admin RPED has 40 parts each
+ - code_imp: now Admin outfit and Debug outfit(what is that) has AdminDebug RPED
+ with 40 parts, and not just tier4 RPED, which is like game thing
+ Melbert:
+ - bugfix: Fix moths being able to fly in space
+ QuiteLiterallyAnything:
+ - image: Added a directional sprite for coffins.
+ Rhials, Dendydoom:
+ - rscadd: Apple Fritters have been added to your local chef's cookbook!
+ SyncIt21:
+ - code_imp: borg attack chain now passes modifiers to its item `attack_self()` proc
+ - qol: omni tool welder now won't turn on automatically when switching to it via
+ radial menu or using inventory button. This stops accidental combustion in hazardous
+ areas
+ - qol: omni tool welder can now be turned on/off via LMB like regular welder & not
+ CTRL+LMB. Use RMB to open radial menu to switch to other tools when using welder
+ TealSeer:
+ - bugfix: fixed TGUI spawn panel direction option not working
+ TheRyeGuyWhoWillNowDie:
+ - balance: Syndicate assault cyborgs can now block attacks with their energy swords
+ - balance: Syndicate medical cyborgs now feature atropine in their hypospray to
+ prevent microbomb detonations.
+ UgoManzo:
+ - bugfix: Water coolers now properly reveal their custom reagents when examined
+ by ghosts or players with science goggles.
+ lelandkemble:
+ - bugfix: Pinpointers stop tracking a shunted ai when that ai is no longer shunted
diff --git a/icons/map_icons/clothing/_clothing.dmi b/icons/map_icons/clothing/_clothing.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/clothing/_clothing.dmi and b/icons/map_icons/clothing/_clothing.dmi differ
diff --git a/icons/map_icons/clothing/accessory.dmi b/icons/map_icons/clothing/accessory.dmi
index e41990215450..55df7680741f 100644
Binary files a/icons/map_icons/clothing/accessory.dmi and b/icons/map_icons/clothing/accessory.dmi differ
diff --git a/icons/map_icons/clothing/head/_head.dmi b/icons/map_icons/clothing/head/_head.dmi
index 40a524f88dc4..bf12dcac4524 100644
Binary files a/icons/map_icons/clothing/head/_head.dmi and b/icons/map_icons/clothing/head/_head.dmi differ
diff --git a/icons/map_icons/clothing/head/beret.dmi b/icons/map_icons/clothing/head/beret.dmi
index fb920ce83276..56e229a0badb 100644
Binary files a/icons/map_icons/clothing/head/beret.dmi and b/icons/map_icons/clothing/head/beret.dmi differ
diff --git a/icons/map_icons/clothing/mask.dmi b/icons/map_icons/clothing/mask.dmi
index 2841cb9effc4..660a0c223705 100644
Binary files a/icons/map_icons/clothing/mask.dmi and b/icons/map_icons/clothing/mask.dmi differ
diff --git a/icons/map_icons/clothing/neck.dmi b/icons/map_icons/clothing/neck.dmi
index 3a5094318ef1..b95176c3deda 100644
Binary files a/icons/map_icons/clothing/neck.dmi and b/icons/map_icons/clothing/neck.dmi differ
diff --git a/icons/map_icons/clothing/shoes.dmi b/icons/map_icons/clothing/shoes.dmi
index 6d790cf92eb1..8ada17028560 100644
Binary files a/icons/map_icons/clothing/shoes.dmi and b/icons/map_icons/clothing/shoes.dmi differ
diff --git a/icons/map_icons/clothing/suit/_suit.dmi b/icons/map_icons/clothing/suit/_suit.dmi
index cdbb28e990c8..62a890ded65f 100644
Binary files a/icons/map_icons/clothing/suit/_suit.dmi and b/icons/map_icons/clothing/suit/_suit.dmi differ
diff --git a/icons/map_icons/clothing/suit/costume.dmi b/icons/map_icons/clothing/suit/costume.dmi
index b47fc0c68ab3..b4cc21296e79 100644
Binary files a/icons/map_icons/clothing/suit/costume.dmi and b/icons/map_icons/clothing/suit/costume.dmi differ
diff --git a/icons/map_icons/clothing/under/_under.dmi b/icons/map_icons/clothing/under/_under.dmi
index 5fdc8b31664c..1ea52de10fa6 100644
Binary files a/icons/map_icons/clothing/under/_under.dmi and b/icons/map_icons/clothing/under/_under.dmi differ
diff --git a/icons/map_icons/clothing/under/color.dmi b/icons/map_icons/clothing/under/color.dmi
index 87189da75482..ec7ae58f833a 100644
Binary files a/icons/map_icons/clothing/under/color.dmi and b/icons/map_icons/clothing/under/color.dmi differ
diff --git a/icons/map_icons/clothing/under/costume.dmi b/icons/map_icons/clothing/under/costume.dmi
index 17e56c8eaa12..da6eaec2611b 100644
Binary files a/icons/map_icons/clothing/under/costume.dmi and b/icons/map_icons/clothing/under/costume.dmi differ
diff --git a/icons/map_icons/clothing/under/dress.dmi b/icons/map_icons/clothing/under/dress.dmi
index 14e11b72a7fc..15861b7f3581 100644
Binary files a/icons/map_icons/clothing/under/dress.dmi and b/icons/map_icons/clothing/under/dress.dmi differ
diff --git a/icons/map_icons/items/_item.dmi b/icons/map_icons/items/_item.dmi
index 16535bf205f1..6fad912023d0 100644
Binary files a/icons/map_icons/items/_item.dmi and b/icons/map_icons/items/_item.dmi differ
diff --git a/icons/map_icons/items/encryptionkey.dmi b/icons/map_icons/items/encryptionkey.dmi
index 360180773f39..4c6cd37c691e 100644
Binary files a/icons/map_icons/items/encryptionkey.dmi and b/icons/map_icons/items/encryptionkey.dmi differ
diff --git a/icons/map_icons/items/pda.dmi b/icons/map_icons/items/pda.dmi
index c28a9b7043fc..40ca680e0f6e 100644
Binary files a/icons/map_icons/items/pda.dmi and b/icons/map_icons/items/pda.dmi differ
diff --git a/icons/map_icons/mobs.dmi b/icons/map_icons/mobs.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/mobs.dmi and b/icons/map_icons/mobs.dmi differ
diff --git a/icons/map_icons/objects.dmi b/icons/map_icons/objects.dmi
index dd3a926e2a7c..c6f765f81e8b 100644
Binary files a/icons/map_icons/objects.dmi and b/icons/map_icons/objects.dmi differ
diff --git a/icons/map_icons/turfs.dmi b/icons/map_icons/turfs.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/turfs.dmi and b/icons/map_icons/turfs.dmi differ
diff --git a/icons/map_icons/unsorted.dmi b/icons/map_icons/unsorted.dmi
index 6a9956844f4d..778966219458 100644
Binary files a/icons/map_icons/unsorted.dmi and b/icons/map_icons/unsorted.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_lefthand.dmi b/icons/mob/inhands/weapons/guns_lefthand.dmi
index de6061573904..df7edf9077b7 100644
Binary files a/icons/mob/inhands/weapons/guns_lefthand.dmi and b/icons/mob/inhands/weapons/guns_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/guns_righthand.dmi b/icons/mob/inhands/weapons/guns_righthand.dmi
index 4e97ea35e56a..62518d7646bf 100644
Binary files a/icons/mob/inhands/weapons/guns_righthand.dmi and b/icons/mob/inhands/weapons/guns_righthand.dmi differ
diff --git a/icons/mob/simple/broadMobs.dmi b/icons/mob/simple/broadMobs.dmi
index 00d01226f8ee..ae99a6ad7f05 100644
Binary files a/icons/mob/simple/broadMobs.dmi and b/icons/mob/simple/broadMobs.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index 84c9248a3641..0ed1aaae4b97 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/service/hydroponics/equipment.dmi b/icons/obj/service/hydroponics/equipment.dmi
index 24a8d7bc5f93..4378a61cf8f3 100644
Binary files a/icons/obj/service/hydroponics/equipment.dmi and b/icons/obj/service/hydroponics/equipment.dmi differ
diff --git a/icons/obj/storage/crates.dmi b/icons/obj/storage/crates.dmi
index 3770375556b7..cf62706c5a18 100644
Binary files a/icons/obj/storage/crates.dmi and b/icons/obj/storage/crates.dmi differ
diff --git a/icons/obj/weapons/guns/energy.dmi b/icons/obj/weapons/guns/energy.dmi
index 5d3834b6030b..46e2ceefdc46 100644
Binary files a/icons/obj/weapons/guns/energy.dmi and b/icons/obj/weapons/guns/energy.dmi differ
diff --git a/icons/turf/floors/basalt_outline.dmi b/icons/turf/floors/basalt_outline.dmi
new file mode 100644
index 000000000000..e2d774c00a3f
Binary files /dev/null and b/icons/turf/floors/basalt_outline.dmi differ
diff --git a/icons/turf/floors/basalt_outline.png b/icons/turf/floors/basalt_outline.png
new file mode 100644
index 000000000000..ce68171f9601
Binary files /dev/null and b/icons/turf/floors/basalt_outline.png differ
diff --git a/icons/turf/floors/basalt_outline.png.toml b/icons/turf/floors/basalt_outline.png.toml
new file mode 100644
index 000000000000..ebc026f21ca9
--- /dev/null
+++ b/icons/turf/floors/basalt_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "basalt_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/icons/turf/floors/shale_outline.dmi b/icons/turf/floors/shale_outline.dmi
new file mode 100644
index 000000000000..7c1313a093bb
Binary files /dev/null and b/icons/turf/floors/shale_outline.dmi differ
diff --git a/icons/turf/floors/shale_outline.png b/icons/turf/floors/shale_outline.png
new file mode 100644
index 000000000000..dd4104b06ee8
Binary files /dev/null and b/icons/turf/floors/shale_outline.png differ
diff --git a/icons/turf/floors/shale_outline.png.toml b/icons/turf/floors/shale_outline.png.toml
new file mode 100644
index 000000000000..3c104a643f64
--- /dev/null
+++ b/icons/turf/floors/shale_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "shale_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/icons/turf/floors/siderite_outline.dmi b/icons/turf/floors/siderite_outline.dmi
new file mode 100644
index 000000000000..adbf092ee485
Binary files /dev/null and b/icons/turf/floors/siderite_outline.dmi differ
diff --git a/icons/turf/floors/siderite_outline.png b/icons/turf/floors/siderite_outline.png
new file mode 100644
index 000000000000..14da6135eda9
Binary files /dev/null and b/icons/turf/floors/siderite_outline.png differ
diff --git a/icons/turf/floors/siderite_outline.png.toml b/icons/turf/floors/siderite_outline.png.toml
new file mode 100644
index 000000000000..ec6db4a74167
--- /dev/null
+++ b/icons/turf/floors/siderite_outline.png.toml
@@ -0,0 +1,2 @@
+output_name = "siderite_outline"
+template = "bitmask/diagonal_32x32.toml"
diff --git a/rust_g.dll b/rust_g.dll
index 62c8ba24f015..3fafd146ec81 100644
Binary files a/rust_g.dll and b/rust_g.dll differ
diff --git a/tgstation.dme b/tgstation.dme
index d9914a272a00..58f475443e64 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -759,6 +759,7 @@
#include "code\controllers\subsystem\shuttle.dm"
#include "code\controllers\subsystem\skills.dm"
#include "code\controllers\subsystem\sound_loops.dm"
+#include "code\controllers\subsystem\sound_tokens.dm"
#include "code\controllers\subsystem\sounds.dm"
#include "code\controllers\subsystem\spatial_gridmap.dm"
#include "code\controllers\subsystem\speech_controller.dm"
@@ -893,6 +894,7 @@
#include "code\datums\ruins.dm"
#include "code\datums\saymode.dm"
#include "code\datums\signals.dm"
+#include "code\datums\sound_token.dm"
#include "code\datums\spawners_menu.dm"
#include "code\datums\sprite_accessories.dm"
#include "code\datums\station_alert.dm"
@@ -960,7 +962,6 @@
#include "code\datums\actions\mobs\sign_language.dm"
#include "code\datums\actions\mobs\sneak.dm"
#include "code\datums\actions\mobs\teleport.dm"
-#include "code\datums\actions\mobs\transform_weapon.dm"
#include "code\datums\actions\mobs\sequences\dash_attack.dm"
#include "code\datums\actions\mobs\sequences\projectile.dm"
#include "code\datums\ai\_ai_behavior.dm"
@@ -1004,6 +1005,7 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\capricious_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\climb_tree.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\drag_items.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\enrage.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\escape_captivity.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\express_happiness.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
@@ -5229,6 +5231,7 @@
#include "code\modules\mob\living\basic\blob_minions\blobbernaut.dm"
#include "code\modules\mob\living\basic\boss\boss.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\_blood_drunk_miner.dm"
+#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_actions.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_ai.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_objects.dm"
#include "code\modules\mob\living\basic\boss\blood_drunk_miner\blood_drunk_subtypes.dm"
@@ -5345,6 +5348,7 @@
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_abilities.dm"
#include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_ai.dm"
+#include "code\modules\mob\living\basic\icemoon\polar_bear\polar_bear.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf_ai.dm"
#include "code\modules\mob\living\basic\icemoon\wolf\wolf_extras.dm"
@@ -5758,7 +5762,6 @@
#include "code\modules\mob\living\simple_animal\hostile\megafauna\legion.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\wendigo.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\mining_mobs.dm"
-#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\polarbear.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\elite.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\goliath_broodmother.dm"
#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\elites\herald.dm"
diff --git a/tools/UpdatePaths/Scripts/96295_polarbear.txt b/tools/UpdatePaths/Scripts/96295_polarbear.txt
new file mode 100644
index 000000000000..c3b0c19872f7
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96295_polarbear.txt
@@ -0,0 +1 @@
+/mob/living/simple_animal/hostile/asteroid/polarbear : /mob/living/basic/mining/polarbear{@OLD}