From c7a729c18d0df3de7a170da41f8f492186366dfd Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Wed, 13 May 2026 00:03:41 +1000 Subject: [PATCH 01/79] Implementing variant fence icons from Doe. --- code/game/objects/structures/fence_types.dm | 49 ++++++ code/game/objects/structures/fences.dm | 140 ++++++++++++++---- .../crafting/stack_recipes/recipes_bricks.dm | 8 + .../crafting/stack_recipes/recipes_logs.dm | 17 ++- .../crafting/stack_recipes/recipes_planks.dm | 8 + .../crafting/stack_recipes/recipes_rods.dm | 16 ++ icons/obj/structures/fence.dmi | Bin 17048 -> 16571 bytes 7 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 code/game/objects/structures/fence_types.dm diff --git a/code/game/objects/structures/fence_types.dm b/code/game/objects/structures/fence_types.dm new file mode 100644 index 00000000000..940709d14a6 --- /dev/null +++ b/code/game/objects/structures/fence_types.dm @@ -0,0 +1,49 @@ +/decl/fence_type + var/name = "chain link fence" + var/desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out." + var/corner_state = "corner" + var/straight_state = "straight" + var/post_state = "post" + var/end_state = "end" + var/door_closed_state = "door-opened" + var/door_open_state = "door-closed" + +/decl/fence_type/brick + name = "brick fence" + desc = "A brick fence. Not as effective as a wall, but generally it keeps people out." + corner_state = "corner_stone" + straight_state = "straight_stone" + post_state = "post_stone" + end_state = "end_stone" + door_closed_state = "door_stone-opened" + door_open_state = "door_stone-closed" + +/decl/fence_type/palisade + name = "palisade" + desc = "A tall and imposing palisade with sharpened points atop it." + corner_state = "corner_palisade" + straight_state = "straight_palisade" + post_state = "post_palisade" + end_state = "end_palisade" + door_closed_state = "door_palisade-opened" + door_open_state = "door_palisade-closed" + +/decl/fence_type/stick + name = "stick fence" + desc = "A stick fence. Not as effective as a wall, but generally it keeps people out." + corner_state = "corner_stick" + straight_state = "straight_stick" + post_state = "post_stick" + end_state = "end_stick" + door_closed_state = "door_stick-opened" + door_open_state = "door_stick-closed" + +/decl/fence_type/plank + name = "plank fence" + desc = "A plank fence. Not as effective as a wall, but generally it keeps people out." + corner_state = "corner_plank" + straight_state = "straight_plank" + post_state = "post_plank" + end_state = "end_plank" + door_closed_state = "door_plank-opened" + door_open_state = "door_plank-closed" diff --git a/code/game/objects/structures/fences.dm b/code/game/objects/structures/fences.dm index 1ed302b304a..66243aac4f0 100644 --- a/code/game/objects/structures/fences.dm +++ b/code/game/objects/structures/fences.dm @@ -1,5 +1,6 @@ -//Chain link fences -//Sprites ported from /VG/ +// Various fences +// Chain link sprites ported from /VG/ +// Stone, stick, plank and palisade sprites by Doe. #define CUT_TIME 10 SECONDS #define CLIMB_TIME 5 SECONDS @@ -14,24 +15,81 @@ /obj/structure/fence name = "fence" - desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out." + desc = "A fence. Not as effective as a wall, but generally it keeps people out." density = TRUE anchored = TRUE - icon = 'icons/obj/structures/fence.dmi' icon_state = "straight" - material = /decl/material/solid/metal/steel material_alteration = MAT_FLAG_ALTERATION_ALL tool_interaction_flags = TOOL_INTERACTION_DECONSTRUCT - var/cuttable = TRUE + var/decl/fence_type/fence_data = /decl/fence_type var/hole_size = NO_HOLE /obj/structure/fence/Initialize(mapload) update_cut_status() + if(ispath(fence_data)) + fence_data = GET_DECL(fence_data) + SetName(fence_data.name) + desc = (fence_data.desc) + else if(!istype(fence_data)) + fence_data = null + queue_icon_update() return ..() +/obj/structure/fence/update_icon() + . = ..() + if(!istype(fence_data)) + return + update_fence_icon() + +/obj/structure/fence/proc/update_fence_icon() + + // Find any adjacent fences. + var/static/list/direct_adjacent = list(NORTH, SOUTH, EAST, WEST) + var/connected_dirs = 0 + for(var/check_dir in direct_adjacent) + var/turf/neighbor = get_step_resolving_mimic(get_turf(src), check_dir) + if(!istype(neighbor) || !(locate(/obj/structure/fence) in neighbor)) + continue + connected_dirs |= check_dir + + // End segments. + if(check_dir == NORTH || check_dir == SOUTH || check_dir == EAST || check_dir == WEST) + set_dir(global.reverse_dir[check_dir]) + set_icon_state(fence_data.end_state) + // Straight segments. + else if(check_dir == (NORTH | SOUTH) || check_dir == (EAST | WEST)) + if(check_dir & NORTH) + set_dir(NORTH) + else + set_dir(EAST) + switch(hole_size) + if(MEDIUM_HOLE) + set_icon_state("[fence_data.straight_state]-cut2") + if(LARGE_HOLE) + set_icon_state("[fence_data.straight_state]-cut3") + else + set_icon_state(fence_data.straight_state) + + // Corner segments. + else if(check_dir in global.cornerdirs) + set_icon_state(fence_data.corner_state) + var/static/list/_corner_fence_to_state_mapping = alist( + (NORTHWEST) = SOUTH, + (NORTHEAST) = NORTH, + (SOUTHWEST) = EAST, + (SOUTHEAST) = WEST + ) + set_dir(_corner_fence_to_state_mapping[check_dir]) + + // Junction segments - not currently supported. + + +/obj/structure/fence/proc/is_cuttable() + return icon_state == fence_data.straight_state && hole_size < MAX_HOLE_SIZE + /obj/structure/fence/get_examine_strings(mob/user, distance, infix, suffix) . = ..() switch(hole_size) @@ -45,18 +103,6 @@ if(cuttable && hole_size < MAX_HOLE_SIZE) LAZYADD(., SPAN_SUBTLE("Use wirecutters to [hole_size > NO_HOLE ? "expand the":"cut a"] hole into the fence, allowing passage.")) -/obj/structure/fence/end - icon_state = "end" - cuttable = FALSE - -/obj/structure/fence/corner - icon_state = "corner" - cuttable = FALSE - -/obj/structure/fence/post - icon_state = "post" - cuttable = FALSE - /obj/structure/fence/cut/medium icon_state = "straight-cut2" hole_size = MEDIUM_HOLE @@ -69,7 +115,7 @@ /obj/structure/fence/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) if(mover?.checkpass(PASS_FLAG_TABLE)) return TRUE - if(hole_size == MEDIUM_HOLE && issmall(mover)) + if(hole_size >= MEDIUM_HOLE && issmall(mover)) return TRUE return ..() @@ -119,7 +165,6 @@ if(!cuttable) return density = TRUE - switch(hole_size) if(NO_HOLE) icon_state = initial(icon_state) @@ -130,7 +175,6 @@ density = FALSE //FENCE DOORS - /obj/structure/fence/door name = "fence door" desc = "Not very useful without a real lock." @@ -140,8 +184,16 @@ var/locked = FALSE /obj/structure/fence/door/Initialize(mapload) + . = ..() update_door_status() - return ..() + +/obj/structure/fence/door/update_fence_icon() + if(!istype(fence_data)) + return + if(density) + set_icon_state(fence_data.door_closed_state) + else + set_icon_state(fence_data.door_opened_state) /obj/structure/fence/door/opened icon_state = "door-opened" @@ -173,13 +225,8 @@ playsound(src, 'sound/machines/click.ogg', 100, 1) /obj/structure/fence/door/proc/update_door_status() - switch(open) - if(FALSE) - density = TRUE - icon_state = "door-closed" - if(TRUE) - density = FALSE - icon_state = "door-opened" + density = !open + update_icon() /obj/structure/fence/door/proc/can_open(mob/user) if(locked) @@ -192,4 +239,37 @@ #undef NO_HOLE #undef MEDIUM_HOLE #undef LARGE_HOLE -#undef MAX_HOLE_SIZE \ No newline at end of file +#undef MAX_HOLE_SIZE + +// Mapping/crafting helpers. +/obj/structure/fence/brick + icon_state = /decl/fence_type/brick::straight_state + fence_data = /decl/fence_type/brick + +/obj/structure/fence/door/brick + icon_state = /decl/fence_type/brick::door_state_closed + fence_data = /decl/fence_type/brick + +/obj/structure/fence/palisade + icon_state = /decl/fence_type/palisade::straight_state + fence_data = /decl/fence_type/palisade + +/obj/structure/fence/door/palisade + icon_state = /decl/fence_type/palisade::door_state_closed + fence_data = /decl/fence_type/palisade + +/obj/structure/fence/stick + icon_state = /decl/fence_type/stick::straight_state + fence_data = /decl/fence_type/stick + +/obj/structure/fence/door/stick + icon_state = /decl/fence_type/stick::door_state_closed + fence_data = /decl/fence_type/stick + +/obj/structure/fence/plank + icon_state = /decl/fence_type/plank::straight_state + fence_data = /decl/fence_type/plank + +/obj/structure/fence/door/plank + icon_state = /decl/fence_type/plank::door_state_closed + fence_data = /decl/fence_type/plank diff --git a/code/modules/crafting/stack_recipes/recipes_bricks.dm b/code/modules/crafting/stack_recipes/recipes_bricks.dm index 0c9670fbcd9..a7dcdbc92fd 100644 --- a/code/modules/crafting/stack_recipes/recipes_bricks.dm +++ b/code/modules/crafting/stack_recipes/recipes_bricks.dm @@ -101,6 +101,14 @@ name = "pedestal, round" result_type = /obj/structure/pedestal/round +/decl/stack_recipe/bricks/furniture/fence + result_type = /obj/structure/fence/brick + difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/bricks/furniture/fence_door + result_type = /obj/structure/fence/door/brick + difficulty = MAT_VALUE_NORMAL_DIY + /decl/stack_recipe/bricks/gravestone result_type = /obj/item/gravemarker/gravestone difficulty = MAT_VALUE_NORMAL_DIY diff --git a/code/modules/crafting/stack_recipes/recipes_logs.dm b/code/modules/crafting/stack_recipes/recipes_logs.dm index 02010e3a925..3dadcadb424 100644 --- a/code/modules/crafting/stack_recipes/recipes_logs.dm +++ b/code/modules/crafting/stack_recipes/recipes_logs.dm @@ -22,4 +22,19 @@ /decl/stack_recipe/logs/wall_frame result_type = /obj/structure/wall_frame/log - difficulty = MAT_VALUE_HARD_DIY \ No newline at end of file + difficulty = MAT_VALUE_HARD_DIY + +/decl/stack_recipe/logs/furniture + abstract_type = /decl/stack_recipe/logs/furniture + one_per_turf = TRUE + on_floor = TRUE + difficulty = MAT_VALUE_HARD_DIY + category = "furniture" + +/decl/stack_recipe/logs/furniture/fence + result_type = /obj/structure/fence/palisade + difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/logs/furniture/fence_door + result_type = /obj/structure/fence/door/palisade + difficulty = MAT_VALUE_NORMAL_DIY diff --git a/code/modules/crafting/stack_recipes/recipes_planks.dm b/code/modules/crafting/stack_recipes/recipes_planks.dm index feac5e54b58..f00339f99e1 100644 --- a/code/modules/crafting/stack_recipes/recipes_planks.dm +++ b/code/modules/crafting/stack_recipes/recipes_planks.dm @@ -251,3 +251,11 @@ /decl/stack_recipe/planks/furniture/target_stake result_type = /obj/structure/target_stake difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/planks/furniture/fence + result_type = /obj/structure/fence/plank + difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/planks/furniture/fence_door + result_type = /obj/structure/fence/door/plank + difficulty = MAT_VALUE_NORMAL_DIY diff --git a/code/modules/crafting/stack_recipes/recipes_rods.dm b/code/modules/crafting/stack_recipes/recipes_rods.dm index 4b506210b57..96a92267ab9 100644 --- a/code/modules/crafting/stack_recipes/recipes_rods.dm +++ b/code/modules/crafting/stack_recipes/recipes_rods.dm @@ -77,3 +77,19 @@ result_type = /obj/structure/grille one_per_turf = TRUE difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/rods/furniture + abstract_type = /decl/stack_recipe/rods/furniture + one_per_turf = TRUE + on_floor = TRUE + difficulty = MAT_VALUE_HARD_DIY + category = "furniture" + +/decl/stack_recipe/rods/furniture/fence + result_type = /obj/structure/fence/stick + difficulty = MAT_VALUE_NORMAL_DIY + +/decl/stack_recipe/rods/furniture/fence_door + result_type = /obj/structure/fence/door/stick + difficulty = MAT_VALUE_NORMAL_DIY + diff --git a/icons/obj/structures/fence.dmi b/icons/obj/structures/fence.dmi index b3d997a940f8e7fa816ee9017629d0d30b015eae..e75db9717befe2449aec5c3340f77f7f8fa8b7c3 100644 GIT binary patch literal 16571 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 literal 17048 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 X_wY#AofG`23COc2stQH&Mn3-^CG+*z From 247f90c19a9f2070797d744130c3d1fea05ea075 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Wed, 13 May 2026 22:44:35 +1000 Subject: [PATCH 02/79] Implementing fence smoothing and crafting. --- code/game/objects/structures/fence_types.dm | 49 ----- .../{fences.dm => fences/_fences.dm} | 190 +++++++++++------- .../objects/structures/fences/fence_types.dm | 61 ++++++ icons/obj/structures/fence.dmi | Bin 16571 -> 0 bytes icons/obj/structures/fences/brick.dmi | Bin 0 -> 2885 bytes icons/obj/structures/fences/chain.dmi | Bin 0 -> 7160 bytes icons/obj/structures/fences/palisade.dmi | Bin 0 -> 3403 bytes icons/obj/structures/fences/plank.dmi | Bin 0 -> 2297 bytes icons/obj/structures/fences/stick.dmi | Bin 0 -> 2853 bytes nebula.dme | 3 +- 10 files changed, 181 insertions(+), 122 deletions(-) delete mode 100644 code/game/objects/structures/fence_types.dm rename code/game/objects/structures/{fences.dm => fences/_fences.dm} (60%) create mode 100644 code/game/objects/structures/fences/fence_types.dm delete mode 100644 icons/obj/structures/fence.dmi create mode 100755 icons/obj/structures/fences/brick.dmi create mode 100755 icons/obj/structures/fences/chain.dmi create mode 100755 icons/obj/structures/fences/palisade.dmi create mode 100755 icons/obj/structures/fences/plank.dmi create mode 100755 icons/obj/structures/fences/stick.dmi diff --git a/code/game/objects/structures/fence_types.dm b/code/game/objects/structures/fence_types.dm deleted file mode 100644 index 940709d14a6..00000000000 --- a/code/game/objects/structures/fence_types.dm +++ /dev/null @@ -1,49 +0,0 @@ -/decl/fence_type - var/name = "chain link fence" - var/desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out." - var/corner_state = "corner" - var/straight_state = "straight" - var/post_state = "post" - var/end_state = "end" - var/door_closed_state = "door-opened" - var/door_open_state = "door-closed" - -/decl/fence_type/brick - name = "brick fence" - desc = "A brick fence. Not as effective as a wall, but generally it keeps people out." - corner_state = "corner_stone" - straight_state = "straight_stone" - post_state = "post_stone" - end_state = "end_stone" - door_closed_state = "door_stone-opened" - door_open_state = "door_stone-closed" - -/decl/fence_type/palisade - name = "palisade" - desc = "A tall and imposing palisade with sharpened points atop it." - corner_state = "corner_palisade" - straight_state = "straight_palisade" - post_state = "post_palisade" - end_state = "end_palisade" - door_closed_state = "door_palisade-opened" - door_open_state = "door_palisade-closed" - -/decl/fence_type/stick - name = "stick fence" - desc = "A stick fence. Not as effective as a wall, but generally it keeps people out." - corner_state = "corner_stick" - straight_state = "straight_stick" - post_state = "post_stick" - end_state = "end_stick" - door_closed_state = "door_stick-opened" - door_open_state = "door_stick-closed" - -/decl/fence_type/plank - name = "plank fence" - desc = "A plank fence. Not as effective as a wall, but generally it keeps people out." - corner_state = "corner_plank" - straight_state = "straight_plank" - post_state = "post_plank" - end_state = "end_plank" - door_closed_state = "door_plank-opened" - door_open_state = "door_plank-closed" diff --git a/code/game/objects/structures/fences.dm b/code/game/objects/structures/fences/_fences.dm similarity index 60% rename from code/game/objects/structures/fences.dm rename to code/game/objects/structures/fences/_fences.dm index 66243aac4f0..c94d2229caf 100644 --- a/code/game/objects/structures/fences.dm +++ b/code/game/objects/structures/fences/_fences.dm @@ -3,7 +3,6 @@ // Stone, stick, plank and palisade sprites by Doe. #define CUT_TIME 10 SECONDS -#define CLIMB_TIME 5 SECONDS ///section is intact #define NO_HOLE 0 @@ -14,67 +13,112 @@ #define MAX_HOLE_SIZE LARGE_HOLE /obj/structure/fence - name = "fence" - desc = "A fence. Not as effective as a wall, but generally it keeps people out." - density = TRUE - anchored = TRUE - icon = 'icons/obj/structures/fence.dmi' - icon_state = "straight" - material = /decl/material/solid/metal/steel - material_alteration = MAT_FLAG_ALTERATION_ALL + name = "fence" + desc = "A fence. Not as effective as a wall, but generally it keeps people out." + density = TRUE + anchored = TRUE + icon = /decl/fence_type::fence_icon + icon_state = /decl/fence_type::straight_state + material = /decl/material/solid/metal/steel + atom_flags = ATOM_FLAG_CLIMBABLE + material_alteration = MAT_FLAG_ALTERATION_ALL tool_interaction_flags = TOOL_INTERACTION_DECONSTRUCT var/decl/fence_type/fence_data = /decl/fence_type - var/hole_size = NO_HOLE + var/hole_size = NO_HOLE + var/connected_dirs = 0 -/obj/structure/fence/Initialize(mapload) - update_cut_status() +/obj/structure/fence/Destroy() + var/turf/prior_loc = loc + . = ..() + if(istype(prior_loc)) + for(var/check_dir in global.cardinal) + for(var/obj/structure/fence/fence in get_step_resolving_mimic(prior_loc, check_dir)) + fence.update_icon() + +/obj/structure/fence/Initialize(ml, _mat, _reinf_mat) if(ispath(fence_data)) fence_data = GET_DECL(fence_data) - SetName(fence_data.name) - desc = (fence_data.desc) + set_icon(fence_data.fence_icon) else if(!istype(fence_data)) fence_data = null - queue_icon_update() - return ..() + . = ..() + update_cut_status() + if(ml) + queue_icon_update() + else + return INITIALIZE_HINT_LATELOAD -/obj/structure/fence/update_icon() +/obj/structure/fence/LateInitialize() . = ..() - if(!istype(fence_data)) - return - update_fence_icon() + update_icon() + for(var/check_dir in global.cardinal) + var/turf/neighbor = get_step_resolving_mimic(get_turf(src), check_dir) + if(istype(neighbor)) + for(var/obj/structure/fence/fence in neighbor) + if(fence_data == RESOLVE_TO_DECL(fence.fence_data)) + fence.update_icon() -/obj/structure/fence/proc/update_fence_icon() +/obj/structure/fence/update_material_name(override_name) + override_name ||= fence_data.name + . = ..() + +/obj/structure/fence/update_material_desc(override_desc) + override_desc ||= fence_data.desc + . = ..() + +/obj/structure/fence/on_update_icon() + . = ..() + if(istype(fence_data)) + update_fence_connections() + update_fence_icon() + +/obj/structure/fence/proc/is_fencepost() + return FALSE // TODO: detect doors and junctions next to us. +/obj/structure/fence/proc/update_fence_connections() // Find any adjacent fences. - var/static/list/direct_adjacent = list(NORTH, SOUTH, EAST, WEST) - var/connected_dirs = 0 - for(var/check_dir in direct_adjacent) - var/turf/neighbor = get_step_resolving_mimic(get_turf(src), check_dir) - if(!istype(neighbor) || !(locate(/obj/structure/fence) in neighbor)) + connected_dirs = 0 + var/turf/my_turf = get_turf(src) + for(var/check_dir in global.cardinal) + var/turf/neighbor = get_step_resolving_mimic(my_turf, check_dir) + if(!istype(neighbor)) continue - connected_dirs |= check_dir + for(var/obj/structure/fence/fence in neighbor) + if(fence_data == RESOLVE_TO_DECL(fence.fence_data)) + connected_dirs |= check_dir + break + +/obj/structure/fence/proc/update_fence_icon() + + // Standalone segment. + if(!connected_dirs) + set_icon_state(fence_data.single_state) + + // Four-way junction. + else if(connected_dirs == (NORTH|SOUTH|EAST|WEST)) + set_icon_state(fence_data.four_way_state) // End segments. - if(check_dir == NORTH || check_dir == SOUTH || check_dir == EAST || check_dir == WEST) - set_dir(global.reverse_dir[check_dir]) + else if(connected_dirs == NORTH || connected_dirs == SOUTH || connected_dirs == EAST || connected_dirs == WEST) + set_dir(connected_dirs) set_icon_state(fence_data.end_state) + // Straight segments. - else if(check_dir == (NORTH | SOUTH) || check_dir == (EAST | WEST)) - if(check_dir & NORTH) + else if(connected_dirs == (NORTH | SOUTH) || connected_dirs == (EAST | WEST)) + if(connected_dirs & NORTH) set_dir(NORTH) else set_dir(EAST) - switch(hole_size) - if(MEDIUM_HOLE) - set_icon_state("[fence_data.straight_state]-cut2") - if(LARGE_HOLE) - set_icon_state("[fence_data.straight_state]-cut3") - else - set_icon_state(fence_data.straight_state) + if(hole_size > 0) + set_icon_state("[fence_data.straight_state]-cut[hole_size]") + else if(is_fencepost()) + set_icon_state(fence_data.post_state) + else + set_icon_state(fence_data.straight_state) // Corner segments. - else if(check_dir in global.cornerdirs) + else if(connected_dirs in global.cornerdirs) set_icon_state(fence_data.corner_state) var/static/list/_corner_fence_to_state_mapping = alist( (NORTHWEST) = SOUTH, @@ -82,10 +126,15 @@ (SOUTHWEST) = EAST, (SOUTHEAST) = WEST ) - set_dir(_corner_fence_to_state_mapping[check_dir]) - - // Junction segments - not currently supported. + set_dir(_corner_fence_to_state_mapping[connected_dirs]) + // Junction segments. + else + set_icon_state(fence_data.three_way_state) + for(var/check_dir in global.cardinal) + if(!(connected_dirs & check_dir)) + set_dir(check_dir) + break /obj/structure/fence/proc/is_cuttable() return icon_state == fence_data.straight_state && hole_size < MAX_HOLE_SIZE @@ -100,7 +149,7 @@ /obj/structure/fence/get_examine_hints(mob/user, distance, infix, suffix) . = ..() - if(cuttable && hole_size < MAX_HOLE_SIZE) + if(is_cuttable()) LAZYADD(., SPAN_SUBTLE("Use wirecutters to [hole_size > NO_HOLE ? "expand the":"cut a"] hole into the fence, allowing passage.")) /obj/structure/fence/cut/medium @@ -134,10 +183,9 @@ return TRUE return ..() - /obj/structure/fence/attackby(obj/item/used_item, mob/user) if(IS_WIRECUTTER(used_item)) - if(!cuttable) + if(!is_cuttable()) to_chat(user, SPAN_WARNING("This section of the fence can't be cut.")) return TRUE var/current_stage = hole_size @@ -162,7 +210,7 @@ return ..() /obj/structure/fence/proc/update_cut_status() - if(!cuttable) + if(!is_cuttable()) return density = TRUE switch(hole_size) @@ -179,62 +227,60 @@ name = "fence door" desc = "Not very useful without a real lock." icon_state = "door-closed" - cuttable = FALSE - var/open = FALSE - var/locked = FALSE -/obj/structure/fence/door/Initialize(mapload) +/obj/structure/fence/door/update_material_name(override_name) + override_name ||= fence_data.door_name . = ..() - update_door_status() + +/obj/structure/fence/door/update_material_desc(override_desc) + override_desc ||= fence_data.door_desc + . = ..() + return INITIALIZE_HINT_LATELOAD /obj/structure/fence/door/update_fence_icon() if(!istype(fence_data)) return + if((connected_dirs & NORTH) || (connected_dirs & SOUTH)) + set_dir(NORTH) + else + set_dir(EAST) if(density) - set_icon_state(fence_data.door_closed_state) + set_icon_state(fence_data.door_state_closed) else - set_icon_state(fence_data.door_opened_state) + set_icon_state(fence_data.door_state_opened) /obj/structure/fence/door/opened icon_state = "door-opened" - open = TRUE density = TRUE /obj/structure/fence/door/locked desc = "It looks like it has a strong padlock attached." - locked = TRUE + +/obj/structure/fence/door/locked/Initialize(mapload) + lock ||= "[random_id(type, 10000, 99999)]" + . = ..() /obj/structure/fence/door/attack_hand(mob/user, list/params) SHOULD_CALL_PARENT(FALSE) if(can_open(user)) toggle(user) else - to_chat(user, SPAN_WARNING("\The [src] is [!open ? "locked" : "stuck open"].")) + to_chat(user, SPAN_WARNING("\The [src] is [density ? "locked" : "stuck open"].")) return TRUE /obj/structure/fence/door/proc/toggle(mob/user) - switch(open) - if(FALSE) - visible_message(SPAN_NOTICE("\The [user] opens \the [src].")) - open = TRUE - if(TRUE) - visible_message(SPAN_NOTICE("\The [user] closes \the [src].")) - open = FALSE - - update_door_status() + density = !density + if(density) + visible_message(SPAN_NOTICE("\The [user] closes \the [src].")) + else + visible_message(SPAN_NOTICE("\The [user] opens \the [src].")) playsound(src, 'sound/machines/click.ogg', 100, 1) - -/obj/structure/fence/door/proc/update_door_status() - density = !open update_icon() /obj/structure/fence/door/proc/can_open(mob/user) - if(locked) - return FALSE - return TRUE + return !lock || !lock.isLocked() #undef CUT_TIME -#undef CLIMB_TIME #undef NO_HOLE #undef MEDIUM_HOLE diff --git a/code/game/objects/structures/fences/fence_types.dm b/code/game/objects/structures/fences/fence_types.dm new file mode 100644 index 00000000000..bec8e817efd --- /dev/null +++ b/code/game/objects/structures/fences/fence_types.dm @@ -0,0 +1,61 @@ +/decl/fence_type + var/name = "chain link fence" + var/desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out." + var/door_name = "fence gate" + var/door_desc = "Not very useful without a real lock." + var/fence_icon = 'icons/obj/structures/fences/chain.dmi' + var/single_state = "single" + var/corner_state = "corner" + var/straight_state = "straight" + var/post_state = "post" + var/end_state = "end" + var/three_way_state = "three_way" + var/four_way_state = "four_way" + var/door_state_closed = "door-opened" + var/door_state_opened = "door-closed" + +/decl/fence_type/validate() + . = ..() + + if(!fence_icon) + . += "missing fence icon" + return + + if(!single_state || !check_state_in_icon(single_state, fence_icon)) + . += "missing or invalid single_state '[single_state]' from '[fence_icon]'" + if(!straight_state || !check_state_in_icon(straight_state, fence_icon)) + . += "missing or invalid straight_state '[straight_state]' from '[fence_icon]'" + if(!corner_state || !check_state_in_icon(corner_state, fence_icon)) + . += "missing or invalid corner_state '[corner_state]' from '[fence_icon]'" + if(!post_state || !check_state_in_icon(post_state, fence_icon)) + . += "missing or invalid post_state '[post_state]' from '[fence_icon]'" + if(!end_state || !check_state_in_icon(end_state, fence_icon)) + . += "missing or invalid end_state '[end_state]' from '[fence_icon]'" + if(!door_state_closed || !check_state_in_icon(door_state_closed, fence_icon)) + . += "missing or invalid door_state_closed '[door_state_closed]' from '[fence_icon]'" + if(!door_state_opened || !check_state_in_icon(door_state_opened, fence_icon)) + . += "missing or invalid door_state_opened '[door_state_opened]' from '[fence_icon]'" + if(!three_way_state || !check_state_in_icon(three_way_state, fence_icon)) + . += "missing or invalid three_way_state '[three_way_state]' from '[fence_icon]'" + if(!four_way_state || !check_state_in_icon(four_way_state, fence_icon)) + . += "missing or invalid four_way_state '[four_way_state]' from '[fence_icon]'" + +/decl/fence_type/brick + name = "brick fence" + desc = "A brick fence. Not as effective as a wall, but generally it keeps people out." + fence_icon = 'icons/obj/structures/fences/brick.dmi' + +/decl/fence_type/palisade + name = "palisade" + desc = "A tall and imposing palisade with sharpened points atop it." + fence_icon = 'icons/obj/structures/fences/palisade.dmi' + +/decl/fence_type/stick + name = "stick fence" + desc = "A stick fence. Not as effective as a wall, but generally it keeps people out." + fence_icon = 'icons/obj/structures/fences/stick.dmi' + +/decl/fence_type/plank + name = "plank fence" + desc = "A plank fence. Not as effective as a wall, but generally it keeps people out." + fence_icon = 'icons/obj/structures/fences/plank.dmi' diff --git a/icons/obj/structures/fence.dmi b/icons/obj/structures/fence.dmi deleted file mode 100644 index e75db9717befe2449aec5c3340f77f7f8fa8b7c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16571 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 diff --git a/icons/obj/structures/fences/brick.dmi b/icons/obj/structures/fences/brick.dmi new file mode 100755 index 0000000000000000000000000000000000000000..a9e2f9c17b7d7964af9a58c6e416766d460b0035 GIT binary patch literal 2885 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 OF2LHt-n`z-H}Sumkj_K^ literal 0 HcmV?d00001 diff --git a/icons/obj/structures/fences/chain.dmi b/icons/obj/structures/fences/chain.dmi new file mode 100755 index 0000000000000000000000000000000000000000..109a1aaec4b2be0ac23283ceaa48acec54cd93f1 GIT binary patch literal 7160 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 T0VMb-5BTe@)}7qjX2kyj;_i3A literal 0 HcmV?d00001 diff --git a/icons/obj/structures/fences/palisade.dmi b/icons/obj/structures/fences/palisade.dmi new file mode 100755 index 0000000000000000000000000000000000000000..ad5c81aca43deead7b9e828ad8b4452c847fbbc3 GIT binary patch literal 3403 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 N2z3%;Uuug5{sA~$gzNwS literal 0 HcmV?d00001 diff --git a/icons/obj/structures/fences/stick.dmi b/icons/obj/structures/fences/stick.dmi new file mode 100755 index 0000000000000000000000000000000000000000..c10c06afd293e18765b7c4cd10d4703891e453dd GIT binary patch literal 2853 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 literal 0 HcmV?d00001 diff --git a/nebula.dme b/nebula.dme index e664d65973b..76e67c95d00 100644 --- a/nebula.dme +++ b/nebula.dme @@ -1443,7 +1443,6 @@ #include "code\game\objects\structures\drying_rack.dm" #include "code\game\objects\structures\emergency_dispenser.dm" #include "code\game\objects\structures\extinguisher.dm" -#include "code\game\objects\structures\fences.dm" #include "code\game\objects\structures\fireaxe_cabinet.dm" #include "code\game\objects\structures\fires.dm" #include "code\game\objects\structures\fishtanks.dm" @@ -1548,6 +1547,8 @@ #include "code\game\objects\structures\decorations\_decoration.dm" #include "code\game\objects\structures\decorations\gargoyle.dm" #include "code\game\objects\structures\doors\_door.dm" +#include "code\game\objects\structures\fences\_fences.dm" +#include "code\game\objects\structures\fences\fence_types.dm" #include "code\game\objects\structures\flora\_flora.dm" #include "code\game\objects\structures\flora\bush.dm" #include "code\game\objects\structures\flora\grass.dm" From 689e667dd874a72411446565c44322ee50c86320 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Thu, 14 May 2026 20:26:31 +1000 Subject: [PATCH 03/79] Fixing typo in lock code. --- code/game/objects/structures/__structure.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/objects/structures/__structure.dm b/code/game/objects/structures/__structure.dm index 27239ec9127..b3247344e7d 100644 --- a/code/game/objects/structures/__structure.dm +++ b/code/game/objects/structures/__structure.dm @@ -63,7 +63,7 @@ . = ..() update_materials() paint_verb ||= "painted" // fallback for the case of no material - if(lock && !istype(loc)) + if(lock && !istype(lock)) lock = new /datum/lock(src, lock) if(!CanFluidPass()) fluid_update(TRUE) From 1863fd70228fcb8fb4e29b0943abb8b1b4536181 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Thu, 14 May 2026 20:28:16 +1000 Subject: [PATCH 04/79] Fixing fence strings. --- .../game/objects/structures/fences/_fences.dm | 55 +++++++----------- .../objects/structures/fences/fence_types.dm | 2 +- icons/obj/structures/fences/brick.dmi | Bin icons/obj/structures/fences/chain.dmi | Bin icons/obj/structures/fences/palisade.dmi | Bin icons/obj/structures/fences/plank.dmi | Bin icons/obj/structures/fences/stick.dmi | Bin 7 files changed, 22 insertions(+), 35 deletions(-) mode change 100755 => 100644 icons/obj/structures/fences/brick.dmi mode change 100755 => 100644 icons/obj/structures/fences/chain.dmi mode change 100755 => 100644 icons/obj/structures/fences/palisade.dmi mode change 100755 => 100644 icons/obj/structures/fences/plank.dmi mode change 100755 => 100644 icons/obj/structures/fences/stick.dmi diff --git a/code/game/objects/structures/fences/_fences.dm b/code/game/objects/structures/fences/_fences.dm index c94d2229caf..9bc566ec9e8 100644 --- a/code/game/objects/structures/fences/_fences.dm +++ b/code/game/objects/structures/fences/_fences.dm @@ -2,15 +2,6 @@ // Chain link sprites ported from /VG/ // Stone, stick, plank and palisade sprites by Doe. -#define CUT_TIME 10 SECONDS - -///section is intact -#define NO_HOLE 0 -///medium hole in the section - can climb through -#define MEDIUM_HOLE 1 -///large hole in the section - can walk through -#define LARGE_HOLE 2 -#define MAX_HOLE_SIZE LARGE_HOLE /obj/structure/fence name = "fence" @@ -28,6 +19,15 @@ var/hole_size = NO_HOLE var/connected_dirs = 0 + var/const/CUT_TIME = 10 SECONDS + ///section is intact + var/const/NO_HOLE = 0 + ///medium hole in the section - can climb through + var/const/MEDIUM_HOLE = 1 + ///large hole in the section - can walk through + var/const/LARGE_HOLE = 2 + var/const/MAX_HOLE_SIZE = LARGE_HOLE + /obj/structure/fence/Destroy() var/turf/prior_loc = loc . = ..() @@ -224,10 +224,13 @@ //FENCE DOORS /obj/structure/fence/door - name = "fence door" - desc = "Not very useful without a real lock." + name = "fence gate" + desc = "Much like a regular door, but thinner." icon_state = "door-closed" +/obj/structure/fence/door/can_install_lock() + return TRUE + /obj/structure/fence/door/update_material_name(override_name) override_name ||= fence_data.door_name . = ..() @@ -253,40 +256,24 @@ icon_state = "door-opened" density = TRUE -/obj/structure/fence/door/locked - desc = "It looks like it has a strong padlock attached." - /obj/structure/fence/door/locked/Initialize(mapload) - lock ||= "[random_id(type, 10000, 99999)]" + lock ||= "fence key #[random_id(type, 10000, 99999)]" . = ..() /obj/structure/fence/door/attack_hand(mob/user, list/params) SHOULD_CALL_PARENT(FALSE) - if(can_open(user)) - toggle(user) + if(!density || can_open(user)) + density = !density + visible_message(SPAN_NOTICE("\The [user] [density ? "opens" : "closes"] \the [src].")) + playsound(src, 'sound/machines/click.ogg', 100, 1) + update_icon() else - to_chat(user, SPAN_WARNING("\The [src] is [density ? "locked" : "stuck open"].")) + to_chat(user, SPAN_WARNING("\The [src] is locked.")) return TRUE -/obj/structure/fence/door/proc/toggle(mob/user) - density = !density - if(density) - visible_message(SPAN_NOTICE("\The [user] closes \the [src].")) - else - visible_message(SPAN_NOTICE("\The [user] opens \the [src].")) - playsound(src, 'sound/machines/click.ogg', 100, 1) - update_icon() - /obj/structure/fence/door/proc/can_open(mob/user) return !lock || !lock.isLocked() -#undef CUT_TIME - -#undef NO_HOLE -#undef MEDIUM_HOLE -#undef LARGE_HOLE -#undef MAX_HOLE_SIZE - // Mapping/crafting helpers. /obj/structure/fence/brick icon_state = /decl/fence_type/brick::straight_state diff --git a/code/game/objects/structures/fences/fence_types.dm b/code/game/objects/structures/fences/fence_types.dm index bec8e817efd..029df039000 100644 --- a/code/game/objects/structures/fences/fence_types.dm +++ b/code/game/objects/structures/fences/fence_types.dm @@ -2,7 +2,7 @@ var/name = "chain link fence" var/desc = "A chain link fence. Not as effective as a wall, but generally it keeps people out." var/door_name = "fence gate" - var/door_desc = "Not very useful without a real lock." + var/door_desc = "Much like a regular door, but thinner." var/fence_icon = 'icons/obj/structures/fences/chain.dmi' var/single_state = "single" var/corner_state = "corner" diff --git a/icons/obj/structures/fences/brick.dmi b/icons/obj/structures/fences/brick.dmi old mode 100755 new mode 100644 diff --git a/icons/obj/structures/fences/chain.dmi b/icons/obj/structures/fences/chain.dmi old mode 100755 new mode 100644 diff --git a/icons/obj/structures/fences/palisade.dmi b/icons/obj/structures/fences/palisade.dmi old mode 100755 new mode 100644 diff --git a/icons/obj/structures/fences/plank.dmi b/icons/obj/structures/fences/plank.dmi old mode 100755 new mode 100644 diff --git a/icons/obj/structures/fences/stick.dmi b/icons/obj/structures/fences/stick.dmi old mode 100755 new mode 100644 From cd4d673875a6859694a4e03b0b594cd246412a5c Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Sat, 16 May 2026 14:39:07 +1000 Subject: [PATCH 05/79] Corrects 'a multiple iron arrow' in embed examine. --- code/modules/mob/living/human/examine.dm | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/code/modules/mob/living/human/examine.dm b/code/modules/mob/living/human/examine.dm index 00c79f54619..32bfaad2c93 100644 --- a/code/modules/mob/living/human/examine.dm +++ b/code/modules/mob/living/human/examine.dm @@ -165,12 +165,15 @@ shown_objects += embedlist var/parsedembed[0] for(var/obj/embedded in embedlist) - if(!parsedembed.len || (!parsedembed.Find(embedded.name) && !parsedembed.Find("multiple [embedded.name]"))) - parsedembed.Add(embedded.name) - else if(!parsedembed.Find("multiple [embedded.name]")) - parsedembed.Remove(embedded.name) - parsedembed.Add("multiple "+embedded.name) - wound_flavor_text["[E.name]"] += "The [wound.desc] on [use_his] [E.name] has \a [english_list(parsedembed, and_text = " and a ", comma_text = ", a ")] sticking out of it!
" + var/single_embed_string = "\a [embedded.name]" + var/plural_embed_string = "multiple [text_make_plural(embedded.name)]" + if(!parsedembed.len || (!parsedembed.Find(single_embed_string) && !parsedembed.Find(plural_embed_string))) + parsedembed.Add(single_embed_string) + else if(!parsedembed.Find(plural_embed_string)) + parsedembed.Remove(single_embed_string) + parsedembed.Add(plural_embed_string) + wound_flavor_text[E.organ_tag] += SPAN_WARNING("The [wound.desc] on [pronouns.his] [E.name] has [english_list(parsedembed, and_text = " and ", comma_text = ", ")] sticking out of it!") + for(var/hidden in hidden_bleeders) wound_flavor_text[hidden] = "[use_He] [use_has] blood soaking through [hidden] around [use_his] [english_list(hidden_bleeders[hidden])]!
" From b7ad045f1e12fc38cf821c291f57e1ea115d1abc Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 18 May 2026 12:10:05 +1000 Subject: [PATCH 06/79] Walls will be undamageable to certain weapons based on material. --- code/game/turfs/walls/wall_attacks.dm | 12 +++++++----- code/modules/materials/_materials.dm | 3 +++ .../definitions/solids/materials_solid_alien.dm | 1 + .../definitions/solids/materials_solid_exotic.dm | 2 ++ .../definitions/solids/materials_solid_fission.dm | 3 ++- .../definitions/solids/materials_solid_gemstones.dm | 1 + .../definitions/solids/materials_solid_ice.dm | 1 + .../definitions/solids/materials_solid_metal.dm | 1 + .../definitions/solids/materials_solid_mineral.dm | 1 + .../definitions/solids/materials_solid_mundane.dm | 1 + .../definitions/solids/materials_solid_organic.dm | 1 + .../definitions/solids/materials_solid_stone.dm | 1 + .../definitions/solids/materials_solid_wood.dm | 1 + 13 files changed, 23 insertions(+), 6 deletions(-) diff --git a/code/game/turfs/walls/wall_attacks.dm b/code/game/turfs/walls/wall_attacks.dm index 4c567def785..16d98d06821 100644 --- a/code/game/turfs/walls/wall_attacks.dm +++ b/code/game/turfs/walls/wall_attacks.dm @@ -303,25 +303,27 @@ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN) user.do_attack_animation(src) + + var/damage_threshold = max(2, max(material.wall_damage_threshold, reinf_material?.wall_damage_threshold)) var/material_divisor = max(material.brute_armor, reinf_material?.brute_armor) if(W.atom_damage_type == BURN) material_divisor = max(material.burn_armor, reinf_material?.burn_armor) var/effective_force = round(force / material_divisor) - if(effective_force < 2) - visible_message(SPAN_DANGER("\The [user] [pick(W.attack_verb)] \the [src] with \the [W], but it had no effect!")) + if(effective_force < damage_threshold) + visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W], but it has no effect!")) playsound(src, hitsound, 25, 1) return TRUE // Check for a glancing blow. var/dam_prob = max(0, 100 - material.hardness + effective_force + W.armor_penetration) if(!prob(dam_prob)) - visible_message(SPAN_DANGER("\The [user] [pick(W.attack_verb)] \the [src] with \the [W], but it bounced off!")) + visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W], but it bounced off!")) playsound(src, hitsound, 25, 1) if(user.skill_fail_prob(SKILL_HAULING, 40, SKILL_ADEPT)) SET_STATUS_MAX(user, STAT_WEAK, 2) visible_message(SPAN_DANGER("\The [user] is knocked back by the force of the blow!")) return TRUE - playsound(src, get_hit_sound(), 50, 1) - visible_message(SPAN_DANGER("\The [user] [pick(W.attack_verb)] \the [src] with \the [W]!")) + visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W]!")) + playsound(src, hitsound, 50, 1) take_damage(effective_force) return TRUE \ No newline at end of file diff --git a/code/modules/materials/_materials.dm b/code/modules/materials/_materials.dm index 65b38e35046..3b5e519e9fe 100644 --- a/code/modules/materials/_materials.dm +++ b/code/modules/materials/_materials.dm @@ -362,6 +362,9 @@ INITIALIZE_IMMEDIATE(/obj/effect/gas_overlay) /// If an item has a null paint_verb, it automatically sets it based on material. var/paint_verb = "painted" + // Physical attacks against walls must beat this threshold to cause damage. + var/wall_damage_threshold = 2 + // Placeholders for light tiles and rglass. /decl/material/proc/reinforce(var/mob/user, var/obj/item/stack/material/used_stack, var/obj/item/stack/material/target_stack, var/use_sheets = 1) if(!used_stack.can_use(use_sheets)) diff --git a/code/modules/materials/definitions/solids/materials_solid_alien.dm b/code/modules/materials/definitions/solids/materials_solid_alien.dm index 97ff03bfe45..c36607ddac7 100644 --- a/code/modules/materials/definitions/solids/materials_solid_alien.dm +++ b/code/modules/materials/definitions/solids/materials_solid_alien.dm @@ -12,6 +12,7 @@ default_solid_form = /obj/item/stack/material/cubes exoplanet_rarity_plant = MAT_RARITY_EXOTIC exoplanet_rarity_gas = MAT_RARITY_NOWHERE + wall_damage_threshold = 20 /decl/material/solid/metal/aliumium/Initialize() icon_base = 'icons/turf/walls/metal.dmi' diff --git a/code/modules/materials/definitions/solids/materials_solid_exotic.dm b/code/modules/materials/definitions/solids/materials_solid_exotic.dm index 1d3448bdfed..11c4c804bf1 100644 --- a/code/modules/materials/definitions/solids/materials_solid_exotic.dm +++ b/code/modules/materials/definitions/solids/materials_solid_exotic.dm @@ -30,6 +30,7 @@ default_solid_form = /obj/item/stack/material/segment exoplanet_rarity_plant = MAT_RARITY_EXOTIC exoplanet_rarity_gas = MAT_RARITY_NOWHERE + wall_damage_threshold = 20 /decl/material/solid/exotic_matter name = "exotic matter" @@ -65,3 +66,4 @@ default_solid_form = /obj/item/stack/material/segment exoplanet_rarity_plant = MAT_RARITY_EXOTIC exoplanet_rarity_gas = MAT_RARITY_NOWHERE + wall_damage_threshold = 10 diff --git a/code/modules/materials/definitions/solids/materials_solid_fission.dm b/code/modules/materials/definitions/solids/materials_solid_fission.dm index f68c8c85673..066c788dbc0 100644 --- a/code/modules/materials/definitions/solids/materials_solid_fission.dm +++ b/code/modules/materials/definitions/solids/materials_solid_fission.dm @@ -27,7 +27,8 @@ fission_heat = 35000 fission_energy = 4000 neutron_absorption = 4 - + wall_damage_threshold = 20 + /decl/material/solid/metal/neptunium // Np-237. name = "neptunium" diff --git a/code/modules/materials/definitions/solids/materials_solid_gemstones.dm b/code/modules/materials/definitions/solids/materials_solid_gemstones.dm index f0718ad4ccf..46a87745eea 100644 --- a/code/modules/materials/definitions/solids/materials_solid_gemstones.dm +++ b/code/modules/materials/definitions/solids/materials_solid_gemstones.dm @@ -13,6 +13,7 @@ abstract_type = /decl/material/solid/gemstone sound_manipulate = 'sound/foley/pebblespickup1.ogg' sound_dropped = 'sound/foley/pebblesdrop1.ogg' + wall_damage_threshold = 10 /decl/material/solid/gemstone/diamond name = "diamond" diff --git a/code/modules/materials/definitions/solids/materials_solid_ice.dm b/code/modules/materials/definitions/solids/materials_solid_ice.dm index 34144fe8393..a9ca928102d 100644 --- a/code/modules/materials/definitions/solids/materials_solid_ice.dm +++ b/code/modules/materials/definitions/solids/materials_solid_ice.dm @@ -20,6 +20,7 @@ heating_products = list( /decl/material/liquid/water = 1 ) + wall_damage_threshold = 5 /decl/material/solid/ice/Initialize() liquid_name ||= "liquid [name]" // avoiding the 'molten ice' issue diff --git a/code/modules/materials/definitions/solids/materials_solid_metal.dm b/code/modules/materials/definitions/solids/materials_solid_metal.dm index 5aa1b8acd50..ff248eb40d5 100644 --- a/code/modules/materials/definitions/solids/materials_solid_metal.dm +++ b/code/modules/materials/definitions/solids/materials_solid_metal.dm @@ -20,6 +20,7 @@ icon_reinf = 'icons/turf/walls/reinforced_metal.dmi' exoplanet_rarity_gas = MAT_RARITY_NOWHERE tensile_strength = 0.8 // metal wire is probably better than plastic? + wall_damage_threshold = 10 /decl/material/solid/metal/uranium name = "uranium" diff --git a/code/modules/materials/definitions/solids/materials_solid_mineral.dm b/code/modules/materials/definitions/solids/materials_solid_mineral.dm index 4317abc5b12..61b30748108 100644 --- a/code/modules/materials/definitions/solids/materials_solid_mineral.dm +++ b/code/modules/materials/definitions/solids/materials_solid_mineral.dm @@ -19,6 +19,7 @@ ) ore_type_value = ORE_NUCLEAR ore_data_value = 3 + wall_damage_threshold = 5 /decl/material/solid/graphite name = "graphite" diff --git a/code/modules/materials/definitions/solids/materials_solid_mundane.dm b/code/modules/materials/definitions/solids/materials_solid_mundane.dm index 8579088f2f2..bf4f1f31a15 100644 --- a/code/modules/materials/definitions/solids/materials_solid_mundane.dm +++ b/code/modules/materials/definitions/solids/materials_solid_mundane.dm @@ -24,3 +24,4 @@ /decl/material/gas/sulfur_dioxide = 0.05, /decl/material/gas/carbon_dioxide = 0.05 ) + wall_damage_threshold = 10 diff --git a/code/modules/materials/definitions/solids/materials_solid_organic.dm b/code/modules/materials/definitions/solids/materials_solid_organic.dm index c5b9a8960c9..139e9746f92 100644 --- a/code/modules/materials/definitions/solids/materials_solid_organic.dm +++ b/code/modules/materials/definitions/solids/materials_solid_organic.dm @@ -12,6 +12,7 @@ bakes_into_at_temperature = T0C+500 bakes_into_material = /decl/material/solid/carbon */ + wall_damage_threshold = 5 /decl/material/solid/organic/plastic name = "plastic" diff --git a/code/modules/materials/definitions/solids/materials_solid_stone.dm b/code/modules/materials/definitions/solids/materials_solid_stone.dm index 6334b03f1b8..39072835713 100644 --- a/code/modules/materials/definitions/solids/materials_solid_stone.dm +++ b/code/modules/materials/definitions/solids/materials_solid_stone.dm @@ -23,6 +23,7 @@ ore_result_amount = 4 sound_manipulate = 'sound/foley/rockscrape.ogg' sound_dropped = 'sound/foley/rockscrape.ogg' + wall_damage_threshold = 10 /decl/material/solid/stone/sandstone name = "sandstone" diff --git a/code/modules/materials/definitions/solids/materials_solid_wood.dm b/code/modules/materials/definitions/solids/materials_solid_wood.dm index ac5d06a17fe..0695085e3c6 100644 --- a/code/modules/materials/definitions/solids/materials_solid_wood.dm +++ b/code/modules/materials/definitions/solids/materials_solid_wood.dm @@ -48,6 +48,7 @@ compost_value = 0.2 temperature_burn_milestone_material = /decl/material/solid/organic/wood paint_verb = "stained" + wall_damage_threshold = 8 // Wood is hard but can't really give it an edge. /decl/material/solid/organic/wood/can_hold_edge() From 5d11fd4bc22a633bc51df6e6b6e4756b1744fa64 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Tue, 19 May 2026 13:37:07 +1000 Subject: [PATCH 07/79] Consistency pass on wall hitsound. --- code/game/turfs/walls/_wall.dm | 5 ++--- code/game/turfs/walls/wall_attacks.dm | 8 ++++---- code/game/turfs/walls/wall_icon.dm | 1 - .../materials/definitions/solids/materials_solid_metal.dm | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/code/game/turfs/walls/_wall.dm b/code/game/turfs/walls/_wall.dm index 084fb135e17..4fcb5558c71 100644 --- a/code/game/turfs/walls/_wall.dm +++ b/code/game/turfs/walls/_wall.dm @@ -41,7 +41,6 @@ var/global/list/wall_fullblend_objects = list( var/decl/material/reinf_material var/decl/material/girder_material = /decl/material/solid/metal/steel var/construction_stage - var/hitsound = 'sound/weapons/Genhit.ogg' /// A list of connections to walls for each corner, used for icon generation. Can be converted to a list of dirs with corner_states_to_dirs(). var/list/wall_connections /// A list of connections to non-walls for each corner, used for icon generation. Can be converted to a list of dirs with corner_states_to_dirs(). @@ -152,7 +151,7 @@ var/global/list/wall_fullblend_objects = list( . = ..() if(. && density && !ismob(AM)) var/tforce = AM.get_thrown_attack_force() * (TT.speed/THROWFORCE_SPEED_DIVISOR) - playsound(src, hitsound, tforce >= 15 ? 60 : 25, TRUE) + playsound(src, get_hit_sound(), tforce >= 15 ? 60 : 25, TRUE) if(tforce > 0) take_damage(tforce) @@ -323,7 +322,7 @@ var/global/list/wall_fullblend_objects = list( handle_melting() /turf/wall/proc/get_hit_sound() - return 'sound/effects/metalhit.ogg' + return material?.hitsound || 'sound/weapons/Genhit.ogg' // Mapped premade for false walls /turf/wall/false diff --git a/code/game/turfs/walls/wall_attacks.dm b/code/game/turfs/walls/wall_attacks.dm index 16d98d06821..d0e1b0beda6 100644 --- a/code/game/turfs/walls/wall_attacks.dm +++ b/code/game/turfs/walls/wall_attacks.dm @@ -95,7 +95,7 @@ if (isnull(construction_stage) || !reinf_material) to_chat(user, "You push \the [src], but nothing happens.") - playsound(src, hitsound, 25, 1) + playsound(src, get_hit_sound(), 25, 1) return TRUE /turf/wall/attack_hand(var/mob/user) @@ -311,19 +311,19 @@ var/effective_force = round(force / material_divisor) if(effective_force < damage_threshold) visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W], but it has no effect!")) - playsound(src, hitsound, 25, 1) + playsound(src, get_hit_sound(), 25, 1) return TRUE // Check for a glancing blow. var/dam_prob = max(0, 100 - material.hardness + effective_force + W.armor_penetration) if(!prob(dam_prob)) visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W], but it bounced off!")) - playsound(src, hitsound, 25, 1) + playsound(src, get_hit_sound(), 25, 1) if(user.skill_fail_prob(SKILL_HAULING, 40, SKILL_ADEPT)) SET_STATUS_MAX(user, STAT_WEAK, 2) visible_message(SPAN_DANGER("\The [user] is knocked back by the force of the blow!")) return TRUE visible_message(SPAN_DANGER("\The [user] has [pick(W.attack_verb)] \the [src] with \the [W]!")) - playsound(src, hitsound, 50, 1) + playsound(src, get_hit_sound(), 50, 1) take_damage(effective_force) return TRUE \ No newline at end of file diff --git a/code/game/turfs/walls/wall_icon.dm b/code/game/turfs/walls/wall_icon.dm index c682d4f266f..365e76169d3 100644 --- a/code/game/turfs/walls/wall_icon.dm +++ b/code/game/turfs/walls/wall_icon.dm @@ -11,7 +11,6 @@ material = get_default_material() if(material) explosion_resistance = material.explosion_resistance - hitsound = material.hitsound if(reinf_material && reinf_material.explosion_resistance > explosion_resistance) explosion_resistance = reinf_material.explosion_resistance update_strings() diff --git a/code/modules/materials/definitions/solids/materials_solid_metal.dm b/code/modules/materials/definitions/solids/materials_solid_metal.dm index ff248eb40d5..dadcc8fa395 100644 --- a/code/modules/materials/definitions/solids/materials_solid_metal.dm +++ b/code/modules/materials/definitions/solids/materials_solid_metal.dm @@ -21,6 +21,7 @@ exoplanet_rarity_gas = MAT_RARITY_NOWHERE tensile_strength = 0.8 // metal wire is probably better than plastic? wall_damage_threshold = 10 + hitsound = 'sound/effects/metalhit.ogg' /decl/material/solid/metal/uranium name = "uranium" From 3f9162643992f28878460732035c3e45e4761cfd Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 10:51:08 +1000 Subject: [PATCH 08/79] Rewriting beehives and beehavior. --- maps/ministation/ministation-1.dmm | 2 +- mods/content/beekeeping/_beekeeping.dm | 3 +- mods/content/beekeeping/_beekeeping.dme | 11 +- mods/content/beekeeping/closets.dm | 4 +- .../beekeeping/hives/hive_extension.dm | 195 +++++++++++ mods/content/beekeeping/hives/hive_flora.dm | 40 +++ .../beekeeping/{ => hives}/hive_frame.dm | 47 ++- mods/content/beekeeping/hives/hive_queen.dm | 23 ++ .../beekeeping/hives/hive_structure.dm | 83 +++++ mods/content/beekeeping/hives/hive_swarm.dm | 322 ++++++++++++++++++ .../hives/insect_species/_insects.dm | 174 ++++++++++ .../insect_species/insects_pollinators.dm | 26 ++ mods/content/beekeeping/icons/apiary.dmi | Bin 0 -> 345 bytes .../beekeeping/icons/apiary_bees_etc.dmi | Bin 3301 -> 0 bytes mods/content/beekeeping/icons/bee_pack.dmi | Bin 0 -> 366 bytes mods/content/beekeeping/icons/beehive.dmi | Bin 0 -> 319 bytes mods/content/beekeeping/icons/beekeeping.dmi | Bin 5053 -> 0 bytes mods/content/beekeeping/icons/comb.dmi | Bin 0 -> 1077 bytes mods/content/beekeeping/icons/smoker.dmi | Bin 1019 -> 480 bytes mods/content/beekeeping/icons/swarm.dmi | Bin 0 -> 1016 bytes mods/content/beekeeping/items.dm | 40 --- mods/content/beekeeping/materials.dm | 20 ++ mods/content/beekeeping/recipes.dm | 7 +- mods/content/beekeeping/trading.dm | 10 +- 24 files changed, 940 insertions(+), 67 deletions(-) create mode 100644 mods/content/beekeeping/hives/hive_extension.dm create mode 100644 mods/content/beekeeping/hives/hive_flora.dm rename mods/content/beekeeping/{ => hives}/hive_frame.dm (52%) create mode 100644 mods/content/beekeeping/hives/hive_queen.dm create mode 100644 mods/content/beekeeping/hives/hive_structure.dm create mode 100644 mods/content/beekeeping/hives/hive_swarm.dm create mode 100644 mods/content/beekeeping/hives/insect_species/_insects.dm create mode 100644 mods/content/beekeeping/hives/insect_species/insects_pollinators.dm create mode 100644 mods/content/beekeeping/icons/apiary.dmi delete mode 100644 mods/content/beekeeping/icons/apiary_bees_etc.dmi create mode 100644 mods/content/beekeeping/icons/bee_pack.dmi create mode 100644 mods/content/beekeeping/icons/beehive.dmi delete mode 100644 mods/content/beekeeping/icons/beekeeping.dmi create mode 100644 mods/content/beekeeping/icons/comb.dmi create mode 100644 mods/content/beekeeping/icons/swarm.dmi create mode 100644 mods/content/beekeeping/materials.dm diff --git a/maps/ministation/ministation-1.dmm b/maps/ministation/ministation-1.dmm index fc3ebff786d..54715c60e4b 100644 --- a/maps/ministation/ministation-1.dmm +++ b/maps/ministation/ministation-1.dmm @@ -8744,7 +8744,7 @@ /turf/floor/tiled, /area/ministation/hall/e2) "Oo" = ( -/obj/machinery/beehive, +/obj/structure/apiary/mapped, /turf/floor/fake_grass, /area/ministation/hydro) "Op" = ( diff --git a/mods/content/beekeeping/_beekeeping.dm b/mods/content/beekeeping/_beekeeping.dm index d3b58be746d..471217ac52d 100644 --- a/mods/content/beekeeping/_beekeeping.dm +++ b/mods/content/beekeeping/_beekeeping.dm @@ -1,7 +1,6 @@ /decl/modpack/beekeeping - name = "Beekeeping Content" + name = "Beekeeping and Insects Content" /datum/storage/hopper/industrial/centrifuge/New() ..() can_hold |= /obj/item/hive_frame - diff --git a/mods/content/beekeeping/_beekeeping.dme b/mods/content/beekeeping/_beekeeping.dme index 75778704f36..92ee753943b 100644 --- a/mods/content/beekeeping/_beekeeping.dme +++ b/mods/content/beekeeping/_beekeeping.dme @@ -3,10 +3,17 @@ // BEGIN_INCLUDE #include "_beekeeping.dm" #include "closets.dm" -#include "hive_frame.dm" #include "items.dm" +#include "materials.dm" #include "recipes.dm" #include "trading.dm" -#include "hives\_hive.dm" +#include "hives\hive_extension.dm" +#include "hives\hive_flora.dm" +#include "hives\hive_frame.dm" +#include "hives\hive_queen.dm" +#include "hives\hive_structure.dm" +#include "hives\hive_swarm.dm" +#include "hives\insect_species\_insects.dm" +#include "hives\insect_species\insects_pollinators.dm" // END_INCLUDE #endif diff --git a/mods/content/beekeeping/closets.dm b/mods/content/beekeeping/closets.dm index c39e70dbb45..9caf9bb459c 100644 --- a/mods/content/beekeeping/closets.dm +++ b/mods/content/beekeeping/closets.dm @@ -1,10 +1,10 @@ /obj/structure/closet/crate/hydroponics/beekeeping name = "beekeeping crate" - desc = "All you need to set up your own beehive." + desc = "All you need to set up your own beehive, except the beehive." /obj/structure/closet/crate/hydroponics/beekeeping/Initialize() . = ..() - new /obj/item/beehive_assembly(src) + new /obj/item/stack/material/plank/mapped/wood/ten new /obj/item/bee_smoker(src) new /obj/item/hive_frame/crafted(src) new /obj/item/hive_frame/crafted(src) diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm new file mode 100644 index 00000000000..d02839f4d40 --- /dev/null +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -0,0 +1,195 @@ +/datum/extension/insect_hive + base_type = /datum/extension/insect_hive + expected_type = /obj/structure + flags = EXTENSION_FLAG_IMMEDIATE + /// The species of insect that made this hive. + var/decl/insect_species/holding_species + /// References to our current swarm effects gathering for the hive. + var/list/swarms + var/current_health = 100 + var/material = 10 + var/raw_reserves = 0 + /// Tracker for the last world.time that a frame was removed. + var/frame_last_removed = 0 + /// Tracker for ticks remaning since we were last smoked. + var/smoked_out = 0 + +/datum/extension/insect_hive/New(datum/holder, _species_decl) + ..() + holding_species = istype(_species_decl, /decl/insect_species) ? _species_decl : GET_DECL(_species_decl) + if(!istype(holding_species)) + CRASH("Insect hive extension instantiated with invalid insect species: '[_species_decl]'.") + START_PROCESSING(SSprocessing, src) + +/datum/extension/insect_hive/Destroy() + STOP_PROCESSING(SSprocessing, src) + if(length(swarms)) + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.owner = null + swarms = null + var/atom/movable/hive = holder + if(istype(hive) && !QDELETED(hive)) + hive.queue_icon_update() + return ..() + +/datum/extension/insect_hive/Process() + if(smoked_out > 0) + smoked_out-- + return + holding_species.process_hive(src) + create_hive_products() + +/datum/extension/insect_hive/proc/handle_item_interaction(mob/user, obj/item/item) + return FALSE + +/datum/extension/insect_hive/proc/drop_nest(atom/drop_loc) + return + +/datum/extension/insect_hive/proc/get_nest_condition() + switch(current_health) + if(0, 10) + return "dying" + if(10, 30) + return "struggling" + if(30, 60) + return "sickly" + if(60, 90) + return null + return "thriving" + +/datum/extension/insect_hive/proc/get_nest_name() + return holding_species?.nest_name + +/datum/extension/insect_hive/proc/examined(mob/user, show_detail) + var/nest_descriptor = get_nest_condition() + if(nest_descriptor) + to_chat(user, SPAN_NOTICE("It contains \a [nest_descriptor] [get_nest_name()].")) + else + to_chat(user, SPAN_NOTICE("It contains \a [get_nest_name()].")) + +/datum/extension/insect_hive/proc/frame_removed(obj/item/frame) + frame_last_removed = world.time + if(!smoked_out) + for(var/obj/effect/insect_swarm/swarm in swarms) + swarm.swarm_agitation = min(100, swarm.swarm_agitation + 5) + +/datum/extension/insect_hive/proc/try_hand_harvest(mob/user) + return FALSE + +/datum/extension/insect_hive/proc/try_tool_harvest(mob/user, obj/item/tool) + return FALSE + +/datum/extension/insect_hive/proc/swarm_destroyed(obj/effect/insect_swarm/swarm) + return + +/datum/extension/insect_hive/proc/swarm_at_hive() + for(var/atom/movable/swarm as anything in swarms) + if(get_turf(swarm) == get_turf(holder)) + return swarm + +/datum/extension/insect_hive/proc/has_material(amt) + return amt <= material + +/datum/extension/insect_hive/proc/consume_material(amt) + if(has_material(amt)) + material = clamp(material-amt, 0, 100) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/add_material(amt) + material = clamp(material+amt, 0, 100) + return TRUE + +/datum/extension/insect_hive/proc/add_reserves(amt) + raw_reserves = clamp(raw_reserves+amt, 0, 100) + return TRUE + +/datum/extension/insect_hive/proc/has_reserves(amt, raw_reserves_only = TRUE) + if(raw_reserves >= amt) + return TRUE + if(raw_reserves_only) + return FALSE + var/reserve = 0 + for(var/obj/item/frame in holder) + reserve += frame.reagents?.total_volume + if(reserve >= amt) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/consume_reserves(amt, raw_reserves_only = TRUE) + if(!has_reserves(amt, raw_reserves_only)) + return FALSE + if(raw_reserves >= amt) + raw_reserves -= amt + return TRUE + if(raw_reserves_only) + return FALSE + amt -= raw_reserves + raw_reserves = 0 + for(var/obj/item/frame in holder) + if(!frame.reagents?.total_volume) + continue + var/consume = min(amt, frame.reagents.total_volume) + frame.reagents.remove_any(consume) + amt -= consume + if(amt <= 0) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/adjust_health(amt) + current_health = clamp(current_health + amt, 0, 100) + if(current_health <= 0) + var/atom/movable/hive = holder + hive.visible_message(SPAN_DANGER("\The [holding_species.nest_name] sags and collapses.")) + remove_extension(holder, base_type) + +/datum/extension/insect_hive/proc/create_hive_products() + + var/atom/movable/hive = holder + if(!istype(hive) || !holding_species) + return TRUE + + if(!swarm_at_hive()) // nobody home to do the work + return TRUE + + // Naturally build up enough material for a new frame (or repairs). + if(!has_material(20)) + add_material(1) + + // Damaged hives cannot produce combs or honey. + if(current_health < 100) + if(consume_material(5)) + adjust_health(rand(3,5)) + return TRUE + + if(!has_reserves(20)) + return TRUE + + var/list/holder_contents = hive.get_contained_external_atoms() + for(var/obj/item/hive_frame/frame in holder_contents) + if(!frame.reagents || (frame.reagents.total_volume >= frame.reagents.maximum_volume)) + continue + var/fill_cost = REAGENTS_FREE_SPACE(frame.reagents) + if(consume_material(5) && consume_reserves(fill_cost)) + holding_species.fill_hive_frame(frame) + return TRUE + + var/obj/item/native_frame = holding_species.native_frame_type + var/native_frame_size = initial(native_frame.w_class) + var/space_left = hive.storage.max_storage_space + for(var/obj/item/thing in hive.get_stored_inventory()) + space_left -= thing.w_class + if(space_left < native_frame_size) + return + + // Put a timer check on this to avoid a hive filling up with combs the moment you take 2 frames out. + if(world.time > (frame_last_removed + 2 MINUTES) && space_left >= native_frame_size && consume_material(20)) + // Frames start empty, and will be filled next run. + // Native 'frames' (combs) are bigger than crafted ones and aren't reusable. + new native_frame(holder, holding_species.produce_material) + hive.storage.update_ui_after_item_insertion() + +/datum/extension/insect_hive/proc/get_total_swarm_intensity() + . = 0 + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + . += swarm.swarm_intensity diff --git a/mods/content/beekeeping/hives/hive_flora.dm b/mods/content/beekeeping/hives/hive_flora.dm new file mode 100644 index 00000000000..e6091c3610b --- /dev/null +++ b/mods/content/beekeeping/hives/hive_flora.dm @@ -0,0 +1,40 @@ +/obj/structure/flora + /// Percentage chance of trying to spawn an insect hive here, if appropriate. + var/insect_hive_chance = 20 + +/obj/structure/flora/Initialize(ml, _mat, _reinf_mat) + . = ..() + if(insect_hive_chance && length(get_supported_insects())) + return INITIALIZE_HINT_LATELOAD + +/obj/structure/flora/LateInitialize() + ..() + if(prob(insect_hive_chance) && !has_extension(src, /datum/extension/insect_hive)) + var/list/insects = get_supported_insects() + if(length(insects)) + insects = insects.Copy() // don't mutate the static list. + for(var/species_type in insects) + var/decl/insect_species/species = GET_DECL(species_type) + if(!species.can_spawn_in_flora(src)) + insects -= species_type + if(length(insects)) + set_extension(src, /datum/extension/insect_hive, pickweight(insects)) + update_icon() + +// Insect species that can hive in this flora. +/obj/structure/flora/proc/get_supported_insects() + return + +/obj/structure/flora/tree/get_supported_insects() + var/static/list/_insects = list( + /decl/insect_species/honeybees = 10, + /decl/insect_species/wasps = 1 + ) + return _insects + +/obj/structure/flora/stump/get_supported_insects() + var/static/list/_insects = list( + /decl/insect_species/honeybees = 10, + /decl/insect_species/wasps = 1 + ) + return _insects diff --git a/mods/content/beekeeping/hive_frame.dm b/mods/content/beekeeping/hives/hive_frame.dm similarity index 52% rename from mods/content/beekeeping/hive_frame.dm rename to mods/content/beekeeping/hives/hive_frame.dm index 35ffd429777..be57c7adf7a 100644 --- a/mods/content/beekeeping/hive_frame.dm +++ b/mods/content/beekeeping/hives/hive_frame.dm @@ -1,7 +1,7 @@ /obj/item/hive_frame - abstract_type = /obj/item/hive_frame - icon_state = ICON_STATE_WORLD - w_class = ITEM_SIZE_SMALL + abstract_type = /obj/item/hive_frame + icon_state = ICON_STATE_WORLD + w_class = ITEM_SIZE_SMALL material_alteration = MAT_FLAG_ALTERATION_ALL chem_volume = 20 var/destroy_on_centrifuge = FALSE @@ -36,20 +36,45 @@ for(var/atom/movable/thing in convert_matter_to_lumps()) thing.dropInto(centrifuge.loc) +/obj/item/hive_frame/honey/populate_reagents() + . = ..() + var/decl/insect_species/bees = GET_DECL(/decl/insect_species/honeybees) + bees.fill_hive_frame(src) + +/obj/item/hive_frame/Move() + var/datum/extension/insect_hive/hive = get_extension(loc, /datum/extension/insect_hive) + . = ..() + if(. && istype(hive) && loc != hive.holder) + hive.frame_removed(src) + // Crafted frame used in apiaries. /obj/item/hive_frame/crafted name = "hive frame" desc = "A wooden frame for insect hives that the workers will fill with products like honey." icon = 'mods/content/beekeeping/icons/frame.dmi' material = /decl/material/solid/organic/wood/oak - material_alteration = MAT_FLAG_ALTERATION_ALL -// TEMP until beewrite redoes hives. -/obj/item/hive_frame/crafted/filled/Initialize() - . = ..() - new /obj/item/stack/material/bar/wax(src) - update_icon() +// Raw version of honeycomb for wild hives. +/obj/item/hive_frame/comb + name = "comb" + icon = 'mods/content/beekeeping/icons/comb.dmi' + material = /decl/material/solid/organic/wax + destroy_on_centrifuge = TRUE + material_alteration = MAT_FLAG_ALTERATION_COLOR + is_spawnable_type = FALSE + w_class = ITEM_SIZE_NORMAL // Larger than crafted frames, because you should use crafted frames in your hive. -/obj/item/hive_frame/crafted/filled/populate_reagents() +/obj/item/hive_frame/comb/Initialize(ml, material_key, decl/insect_species/spawning_hive) . = ..() - reagents.add_reagent(/decl/material/liquid/nutriment/honey, REAGENT_MAXIMUM_VOLUME(reagents)) + if(istype(spawning_hive)) + SetName(spawning_hive.native_frame_name) + desc = spawning_hive.native_frame_desc + spawning_hive.fill_hive_frame(src) + +// Comb subtype for mapping and debugging. +/obj/item/hive_frame/comb/honey + is_spawnable_type = TRUE + color = COLOR_GOLD + +/obj/item/hive_frame/comb/honey/Initialize(ml, material_key) + return ..(ml, material_key, GET_DECL(/decl/insect_species/honeybees)) diff --git a/mods/content/beekeeping/hives/hive_queen.dm b/mods/content/beekeeping/hives/hive_queen.dm new file mode 100644 index 00000000000..599b487d243 --- /dev/null +++ b/mods/content/beekeeping/hives/hive_queen.dm @@ -0,0 +1,23 @@ +/obj/item/bee_pack + name = "bee pack" + desc = "Contains a queen bee and some worker bees. Everything you'll need to start a hive!" + icon = 'mods/content/beekeeping/icons/bee_pack.dmi' + material = /decl/material/solid/organic/plastic + var/contains_insects = /decl/insect_species/honeybees + +/obj/item/bee_pack/Initialize() + . = ..() + update_icon() + +/obj/item/bee_pack/on_update_icon() + . = ..() + if(contains_insects) + add_overlay("[icon_state]-full") + else + add_overlay("[icon_state]-empty") + +/obj/item/bee_pack/proc/empty() + SetName("empty [initial(name)]") + desc = "A stasis pack for moving bees. It's empty." + contains_insects = null + update_icon() diff --git a/mods/content/beekeeping/hives/hive_structure.dm b/mods/content/beekeeping/hives/hive_structure.dm new file mode 100644 index 00000000000..f7586ad6eec --- /dev/null +++ b/mods/content/beekeeping/hives/hive_structure.dm @@ -0,0 +1,83 @@ +/obj/structure/attackby(obj/item/used_item, mob/user) + if((. = ..())) + return + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive) && hive.handle_item_interaction(user, used_item)) + return TRUE + +/obj/structure/attack_hand(mob/user) + if(has_extension(src, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(hive.try_hand_harvest(user)) + return TRUE + return ..() + +/obj/structure/attackby(obj/item/used_item, mob/user) + if(has_extension(src, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(hive.try_tool_harvest(user, used_item)) + return TRUE + return ..() + +/obj/structure/examined_by(mob/user, distance, infix, suffix) + . = ..() + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive)) + hive.examined(user, (distance <= 1)) + +/obj/structure/dismantle_structure(mob/user) + if(isatom(loc)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive)) + hive.drop_nest(loc) + return ..() + +// 'proper' nest structure for building and mapping +/obj/structure/apiary + name = "apiary" + desc = "An artificial hive for raising insects, like bees, and harvesting products like honey." + icon = 'mods/content/beekeeping/icons/apiary.dmi' + icon_state = ICON_STATE_WORLD + density = TRUE + anchored = TRUE + storage = /datum/storage/apiary + material_alteration = MAT_FLAG_ALTERATION_ALL + material = /decl/material/solid/organic/wood/oak + color = /decl/material/solid/organic/wood/oak::color + obj_flags = OBJ_FLAG_ANCHORABLE + tool_interaction_flags = (TOOL_INTERACTION_ANCHOR | TOOL_INTERACTION_DECONSTRUCT) + +/obj/structure/apiary/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) + return air_group || height == 0 || !density || (istype(mover) && mover.checkpass(PASS_FLAG_TABLE)) + +/obj/structure/apiary/attackby(obj/item/used_item, mob/user) + + if(istype(used_item, /obj/item/bee_pack)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive)) + to_chat(user, SPAN_WARNING("\The [src] already contains \a [hive.holding_species.nest_name].")) + return TRUE + var/obj/item/bee_pack/pack = used_item + if(!pack.contains_insects) + to_chat(user, SPAN_WARNING("\The [pack] is empty!")) + return TRUE + user.visible_message(SPAN_NOTICE("\The [user] transfers the contents of \the [pack] into \the [src].")) + set_extension(src, /datum/extension/insect_hive, pack.contains_insects) + pack.empty() + return TRUE + + . = ..() + +/datum/storage/apiary + can_hold = list(/obj/item/hive_frame) + max_w_class = ITEM_SIZE_NORMAL + max_storage_space = ITEM_SIZE_SMALL * 5 // Five regular frames. + +/obj/structure/apiary/mapped/Initialize(ml, _mat, _reinf_mat) + . = ..() + for(var/_ = 1 to 5) + new /obj/item/hive_frame/crafted(src) + +/obj/structure/apiary/mapped/bees/Initialize(ml, _mat, _reinf_mat) + set_extension(src, /datum/extension/insect_hive, /decl/insect_species/honeybees) + . = ..() diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm new file mode 100644 index 00000000000..2e733a96b13 --- /dev/null +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -0,0 +1,322 @@ +/obj/effect/insect_swarm + anchored = TRUE + is_spawnable_type = FALSE + icon_state = "0" + gender = NEUTER + default_pixel_z = 8 + layer = ABOVE_HUMAN_LAYER + pass_flags = PASS_FLAG_TABLE + movement_handlers = list(/datum/movement_handler/delay/insect_swarm) + + /// Current movement target for automove (ie. hive, flowers or victim) + VAR_PRIVATE/atom/move_target + /// Reference to our owning hive. + var/datum/extension/insect_hive/owner + /// Reference to our insect archetype. + var/decl/insect_species/insect_type + /// A counter for disturbances to the hive or this swarm, causes them to sting people. + var/swarm_agitation = 0 + /// Percentage value; if it drops to 0, the swarm will be destroyed. + var/swarm_intensity = 1 + /// if more states are added to swarm.dmi, increase this + var/const/MAX_SWARM_STATE = 6 + /// Cooldown timer for next tick. + VAR_PRIVATE/next_work = 0 + +/datum/movement_handler/delay/insect_swarm + delay = 1 SECOND + +/datum/movement_handler/delay/insect_swarm/DoMove(direction, mob/mover, is_external) + ..() + step(host, direction) + return MOVEMENT_HANDLED + +/obj/effect/insect_swarm/debug/Initialize(mapload) + . = ..(mapload, _insect_type = /decl/insect_species/honeybees) + +/obj/effect/insect_swarm/Initialize(mapload, _insect_type, _hive) + . = ..() + insect_type = istype(_insect_type, /decl/insect_species) ? _insect_type : GET_DECL(_insect_type) + owner = _hive + if(!istype(insect_type)) + PRINT_STACK_TRACE("Insect swarm created with invalid insect type: '[_insect_type]'") + return INITIALIZE_HINT_QDEL + if(!istype(owner)) + PRINT_STACK_TRACE("Insect swarm created with invalid hive: '[owner]'") + return INITIALIZE_HINT_QDEL + color = insect_type.swarm_color + icon = insect_type.swarm_icon + update_swarm() + LAZYDISTINCTADD(owner.swarms, src) + START_PROCESSING(SSobj, src) + +/obj/effect/insect_swarm/Destroy() + if(owner) + owner.swarm_destroyed(src) + LAZYREMOVE(owner.swarms, src) + owner = null + stop_automove() + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/effect/insect_swarm/proc/update_swarm() + icon_state = num2text(ceil((swarm_intensity / insect_type.max_swarm_intensity) * MAX_SWARM_STATE)) + if(icon_state == "1") + SetName(insect_type.name_singular) + desc = insect_type.insect_desc + gender = NEUTER + else + SetName(insect_type.name_plural) + desc = insect_type.swarm_desc + gender = PLURAL + + // Some icon variation via transform. + if(prob(75)) + var/matrix/swarm_transform = matrix() + swarm_transform.Turn(pick(90, 180, 270)) + +/obj/effect/insect_swarm/proc/is_agitated() + return QDELETED(owner) || swarm_agitation > 0 + +/obj/effect/insect_swarm/proc/find_sting_target() + for(var/mob/living/victim in view(7, src)) + if(!victim.simulated || victim.stat || victim.current_posture?.prone) + continue + if(victim.isSynthetic()) + continue + return victim + +/obj/effect/insect_swarm/proc/merge(obj/effect/insect_swarm/other_swarm) + + // If we can fit into one swarm, just merge us together. + var/total_intensity = swarm_intensity + other_swarm.swarm_intensity + if(total_intensity <= insect_type.max_swarm_intensity) + swarm_intensity = total_intensity + swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + update_swarm() + qdel(other_swarm) + return + + // Otherwise equalize between swarms. + swarm_intensity = floor(total_intensity / 2) + other_swarm.swarm_intensity = total_intensity - swarm_intensity + swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + other_swarm.swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + update_swarm() + other_swarm.update_swarm() + +/obj/effect/insect_swarm/Move() + . = ..() + // Swarms from the same hive in the same loc merge together. + if(. && loc && !QDELETED(src)) + try_consolidate_swarms() + +/obj/effect/insect_swarm/Process() + + // Swarms on a loc should try to merge if possible. + try_consolidate_swarms() + if(QDELETED(src)) + return + + // Swarms with no hive gradually decay to nothing. + if(!owner) + adjust_swarm_intensity(-(rand(1,3))) + if(QDELETED(src)) + return + + if(!move_target || !(move_target in view(5, src))) + stop_automove() + + // Angry swarms move with purpose. + if(is_agitated()) + swarm_agitation = max(0, swarm_agitation-1) + if(!move_target) + move_target = find_sting_target() + if(move_target) + start_automove(move_target) + return + + // Large swarms split if they aren't agitated. + if(swarm_can_split() && isturf(loc)) + var/turf/our_turf = loc + for(var/turf/swarm_turf as anything in RANGE_TURFS(our_turf, 1)) + if(swarm_turf == loc || !swarm_turf.CanPass(src)) + continue + var/new_intensity = round(swarm_intensity/2) + var/obj/effect/insect_swarm/new_swarm = new type(swarm_turf, insect_type, owner) + new_swarm.swarm_intensity = new_intensity + new_swarm.swarm_agitation = swarm_agitation + new_swarm.update_swarm() + swarm_intensity -= new_intensity + update_swarm() + break + + // Sting people, if we are so inclined. + if(insect_type.sting_amount || insect_type.sting_reagent) + insect_type.try_sting(src, loc) + + // Hive behavior is dictated by the hive. + if(owner) + handle_hive_behavior() + return + + // If we're not agitated and don't have a hive, we probably shouldn't be pathing somewhere. + stop_automove() + + // Idle swarms with no hive just wander around. + if(prob(5)) + SelfMove(pick(global.alldirs)) + +/obj/effect/insect_swarm/proc/is_first_swarm_at_hive() + var/atom/movable/hive = owner?.holder + if(!isturf(hive?.loc) || loc != hive.loc) + return FALSE + if(length(owner?.swarms) == 1) + return TRUE + for(var/obj/effect/insect_swarm/swarm in hive.loc) + if(swarm == src) + return TRUE + if(swarm in owner.swarms) + break + return FALSE + +/obj/effect/insect_swarm/get_automove_target(datum/automove_metadata/metadata) + return move_target + +/obj/effect/insect_swarm/stop_automove() + SHOULD_CALL_PARENT(FALSE) + move_target = null + //. = ..() // TODO work out why they're not automoving + walk(src, 0) + +/obj/effect/insect_swarm/start_automove(target, movement_type, datum/automove_metadata/metadata) + SHOULD_CALL_PARENT(FALSE) + move_target = target + //. = ..() // TODO work out why they're not automoving + if(move_target) + walk_to(src, move_target, 0, 7) + else + walk(src, 0) + +/obj/effect/insect_swarm/proc/handle_hive_behavior() + + var/atom/movable/hive = owner?.holder + if(!isturf(loc)) + // We've just been created; shunt us out onto the turf. + if(loc == hive) + dropInto(hive.loc) + else + return + + // If we are the first (or only) of our owner swarms in the loc, and we aren't needed, we don't move. Hive needs workers. + if(owner?.raw_reserves >= 15) + if(is_first_swarm_at_hive()) + stop_automove() + return + if(!hive_has_swarm() && loc != hive.loc) + start_automove(owner.holder) + return + + do_work() + +/obj/effect/insect_swarm/proc/do_work() + stop_automove() + if(prob(25)) + var/step_dir = pick(global.alldirs) + if(get_dist(owner.holder, get_step(loc, step_dir)) <= 2) + SelfMove(step_dir) + +/obj/effect/insect_swarm/proc/hive_has_swarm() + var/atom/movable/hive = owner?.holder + if(!isturf(hive?.loc)) + return FALSE + for(var/obj/item/swarm as anything in owner.swarms) + if(swarm.loc == hive.loc) + return TRUE + return FALSE + +/obj/effect/insect_swarm/proc/adjust_swarm_intensity(amount) + var/old_intensity = swarm_intensity + swarm_intensity = clamp(swarm_intensity + amount, 0, insect_type.max_swarm_intensity) + if(old_intensity != swarm_intensity) + if(swarm_intensity <= 0) + qdel(src) + else + update_swarm() + +/obj/effect/insect_swarm/proc/can_grow() + // higher swarm intensity is only seen during agitated states when they converge on a victim and merge. + return swarm_intensity < insect_type.max_swarm_growth_intensity + +/obj/effect/insect_swarm/proc/can_merge() + return swarm_intensity < (is_agitated() ? insect_type.max_swarm_intensity : insect_type.max_swarm_growth_intensity) + +/obj/effect/insect_swarm/proc/swarm_can_split() + return !is_agitated() && swarm_intensity > insect_type.max_swarm_growth_intensity + +/obj/effect/insect_swarm/proc/try_consolidate_swarms() + if(!can_merge()) + return + for(var/obj/effect/insect_swarm/other_swarm in loc) + if(other_swarm == src || !other_swarm.can_merge() || other_swarm.owner != owner || other_swarm.insect_type != insect_type) + continue + merge(other_swarm) + return + +/obj/effect/insect_swarm/pollinator + var/pollen = 0 + +/obj/effect/insect_swarm/pollinator/do_work() + + // Have a rest/do some work. + if(world.time < next_work) + return + + // Unload pollen into hive. + if(pollen) + if(loc == get_turf(owner.holder)) + owner.add_reserves(pollen) + pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + else + start_automove(owner.holder) + return + + // Move to flowers. + if(move_target) + if(get_turf(move_target) == loc || !(move_target in view(src, 7))) + move_target = null + stop_automove() + else + start_automove(move_target) + return + + // Harvest from flowers in our loc. + for(var/obj/machinery/portable_atmospherics/hydroponics/flower in loc) + if(!flower.pollen) + continue + if(flower.seed && !flower.dead) + flower.plant_health += rand(3, 5) + flower.check_plant_health() + pollen += flower.pollen + flower.pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + return + + // Find a flower. + var/closest_dist + var/atom/closest_target + for(var/obj/machinery/portable_atmospherics/hydroponics/flower in view(src, 7)) + if(!flower.pollen) + continue + var/next_dist = get_dist(src, closest_target) + if(isnull(closest_dist) || next_dist < closest_dist) + closest_target = flower + closest_dist = next_dist + + if(closest_target) + start_automove(closest_target) + else + start_automove(owner.holder) diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm new file mode 100644 index 00000000000..013e720ae59 --- /dev/null +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -0,0 +1,174 @@ +/decl/insect_species + abstract_type = /decl/insect_species + + // Descriptive strings for individual insects and swarms. + var/name_singular + var/name_plural + var/insect_desc + + // Vars for nest description and products. + var/nest_name + var/list/produce_reagents + var/decl/material/produce_material + var/produce_material_amount = 1 + var/native_frame_name = "comb" + var/native_frame_desc = "A wax comb from an insect nest." + var/native_frame_type = /obj/item/hive_frame/comb + + // Visual appearance and behavior of swarms. + var/swarm_desc + var/swarm_color = COLOR_BROWN + var/swarm_icon = 'mods/content/beekeeping/icons/swarm.dmi' + var/swarm_type = /obj/effect/insect_swarm + var/max_swarm_growth_intensity = 50 + var/max_swarm_intensity = 100 + + // Venom delivered by swarms whens stinging a victim. + var/sting_reagent + var/sting_amount + +/decl/insect_species/Initialize() + if(produce_material) + produce_material = GET_DECL(produce_material) + return ..() + +/decl/insect_species/validate() + . = ..() + + if(!name_singular) + . += "no singular name set" + if(!name_plural) + . += "no plural name set" + if(!nest_name) + . += "no nest name set" + if(!insect_desc) + . += "no insect desc set" + + if(swarm_type) + if(!ispath(swarm_type, /obj/effect/insect_swarm)) + . += "invalid swarm path (must be /obj/effect/insect_swarm or subtype): '[swarm_type]'" + if(!swarm_desc) + . += "no swarm description set" + + if(produce_reagents) + if(!length(produce_reagents) || !islist(produce_reagents)) + . += "empty or non-list produce_reagents" + else + var/total = 0 + for(var/reagent in produce_reagents) + if(!ispath(reagent, /decl/material)) + . += "non-material produce_reagents entry '[reagent]'" + continue + var/amt = produce_reagents[reagent] + if(!isnum(amt) || amt <= 0) + . += "non-numerical or 0 produce_reagents value: '[reagent]', '[amt]'" + total += amt + if(total != 1) + . += "produce_reagents weighting does not sum to 1: '[total]'" + + if(produce_material) + if(!isnum(produce_material_amount) || produce_material_amount <= 0) + . += "non-numeric or zero produce amount: '[produce_material_amount]'" + if(!istype(produce_material, /decl/material)) + . += "non-material product material type: '[produce_material]'" + +/decl/insect_species/proc/fill_hive_frame(obj/item/frame) + + if(!istype(frame) || QDELETED(frame)) + return FALSE + + var/frame_space = REAGENTS_FREE_SPACE(frame.reagents) + if(frame_space <= 0) + return FALSE + + if(frame.reagents?.maximum_volume && length(produce_reagents)) + var/reagent_split = max(1, floor(min(REAGENTS_FREE_SPACE(frame.reagents), 20) / length(produce_reagents))) + for(var/reagent in produce_reagents) + frame.reagents.add_reagent(reagent, max(1, (reagent_split * produce_reagents[reagent])), defer_update = TRUE) + frame.reagents.handle_update() + if(produce_material && (frame.material != produce_material) && !(locate(/obj/item/stack/material/lump) in frame)) + for(var/atom/movable/thing in produce_material.create_object(frame, produce_material_amount, /obj/item/stack/material/lump)) + thing.forceMove(frame) + return TRUE + +/decl/insect_species/proc/try_sting(obj/effect/insect_swarm/swarm, atom/loc) + if(!istype(swarm) || QDELETED(swarm) || !istype(loc)) + return FALSE + // If we're agitated, always sting. Otherwise, % chance equal to a quarter of our overall swarm intensity. + if(!swarm.is_agitated() && !prob(max(1, round(swarm.swarm_intensity/4)))) + return FALSE + var/sting_mult = sting_amount * clamp(round(swarm.swarm_intensity/10), 1, 10) + for(var/mob/living/victim in loc) + if(!victim.simulated || victim.stat || victim.current_posture?.prone) + continue + var/datum/reagents/injected_reagents = victim.get_injected_reagents() + var/obj/item/organ/external/affecting = victim.get_organ(pick(global.all_limb_tags)) + if(!affecting || BP_IS_PROSTHETIC(affecting) || BP_IS_CRYSTAL(affecting)) + continue + if(injected_reagents && victim.can_inject(victim, affecting.organ_tag)) + injected_reagents.add_reagent(sting_reagent, sting_mult) + affecting.add_pain(sting_mult) + if(sting_mult <= sting_amount * 2) + to_chat(victim, SPAN_DANGER("You are stung on your [affecting.name] by \a [swarm]!")) + else + to_chat(victim, SPAN_DANGER("You are stung multiple times on your [affecting.name] by \a [swarm]!")) + . = TRUE + +/decl/insect_species/proc/can_spawn_in_flora(var/obj/structure/flora) + + // Territory range. + for(var/obj/structure/flora/plant in view(flora, 7)) + if(has_extension(plant, /datum/extension/insect_hive)) + return FALSE + + // Food source. + for(var/obj/machinery/portable_atmospherics/hydroponics/flower in view(flora, 7)) + if(flower.seed?.produces_pollen) + return TRUE + + return FALSE + +/decl/insect_species/proc/process_hive(datum/extension/insect_hive/hive_metadata) + + // Sanity check. + var/atom/movable/hive = hive_metadata.holder + if(!istype(hive) || !swarm_type || !istype(hive_metadata)) + return + + // Make sure we always have at least one swarm. + if(!length(hive_metadata.swarms)) + new swarm_type(hive, src, hive_metadata) + + // Reduce swarms if we have too many. + var/swarm_intensity = hive_metadata.get_total_swarm_intensity() + if(swarm_intensity > max_swarm_intensity && length(hive_metadata.swarms)) + var/obj/effect/insect_swarm/swarm = hive_metadata.swarms[1] + swarm.adjust_swarm_intensity(-(swarm_intensity-max_swarm_intensity)) + return + + // Try to grow an existing swarm until we're at our max. + if(hive_metadata.has_reserves(5) && length(hive_metadata.swarms)) + for(var/obj/effect/insect_swarm/swarm as anything in hive_metadata.swarms) + if(swarm.can_grow() && hive_metadata.consume_reserves(5)) + swarm.adjust_swarm_intensity(min(max_swarm_growth_intensity-swarm_intensity, rand(3,5))) + return + + // If we have sufficient filled combs, create a new swarm. Otherwise, expand a swarm. + if(hive.loc && hive_metadata.has_reserves(5)) + + var/obj/effect/insect_swarm/swarm + for(var/obj/effect/insect_swarm/check_swarm as anything in hive_metadata.swarms) + if(check_swarm.loc == hive.loc && check_swarm.can_grow()) + swarm = check_swarm + break + + if(!swarm) + var/comb_count = 0 + for(var/obj/item/hive_frame/frame in hive) + if(frame.reagents && frame.reagents.total_volume >= frame.reagents.maximum_volume) + comb_count++ + if(length(hive_metadata.swarms) < comb_count) + swarm = new swarm_type(hive.loc, src, hive_metadata) + + if(!QDELETED(swarm) && istype(swarm) && hive_metadata.consume_reserves(5)) + swarm.adjust_swarm_intensity(min((max_swarm_growth_intensity-swarm_intensity), rand(3,5))) diff --git a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm new file mode 100644 index 00000000000..0f9b924e62f --- /dev/null +++ b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm @@ -0,0 +1,26 @@ +/decl/insect_species/honeybees + name_singular = "honeybee" + name_plural = "honeybees" + nest_name = "beehive" + native_frame_name = "honeycomb" + native_frame_desc = "A lattice of hexagonal wax cells usually filled with honey." + native_frame_type = /obj/item/hive_frame/comb + swarm_desc = "A swarm of buzzing honeybees." + insect_desc = "A single buzzing honeybee." + swarm_color = COLOR_GOLD + swarm_type = /obj/effect/insect_swarm/pollinator + sting_reagent = /decl/material/liquid/bee_venom + sting_amount = 1 + produce_reagents = list(/decl/material/liquid/nutriment/honey = 1) + produce_material = /decl/material/solid/organic/wax + +/decl/insect_species/wasps + name_singular = "wasp" + name_plural = "wasps" + nest_name = "wasp hive" + swarm_desc = "A swarm of humming wasps." + insect_desc = "A solitary wasp." + sting_reagent = /decl/material/liquid/cyanide + sting_amount = 5 + swarm_color = COLOR_BRONZE + swarm_type = /obj/effect/insect_swarm/pollinator // tarantula hunter... diff --git a/mods/content/beekeeping/icons/apiary.dmi b/mods/content/beekeeping/icons/apiary.dmi new file mode 100644 index 0000000000000000000000000000000000000000..7ad80a4408f3f53bf80d2991b201ac53255a1200 GIT binary patch literal 345 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 diff --git a/mods/content/beekeeping/icons/bee_pack.dmi b/mods/content/beekeeping/icons/bee_pack.dmi new file mode 100644 index 0000000000000000000000000000000000000000..fe68dad8b4b7b784528b38dea0b402b09a99fae4 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|exd5LK*8>L*Xr`G?I$Q%3*s){B zB`@dyUyI#M6gxUPfKqQ#KSu*47)yfuf*Bm1-ADs+Dyu>wN?cNllZ!G7N;32F7#J$% z1cwzAm45#cT=4Ph6D@CDt#fD22X6>9xM=*~kW=W z2U{2ZKQ;Y0!?BF;slHbwFC2)R&3N5mGIM5w%7cT8s~euO-DuV0;O1~`mufsSDCboFyt I=akR{0DX&+ApigX literal 0 HcmV?d00001 diff --git a/mods/content/beekeeping/icons/beehive.dmi b/mods/content/beekeeping/icons/beehive.dmi new file mode 100644 index 0000000000000000000000000000000000000000..9b58ec19c1b74072539d0b269117bffdb25863f9 GIT binary patch literal 319 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 LF0}L^{MG*ef&6Bj diff --git a/mods/content/beekeeping/icons/comb.dmi b/mods/content/beekeeping/icons/comb.dmi new file mode 100644 index 0000000000000000000000000000000000000000..edb611c93ae4cfa8c7a3c51219b16f0b8858a17e GIT binary patch literal 1077 zcmV-51j_q~P)V=-0C=2r%DoDJAQ(pB+3zY|YA*U)UBU>5x`#?jp%PT;_8XcS+RlL|Mcegq zgcu}U+6U`Sa5Q;A?6xWzaMTqsqwQwmJ0VU{gCn_}-&I0PSs%br5?lRr=(oQrTih}K z(G=Y%AO=Yf@ZToKT(=aW000AcNkl^8@84&$*)NSoL?)Fc`E- zrP7;3BJqpI-tHCs~&JUa!|4jYhVnX}>lajn~O! zvKoy>yKeS&Re)g_uq+Fjropl-Twh-!6bd00i(xPrAP@*391f$`>*46=2=#jXX^9`_ zvzn%vmSwRwH#Z1{LVw0$vD?95uoege`r&Z6-|O|5!^1;!9{!q}H%XEN01!nHj^lt3 zf^<5KcDoJJG_kd{gLYPb@!?xS)H>PR6df+pJkZvZE zVRv_TyKece=Kw`f#)e@)k|d;3DG)+Xuh(H&76>5_MG=$NCn^?;&o&DvigGr?7YHHU z2R=&(>58IgJ;D!vZ{N8lK}nKe7zXzD_u)7WR8c6N5m%*;tiNr#7r zz`(%3z`!&NX{7)F00DGTPE!Ct=GbNc005JER9JLGWpiV4X>fFDZ*Bkpc$`yKaB_9` z^iy#0_2eo`Eh^5;&r`5fFwryM;w;ZhDainGjE%TBGg33tGk;1ToZ^zil2jm5sXV_Z zCq;>iGbOXA7|1u|;!G<_%uR)`;i@u9a})FOGgB0j@>42xi*xcz;*(NyN)Y-?h}KsW zpOKiCLXOTHygHQ?T>V_YK>z@Og;3&NQFc85007iUL_t(&f$f+}4#F@DMC%3n7^&d^ zh=Xv0cEJ(2|9=%oS2)N@aHc}4n73-u=E?oZqf8SaJEAE;>*adK3J_IHaK1gTTgXpP zFsovM3s^zgDnJZrt6*n=galFqA0NUTDkv90u2-B;LF~0Uf*4Z%h1gRggb+f0hHT|L2S)=0bii!ZRFPJD0$iyd|1gL}4`WG^R#5KHoZm^|R-g}&gO2i&~yg}^(E z2uvOr64*R|Z4nVd2q8ygECN`;3O3NO2w(+E|C%<80OZ)b0Dm$Nm$EhwRsaA107*qo IM6N<$f`7cmv;Y7A literal 1019 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSv#LTON?cNllZ!G7N;32F7#J$% z%ssuA>yUv&>qE<(UC!3we_qaV4f1OHCg$a}h;zwLg-)^e_ZBwqSg~U+^ZVe(cfMw| z$cXH}J;m0QW6N&a^ws7^9UqiDITKl5nrglCMUrB`+9^#dEsX6_Ib__KcD2>_F?7aFV0|T>+ zr;B4q#hkZybo-PXW!gU0=Z7uYafpMZ{YH=sL!*vo#$~pHn#UHTP1HEo@G2odTTrG! z`a$*4W$LF>#AhwC*SPn5dXGfLB(IMp9|c!UwmHwcYhS4?LrFKo5y1uvCINSbM;s0^ zj2+4hg$Efc-gkdm|7ZWc#D%)|o#meF)puJw@$2u}**ZT8t^+YTOJGB>lN>74Q2TL|7@B5{d1gxT;Ljp48Hp;*A7^Uznz{im04r@ z_w3^udjHSuJk+Ihi$jb3!Cw7KCVQ)VW-Tj<=X_o%SNh@NewH2gHeP-(r?^%1qnW^; z&<~qWoNx9^xBn)(;L~x3&8PdD;@>}3PMGtKi($R`ukQ>MJm7F)QRu@AEW(0|?n_;3 zds_5!r?A%PP0R286fyD(Ymh$WdAF{n#CD3;sltDSUK>;n?t&TQ9Asjmz5J!^WMUEug} zheAxVfl24n^zQO=rM4}88<{{U39A}3!I&%d91o>f`@8E{S5({YoArWOV40VxhkWBx-co9ECzaln6fAg}Pcw5$4JIeu>n;AS^{an^LB{Ts5PVTgY diff --git a/mods/content/beekeeping/icons/swarm.dmi b/mods/content/beekeeping/icons/swarm.dmi new file mode 100644 index 0000000000000000000000000000000000000000..1d8ea6f1b1ae8e237aaaefaf5352ad590422c65c GIT binary patch literal 1016 zcmVV=-0C=2J zR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+(=$pSoZ^zil2jm5 z$v}yVGbOXA7|1r{;!G<_%uR)`;VKNVt1ttKrsgD80+~iSaEQ=mgk75@R&D5-jmfjq zggkAg1VIpP=E)`bg$(EWxzFaRs2vf)T9ilpwumJRE7-|%>5mY;1#v)<|-QZ z?Z|);!<5D>7emjJaG);mUL>xr3`Q>=44Ts^k_1Tb{>d<1-dOSwJhG$OCBi z33*DnTR8cE zW(1R{(<5Yh3|70j(MBpjZ@r9{GxgFa0>h~}7S&HrMK2+v`cpQ@mG|o4U1&7fb_^!> zB7flXLdJQVrgMGKC#latr(he^sia*+p$pqY4;e2r6Myx_-%2A;Q;fjoMKr8!KQs5V z%h7LB_3o0mff5Qc(1(`gXifcPuZIhrpCeRP?3KXMioXYou start assembling \the [src]...") - if(do_after(user, 30, src)) - user.visible_message("\The [user] constructs a beehive.", "You construct a beehive.") - new /obj/machinery/beehive(get_turf(user)) - qdel(src) - /obj/item/bee_smoker name = "bee smoker" desc = "A device used to calm down bees before harvesting honey." @@ -19,29 +5,3 @@ icon_state = ICON_STATE_WORLD w_class = ITEM_SIZE_SMALL material = /decl/material/solid/metal/steel - -/obj/item/bee_pack - name = "bee pack" - desc = "Contains a queen bee and some worker bees. Everything you'll need to start a hive!" - icon = 'mods/content/beekeeping/icons/beekeeping.dmi' - icon_state = "beepack" - material = /decl/material/solid/organic/plastic - var/full = 1 - -/obj/item/bee_pack/Initialize() - . = ..() - overlays += "beepack-full" - -/obj/item/bee_pack/proc/empty() - full = 0 - name = "empty bee pack" - desc = "A stasis pack for moving bees. It's empty." - overlays.Cut() - overlays += "beepack-empty" - -/obj/item/bee_pack/proc/fill() - full = initial(full) - SetName(initial(name)) - desc = initial(desc) - overlays.Cut() - overlays += "beepack-full" diff --git a/mods/content/beekeeping/materials.dm b/mods/content/beekeeping/materials.dm new file mode 100644 index 00000000000..496d5f9b394 --- /dev/null +++ b/mods/content/beekeeping/materials.dm @@ -0,0 +1,20 @@ +/decl/material/liquid/bee_venom + name = "bee venom" + uid = "liquid_venom_bee" + lore_text = "An irritant used by bees to drive off predators." + taste_description = "noxious bitterness" + color = "#d7d891" + heating_products = list( + /decl/material/liquid/denatured_toxin = 1 + ) + heating_point = 100 CELSIUS + heating_message = "becomes clear." + taste_mult = 1.2 + metabolism = REM * 0.25 + exoplanet_rarity_plant = MAT_RARITY_UNCOMMON + exoplanet_rarity_gas = MAT_RARITY_EXOTIC + +/decl/material/liquid/bee_venom/affect_blood(mob/living/M, removed, datum/reagents/holder) + . = ..() + if(istype(M)) + M.adjustHalLoss(max(1, ceil(removed * 10))) diff --git a/mods/content/beekeeping/recipes.dm b/mods/content/beekeeping/recipes.dm index 8494617d347..3d851511382 100644 --- a/mods/content/beekeeping/recipes.dm +++ b/mods/content/beekeeping/recipes.dm @@ -1,6 +1,5 @@ -/decl/stack_recipe/planks/beehive_assembly - result_type = /obj/item/beehive_assembly - category = "furniture" +/decl/stack_recipe/planks/furniture/apiary + result_type = /obj/structure/apiary /decl/stack_recipe/planks/beehive_frame - result_type = /obj/item/hive_frame/crafted + result_type = /obj/item/hive_frame/crafted diff --git a/mods/content/beekeeping/trading.dm b/mods/content/beekeeping/trading.dm index 45009265f1b..f79cceb6cc5 100644 --- a/mods/content/beekeeping/trading.dm +++ b/mods/content/beekeeping/trading.dm @@ -1,14 +1,14 @@ /datum/trader/trading_beacon/manufacturing/New() - LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/bee_smoker, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/beehive_assembly, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/bee_smoker, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/stack/material/plank/mapped/wood/ten, TRADER_THIS_TYPE) ..() /decl/hierarchy/supply_pack/hydroponics/bee_keeper name = "Equipment - Beekeeping" contains = list( - /obj/item/beehive_assembly, + /obj/item/stack/material/plank/mapped/wood/ten, /obj/item/bee_smoker, /obj/item/hive_frame/crafted = 5, /obj/item/bee_pack From 160712f1b2ceb4a83720d86142681e73cec58801 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 11:10:25 +1000 Subject: [PATCH 09/79] Reimplementing bee smoking. --- mods/content/beekeeping/closets.dm | 2 +- .../beekeeping/hives/hive_extension.dm | 16 ++++++--- mods/content/beekeeping/hives/hive_swarm.dm | 9 ++++- mods/content/beekeeping/items.dm | 33 +++++++++++++++++-- mods/content/beekeeping/trading.dm | 4 +-- 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/mods/content/beekeeping/closets.dm b/mods/content/beekeeping/closets.dm index 9caf9bb459c..daf63a942d5 100644 --- a/mods/content/beekeeping/closets.dm +++ b/mods/content/beekeeping/closets.dm @@ -5,7 +5,7 @@ /obj/structure/closet/crate/hydroponics/beekeeping/Initialize() . = ..() new /obj/item/stack/material/plank/mapped/wood/ten - new /obj/item/bee_smoker(src) + new /obj/item/smoker(src) new /obj/item/hive_frame/crafted(src) new /obj/item/hive_frame/crafted(src) new /obj/item/hive_frame/crafted(src) diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index d02839f4d40..576bbfc369f 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -11,8 +11,8 @@ var/raw_reserves = 0 /// Tracker for the last world.time that a frame was removed. var/frame_last_removed = 0 - /// Tracker for ticks remaning since we were last smoked. - var/smoked_out = 0 + /// Tracker for time that smoke will wear off. + var/smoked_until = 0 /datum/extension/insect_hive/New(datum/holder, _species_decl) ..() @@ -33,8 +33,7 @@ return ..() /datum/extension/insect_hive/Process() - if(smoked_out > 0) - smoked_out-- + if(world.time < smoked_until) return holding_species.process_hive(src) create_hive_products() @@ -69,7 +68,7 @@ /datum/extension/insect_hive/proc/frame_removed(obj/item/frame) frame_last_removed = world.time - if(!smoked_out) + if(world.time >= smoked_until) for(var/obj/effect/insect_swarm/swarm in swarms) swarm.swarm_agitation = min(100, swarm.swarm_agitation + 5) @@ -193,3 +192,10 @@ . = 0 for(var/obj/effect/insect_swarm/swarm as anything in swarms) . += swarm.swarm_intensity + +/datum/extension/insect_hive/proc/smoked_by(mob/user, atom/source, smoke_time = 10 SECONDS) + smoked_until = max(smoked_until, world.time + smoke_time) + // this is a little weird due to telekinetic bee smoking but so it goes + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.was_smoked(max(0, smoked_until-world.time)) + return TRUE diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 2e733a96b13..662e972cd41 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -22,6 +22,8 @@ var/const/MAX_SWARM_STATE = 6 /// Cooldown timer for next tick. VAR_PRIVATE/next_work = 0 + /// Time that smoke will wear off. + var/smoked_until = 0 /datum/movement_handler/delay/insect_swarm delay = 1 SECOND @@ -76,7 +78,7 @@ swarm_transform.Turn(pick(90, 180, 270)) /obj/effect/insect_swarm/proc/is_agitated() - return QDELETED(owner) || swarm_agitation > 0 + return QDELETED(owner) || (swarm_agitation > 0 && world.time > smoked_until) /obj/effect/insect_swarm/proc/find_sting_target() for(var/mob/living/victim in view(7, src)) @@ -320,3 +322,8 @@ start_automove(closest_target) else start_automove(owner.holder) + +// TODO: update icon (twitching on ground?) +// TODO: lower agitation +/obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) + smoked_until = max(smoked_until, world.time + smoke_time) diff --git a/mods/content/beekeeping/items.dm b/mods/content/beekeeping/items.dm index a309ff921a2..1161ae82fd9 100644 --- a/mods/content/beekeeping/items.dm +++ b/mods/content/beekeeping/items.dm @@ -1,7 +1,34 @@ -/obj/item/bee_smoker - name = "bee smoker" - desc = "A device used to calm down bees before harvesting honey." +/obj/item/smoker + name = "smoker" + desc = "A device used to calm insects down before harvesting from a hive." icon = 'mods/content/beekeeping/icons/smoker.dmi' icon_state = ICON_STATE_WORLD w_class = ITEM_SIZE_SMALL material = /decl/material/solid/metal/steel + +// TODO: consume reagents or charges? Unnecessary complexity? +/obj/item/smoker/resolve_attackby(atom/A, mob/user, click_params) + + if(!user.check_dexterity(get_required_attack_dexterity(user, A))) + return TRUE + + var/smoked = FALSE + if(has_extension(A, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(A, /datum/extension/insect_hive) + if(hive.smoked_by(user, A)) + smoked = TRUE + + if(!smoked && isturf(A)) + for(var/obj/effect/insect_swarm/swarm in A) + swarm.was_smoked() + smoked = TRUE + + if(smoked) + var/turf/smoked_turf = get_turf(user) + if(smoked_turf) + playsound(smoked_turf, 'sound/effects/refill.ogg', 25, 1) + user.visible_message(SPAN_NOTICE("\The [user] douses \the [A] in smoke from \the [src].")) + new /obj/effect/effect/smoke(smoked_turf, 2 SECONDS) + return TRUE + + return ..() diff --git a/mods/content/beekeeping/trading.dm b/mods/content/beekeeping/trading.dm index f79cceb6cc5..c63d01f412c 100644 --- a/mods/content/beekeeping/trading.dm +++ b/mods/content/beekeeping/trading.dm @@ -1,6 +1,6 @@ /datum/trader/trading_beacon/manufacturing/New() LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/bee_smoker, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/smoker, TRADER_THIS_TYPE) LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) LAZYSET(possible_trading_items, /obj/item/stack/material/plank/mapped/wood/ten, TRADER_THIS_TYPE) ..() @@ -9,7 +9,7 @@ name = "Equipment - Beekeeping" contains = list( /obj/item/stack/material/plank/mapped/wood/ten, - /obj/item/bee_smoker, + /obj/item/smoker, /obj/item/hive_frame/crafted = 5, /obj/item/bee_pack ) From 8f164aeb7d234eae056f72df730cb36ca311136f Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 11:55:47 +1000 Subject: [PATCH 10/79] Things can be stored inside dead trees. --- code/game/objects/structures/flora/tree.dm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/game/objects/structures/flora/tree.dm b/code/game/objects/structures/flora/tree.dm index 57c59edea80..12c7df0a884 100644 --- a/code/game/objects/structures/flora/tree.dm +++ b/code/game/objects/structures/flora/tree.dm @@ -114,6 +114,11 @@ var/global/list/christmas_trees = list() icon_state = "tree_1" protects_against_weather = FALSE stump_type = /obj/structure/flora/stump/tree/dead + storage = /datum/storage/dead_tree + +/datum/storage/dead_tree + max_w_class = ITEM_SIZE_NORMAL + max_storage_space = ITEM_SIZE_SMALL * 5 /obj/structure/flora/tree/dead/random/init_appearance() icon_state = "tree_[rand(1, 6)]" From ac7b7b4849d7cad12b6b577f39b7fe5ed2205d9a Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 13:04:14 +1000 Subject: [PATCH 11/79] Implementing hive spawning/destruction logic. --- code/game/objects/structures/flora/plant.dm | 36 +++++++++++++++++-- code/game/objects/structures/flora/stump.dm | 1 + .../beekeeping/hives/hive_extension.dm | 11 ++++-- .../beekeeping/hives/hive_structure.dm | 11 +++--- mods/content/beekeeping/hives/hive_swarm.dm | 33 +++++++++++++---- .../hives/insect_species/_insects.dm | 5 +++ 6 files changed, 81 insertions(+), 16 deletions(-) diff --git a/code/game/objects/structures/flora/plant.dm b/code/game/objects/structures/flora/plant.dm index 9e44f55a2f3..b17d620d639 100644 --- a/code/game/objects/structures/flora/plant.dm +++ b/code/game/objects/structures/flora/plant.dm @@ -7,14 +7,20 @@ var/dead = FALSE var/sampled = FALSE var/datum/seed/plant - var/harvestable + var/harvestable = 0 + var/pollen = 0 /obj/structure/flora/plant/large opacity = TRUE density = TRUE -/* Notes for future work moving logic off hydrotrays onto plants themselves: /obj/structure/flora/plant/Process() + if(plant?.produces_pollen <= 0) + return PROCESS_KILL + if(pollen < 10) + pollen += plant.produces_pollen + +/* Notes for future work moving logic off hydrotrays onto plants themselves: // check our immediate environment // ask our environment for available reagents // process the reagents @@ -61,9 +67,13 @@ var/potency = plant.get_trait(TRAIT_POTENCY) set_light(l_range = max(1, round(potency/10)), l_power = clamp(round(potency/30), 0, 1), l_color = plant.get_trait(TRAIT_BIOLUM_COLOUR)) update_icon() - return ..() + . = ..() + if(plant?.produces_pollen && !is_processing) + START_PROCESSING(SSprocessing, src) /obj/structure/flora/plant/Destroy() + if(is_processing) + STOP_PROCESSING(SSprocessing, src) plant = null . = ..() @@ -147,3 +157,23 @@ /obj/structure/flora/plant/random_mushroom/Initialize() plant = pick(get_mushroom_variants()) return ..() + +/obj/structure/flora/plant/random_flower + name = "flower" + color = COLOR_PINK + icon_state = "flower5" + is_spawnable_type = TRUE + +/obj/structure/flora/plant/random_flower/proc/get_flower_variants() + var/static/list/flower_variants + if(isnull(flower_variants)) + flower_variants = list() + for(var/plant in SSplants.seeds) + var/datum/seed/seed = SSplants.seeds[plant] + if(!isnull(seed?.name) && seed.produces_pollen) + flower_variants |= seed.name + return flower_variants + +/obj/structure/flora/plant/random_flower/Initialize() + plant = pick(get_flower_variants()) + return ..() diff --git a/code/game/objects/structures/flora/stump.dm b/code/game/objects/structures/flora/stump.dm index 4911cd3a57e..f13415593f7 100644 --- a/code/game/objects/structures/flora/stump.dm +++ b/code/game/objects/structures/flora/stump.dm @@ -4,6 +4,7 @@ /obj/structure/flora/stump name = "stump" hitsound = 'sound/effects/hit_wood.ogg' + storage = /datum/storage/dead_tree var/log_type = /obj/item/stack/material/log /obj/structure/flora/stump/get_material_health_modifier() diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index 576bbfc369f..7bd559e74cf 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -1,3 +1,5 @@ +#define DEFAULT_FRAME_COST 20 + /datum/extension/insect_hive base_type = /datum/extension/insect_hive expected_type = /obj/structure @@ -42,7 +44,12 @@ return FALSE /datum/extension/insect_hive/proc/drop_nest(atom/drop_loc) - return + if(!isatom(drop_loc)) + return + // handle some kind of physical hive dropping here + remove_extension(holder, /datum/extension/insect_hive) + if(!QDELETED(src)) + qdel(src) /datum/extension/insect_hive/proc/get_nest_condition() switch(current_health) @@ -161,7 +168,7 @@ adjust_health(rand(3,5)) return TRUE - if(!has_reserves(20)) + if(!has_reserves(DEFAULT_FRAME_COST)) return TRUE var/list/holder_contents = hive.get_contained_external_atoms() diff --git a/mods/content/beekeeping/hives/hive_structure.dm b/mods/content/beekeeping/hives/hive_structure.dm index f7586ad6eec..33eb99e67c8 100644 --- a/mods/content/beekeeping/hives/hive_structure.dm +++ b/mods/content/beekeeping/hives/hive_structure.dm @@ -25,11 +25,14 @@ if(istype(hive)) hive.examined(user, (distance <= 1)) +/atom/physically_destroyed(var/skip_qdel) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + hive?.drop_nest(loc) + return ..() + /obj/structure/dismantle_structure(mob/user) - if(isatom(loc)) - var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) - if(istype(hive)) - hive.drop_nest(loc) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + hive?.drop_nest(loc) return ..() // 'proper' nest structure for building and mapping diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 662e972cd41..3284152915f 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -211,7 +211,7 @@ return // If we are the first (or only) of our owner swarms in the loc, and we aren't needed, we don't move. Hive needs workers. - if(owner?.raw_reserves >= 15) + if(owner?.raw_reserves >= DEFAULT_FRAME_COST) if(is_first_swarm_at_hive()) stop_automove() return @@ -307,15 +307,34 @@ stop_automove() return + // Same logic for flora. TODO unify these when seeds are rewritten to be less bespoke. + for(var/obj/structure/flora/plant/flower in loc) + if(!flower.pollen) + continue + pollen += flower.pollen + flower.pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + return + // Find a flower. + var/list/all_potential_targets = list() + for(var/thing in view(src, 7)) + if(istype(thing, /obj/machinery/portable_atmospherics/hydroponics)) + var/obj/machinery/portable_atmospherics/hydroponics/flower = thing + if(flower.pollen) + all_potential_targets += flower + else if(istype(thing, /obj/structure/flora/plant)) + var/obj/structure/flora/plant/flower = thing + if(flower.pollen) + all_potential_targets += flower + var/closest_dist var/atom/closest_target - for(var/obj/machinery/portable_atmospherics/hydroponics/flower in view(src, 7)) - if(!flower.pollen) - continue - var/next_dist = get_dist(src, closest_target) - if(isnull(closest_dist) || next_dist < closest_dist) - closest_target = flower + for(var/atom/thing as anything in shuffle(all_potential_targets)) + var/next_dist = get_dist(src, thing) + if(isnull(closest_target) || next_dist < closest_dist) + closest_target = thing closest_dist = next_dist if(closest_target) diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm index 013e720ae59..0e3dcac55a5 100644 --- a/mods/content/beekeeping/hives/insect_species/_insects.dm +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -126,6 +126,11 @@ if(flower.seed?.produces_pollen) return TRUE + for(var/obj/structure/flora/plant/flower in view(flora, 7)) + if(flower.plant?.produces_pollen) + return TRUE + + return FALSE /decl/insect_species/proc/process_hive(datum/extension/insect_hive/hive_metadata) From 13ab3be3a368787ed414ada6ab6ca437e1d71db5 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 14:47:08 +1000 Subject: [PATCH 12/79] Adjusting rate of development for hives/hive products. --- code/__defines/misc.dm | 5 ++++ code/game/objects/structures/flora/plant.dm | 6 ++--- .../modules/hydroponics/trays/tray_process.dm | 2 +- code/modules/mob/skills/skillset.dm | 3 ++- mods/content/beekeeping/_beekeeping.dm | 7 ++++++ .../beekeeping/hives/hive_extension.dm | 23 +++++++++++-------- mods/content/beekeeping/hives/hive_flora.dm | 4 ++-- mods/content/beekeeping/hives/hive_frame.dm | 15 ++++++++++-- mods/content/beekeeping/hives/hive_swarm.dm | 20 ++++++++++++---- .../hives/insect_species/_insects.dm | 16 ++++++------- mods/content/beekeeping/items.dm | 2 +- 11 files changed, 69 insertions(+), 34 deletions(-) diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 3721efb82c0..6a1de9936b7 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -402,3 +402,8 @@ #define MM_ATTACK_RESULT_NONE 0 #define MM_ATTACK_RESULT_DEFLECTED BITFLAG(0) #define MM_ATTACK_RESULT_BLOCKED BITFLAG(1) + +// Effectively a speed modifier for how fast pollen is produced by flowering plants. Pollen per second. +// In theory, one pollen every 5 seconds (at time of writing) +#define POLLEN_PER_SECOND 0.2 +#define POLLEN_PRODUCTION_MULT (POLLEN_PER_SECOND * (SSplants.wait / 10)) diff --git a/code/game/objects/structures/flora/plant.dm b/code/game/objects/structures/flora/plant.dm index b17d620d639..c56c1886479 100644 --- a/code/game/objects/structures/flora/plant.dm +++ b/code/game/objects/structures/flora/plant.dm @@ -18,7 +18,7 @@ if(plant?.produces_pollen <= 0) return PROCESS_KILL if(pollen < 10) - pollen += plant.produces_pollen + pollen += plant.produces_pollen * POLLEN_PRODUCTION_MULT /* Notes for future work moving logic off hydrotrays onto plants themselves: // check our immediate environment @@ -69,11 +69,11 @@ update_icon() . = ..() if(plant?.produces_pollen && !is_processing) - START_PROCESSING(SSprocessing, src) + START_PROCESSING(SSplants, src) /obj/structure/flora/plant/Destroy() if(is_processing) - STOP_PROCESSING(SSprocessing, src) + STOP_PROCESSING(SSplants, src) plant = null . = ..() diff --git a/code/modules/hydroponics/trays/tray_process.dm b/code/modules/hydroponics/trays/tray_process.dm index d692dc841a5..d6df7e1c6ca 100644 --- a/code/modules/hydroponics/trays/tray_process.dm +++ b/code/modules/hydroponics/trays/tray_process.dm @@ -66,7 +66,7 @@ mutation_level = 0 if(pollen < 10) - pollen += seed?.produces_pollen + pollen += seed?.produces_pollen * POLLEN_PRODUCTION_MULT // Maintain tray nutrient and water levels. if(seed.get_trait(TRAIT_REQUIRES_NUTRIENTS) && seed.get_trait(TRAIT_NUTRIENT_CONSUMPTION) > 0 && nutrilevel > 0 && prob(25)) diff --git a/code/modules/mob/skills/skillset.dm b/code/modules/mob/skills/skillset.dm index eef15136063..9da79495a21 100644 --- a/code/modules/mob/skills/skillset.dm +++ b/code/modules/mob/skills/skillset.dm @@ -47,7 +47,8 @@ var/global/list/all_skill_verbs QDEL_NULL(NM) //Clean all nano_modules for simplicity. QDEL_NULL(mob.skillset.NM) QDEL_NULL_LIST(nm_viewing) - QDEL_NULL_LIST(mob.skillset.nm_viewing) + if(mob.skillset) + QDEL_NULL_LIST(mob.skillset.nm_viewing) on_levels_change() //Called when a player is added as an antag and the antag datum processes the skillset. diff --git a/mods/content/beekeeping/_beekeeping.dm b/mods/content/beekeeping/_beekeeping.dm index 471217ac52d..53ca84daf62 100644 --- a/mods/content/beekeeping/_beekeeping.dm +++ b/mods/content/beekeeping/_beekeeping.dm @@ -1,3 +1,10 @@ +#define FRAME_RESERVE_COST 30 +#define SWARM_AGITATION_PER_FRAME 25 +#define FRAME_MATERIAL_COST 20 +#define SWARM_GROWTH_COST 10 +#define FRAME_FILL_MATERIAL_COST 5 +#define HIVE_REPAIR_MATERIAL_COST 5 + /decl/modpack/beekeeping name = "Beekeeping and Insects Content" diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index 7bd559e74cf..0917903fae5 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -1,5 +1,3 @@ -#define DEFAULT_FRAME_COST 20 - /datum/extension/insect_hive base_type = /datum/extension/insect_hive expected_type = /obj/structure @@ -75,9 +73,12 @@ /datum/extension/insect_hive/proc/frame_removed(obj/item/frame) frame_last_removed = world.time - if(world.time >= smoked_until) - for(var/obj/effect/insect_swarm/swarm in swarms) - swarm.swarm_agitation = min(100, swarm.swarm_agitation + 5) + if(world.time >= smoked_until && length(swarms) > 0) + if(isatom(holder)) + var/atom/hive = holder + hive.visible_message(SPAN_DANGER("The buzzing from \the [holder] intensifies.")) + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.swarm_agitation = min(100, swarm.swarm_agitation + SWARM_AGITATION_PER_FRAME) /datum/extension/insect_hive/proc/try_hand_harvest(mob/user) return FALSE @@ -159,16 +160,16 @@ return TRUE // Naturally build up enough material for a new frame (or repairs). - if(!has_material(20)) + if(!has_material(FRAME_MATERIAL_COST)) add_material(1) // Damaged hives cannot produce combs or honey. if(current_health < 100) - if(consume_material(5)) + if(consume_material(HIVE_REPAIR_MATERIAL_COST)) adjust_health(rand(3,5)) return TRUE - if(!has_reserves(DEFAULT_FRAME_COST)) + if(!has_reserves(FRAME_RESERVE_COST)) return TRUE var/list/holder_contents = hive.get_contained_external_atoms() @@ -176,7 +177,9 @@ if(!frame.reagents || (frame.reagents.total_volume >= frame.reagents.maximum_volume)) continue var/fill_cost = REAGENTS_FREE_SPACE(frame.reagents) - if(consume_material(5) && consume_reserves(fill_cost)) + if(has_material(FRAME_FILL_MATERIAL_COST) && has_reserves(fill_cost)) + consume_material(FRAME_FILL_MATERIAL_COST) + consume_reserves(fill_cost) holding_species.fill_hive_frame(frame) return TRUE @@ -189,7 +192,7 @@ return // Put a timer check on this to avoid a hive filling up with combs the moment you take 2 frames out. - if(world.time > (frame_last_removed + 2 MINUTES) && space_left >= native_frame_size && consume_material(20)) + if(world.time > (frame_last_removed + 2 MINUTES) && space_left >= native_frame_size && consume_material(FRAME_MATERIAL_COST)) // Frames start empty, and will be filled next run. // Native 'frames' (combs) are bigger than crafted ones and aren't reusable. new native_frame(holder, holding_species.produce_material) diff --git a/mods/content/beekeeping/hives/hive_flora.dm b/mods/content/beekeeping/hives/hive_flora.dm index e6091c3610b..d6bc3a50c34 100644 --- a/mods/content/beekeeping/hives/hive_flora.dm +++ b/mods/content/beekeeping/hives/hive_flora.dm @@ -28,13 +28,13 @@ /obj/structure/flora/tree/get_supported_insects() var/static/list/_insects = list( /decl/insect_species/honeybees = 10, - /decl/insect_species/wasps = 1 + ///decl/insect_species/wasps = 1 ) return _insects /obj/structure/flora/stump/get_supported_insects() var/static/list/_insects = list( /decl/insect_species/honeybees = 10, - /decl/insect_species/wasps = 1 + ///decl/insect_species/wasps = 1 ) return _insects diff --git a/mods/content/beekeeping/hives/hive_frame.dm b/mods/content/beekeeping/hives/hive_frame.dm index be57c7adf7a..473e667e521 100644 --- a/mods/content/beekeeping/hives/hive_frame.dm +++ b/mods/content/beekeeping/hives/hive_frame.dm @@ -41,10 +41,21 @@ var/decl/insect_species/bees = GET_DECL(/decl/insect_species/honeybees) bees.fill_hive_frame(src) +/obj/item/hive_frame/forceMove(atom/dest) + var/atom/old_loc = loc + . = ..() + if(. && istype(old_loc)) + check_hive_loc(old_loc) + /obj/item/hive_frame/Move() - var/datum/extension/insect_hive/hive = get_extension(loc, /datum/extension/insect_hive) + var/atom/old_loc = loc . = ..() - if(. && istype(hive) && loc != hive.holder) + if(. && istype(old_loc)) + check_hive_loc(old_loc) + +/obj/item/hive_frame/proc/check_hive_loc(atom/check_loc) + var/datum/extension/insect_hive/hive = get_extension(check_loc, /datum/extension/insect_hive) + if(istype(hive) && loc != hive.holder) hive.frame_removed(src) // Crafted frame used in apiaries. diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 3284152915f..544b7f33406 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -78,7 +78,7 @@ swarm_transform.Turn(pick(90, 180, 270)) /obj/effect/insect_swarm/proc/is_agitated() - return QDELETED(owner) || (swarm_agitation > 0 && world.time > smoked_until) + return QDELETED(owner) || (swarm_agitation > 0 && !is_smoked()) /obj/effect/insect_swarm/proc/find_sting_target() for(var/mob/living/victim in view(7, src)) @@ -129,14 +129,21 @@ if(!move_target || !(move_target in view(5, src))) stop_automove() + if(is_smoked()) + return + // Angry swarms move with purpose. if(is_agitated()) swarm_agitation = max(0, swarm_agitation-1) - if(!move_target) - move_target = find_sting_target() + if(!ismob(move_target)) + var/mob/new_move_target = find_sting_target() + if(istype(new_move_target)) + move_target = new_move_target if(move_target) start_automove(move_target) - return + if(insect_type.sting_amount || insect_type.sting_reagent) + insect_type.try_sting(src, loc) + return // Large swarms split if they aren't agitated. if(swarm_can_split() && isturf(loc)) @@ -211,7 +218,7 @@ return // If we are the first (or only) of our owner swarms in the loc, and we aren't needed, we don't move. Hive needs workers. - if(owner?.raw_reserves >= DEFAULT_FRAME_COST) + if(owner?.has_reserves(FRAME_RESERVE_COST)) if(is_first_swarm_at_hive()) stop_automove() return @@ -346,3 +353,6 @@ // TODO: lower agitation /obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) smoked_until = max(smoked_until, world.time + smoke_time) + +/obj/effect/insect_swarm/proc/is_smoked() + return world.time < smoked_until \ No newline at end of file diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm index 0e3dcac55a5..37e4481748c 100644 --- a/mods/content/beekeeping/hives/insect_species/_insects.dm +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -97,7 +97,8 @@ // If we're agitated, always sting. Otherwise, % chance equal to a quarter of our overall swarm intensity. if(!swarm.is_agitated() && !prob(max(1, round(swarm.swarm_intensity/4)))) return FALSE - var/sting_mult = sting_amount * clamp(round(swarm.swarm_intensity/10), 1, 10) + var/base_sting_chance = (sting_amount * clamp(round(swarm.swarm_intensity/10), 1, 10)) + var/sting_mult = swarm.is_agitated() ? max(base_sting_chance, 65) : base_sting_chance for(var/mob/living/victim in loc) if(!victim.simulated || victim.stat || victim.current_posture?.prone) continue @@ -106,12 +107,9 @@ if(!affecting || BP_IS_PROSTHETIC(affecting) || BP_IS_CRYSTAL(affecting)) continue if(injected_reagents && victim.can_inject(victim, affecting.organ_tag)) + to_chat(victim, SPAN_DANGER("\A [swarm] stings you [sting_mult <= sting_amount * 2 ? "" : "multiple times"] on your [affecting.name]!")) injected_reagents.add_reagent(sting_reagent, sting_mult) affecting.add_pain(sting_mult) - if(sting_mult <= sting_amount * 2) - to_chat(victim, SPAN_DANGER("You are stung on your [affecting.name] by \a [swarm]!")) - else - to_chat(victim, SPAN_DANGER("You are stung multiple times on your [affecting.name] by \a [swarm]!")) . = TRUE /decl/insect_species/proc/can_spawn_in_flora(var/obj/structure/flora) @@ -152,14 +150,14 @@ return // Try to grow an existing swarm until we're at our max. - if(hive_metadata.has_reserves(5) && length(hive_metadata.swarms)) + if(hive_metadata.has_reserves(SWARM_GROWTH_COST) && length(hive_metadata.swarms)) for(var/obj/effect/insect_swarm/swarm as anything in hive_metadata.swarms) - if(swarm.can_grow() && hive_metadata.consume_reserves(5)) + if(swarm.can_grow() && hive_metadata.consume_reserves(SWARM_GROWTH_COST)) swarm.adjust_swarm_intensity(min(max_swarm_growth_intensity-swarm_intensity, rand(3,5))) return // If we have sufficient filled combs, create a new swarm. Otherwise, expand a swarm. - if(hive.loc && hive_metadata.has_reserves(5)) + if(hive.loc && hive_metadata.has_reserves(SWARM_GROWTH_COST)) var/obj/effect/insect_swarm/swarm for(var/obj/effect/insect_swarm/check_swarm as anything in hive_metadata.swarms) @@ -175,5 +173,5 @@ if(length(hive_metadata.swarms) < comb_count) swarm = new swarm_type(hive.loc, src, hive_metadata) - if(!QDELETED(swarm) && istype(swarm) && hive_metadata.consume_reserves(5)) + if(!QDELETED(swarm) && istype(swarm) && hive_metadata.consume_reserves(SWARM_GROWTH_COST)) swarm.adjust_swarm_intensity(min((max_swarm_growth_intensity-swarm_intensity), rand(3,5))) diff --git a/mods/content/beekeeping/items.dm b/mods/content/beekeeping/items.dm index 1161ae82fd9..f520e5085ea 100644 --- a/mods/content/beekeeping/items.dm +++ b/mods/content/beekeeping/items.dm @@ -24,7 +24,7 @@ smoked = TRUE if(smoked) - var/turf/smoked_turf = get_turf(user) + var/turf/smoked_turf = get_turf(A) if(smoked_turf) playsound(smoked_turf, 'sound/effects/refill.ogg', 25, 1) user.visible_message(SPAN_NOTICE("\The [user] douses \the [A] in smoke from \the [src].")) From e9756389598606796c6d92e0f65c05d067b0ff2d Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Mon, 22 Sep 2025 21:36:05 +1000 Subject: [PATCH 13/79] Commenting out wasps. --- mods/content/beekeeping/hives/hive_extension.dm | 9 ++++++++- mods/content/beekeeping/hives/hive_structure.dm | 2 +- .../hives/insect_species/insects_pollinators.dm | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index 0917903fae5..c315dd59e2e 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -80,7 +80,14 @@ for(var/obj/effect/insect_swarm/swarm as anything in swarms) swarm.swarm_agitation = min(100, swarm.swarm_agitation + SWARM_AGITATION_PER_FRAME) -/datum/extension/insect_hive/proc/try_hand_harvest(mob/user) +/datum/extension/insect_hive/proc/try_hand_harvest(mob/user, obj/item/structure) + if(istype(structure) && !structure.storage) + var/obj/item/hive_frame/frame = locate() in structure + if(frame) + frame.dropInto(get_turf(structure)) + if(istype(user)) + user.put_in_hands(frame) + return TRUE return FALSE /datum/extension/insect_hive/proc/try_tool_harvest(mob/user, obj/item/tool) diff --git a/mods/content/beekeeping/hives/hive_structure.dm b/mods/content/beekeeping/hives/hive_structure.dm index 33eb99e67c8..855f9e42c7b 100644 --- a/mods/content/beekeeping/hives/hive_structure.dm +++ b/mods/content/beekeeping/hives/hive_structure.dm @@ -8,7 +8,7 @@ /obj/structure/attack_hand(mob/user) if(has_extension(src, /datum/extension/insect_hive)) var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) - if(hive.try_hand_harvest(user)) + if(hive.try_hand_harvest(user, src)) return TRUE return ..() diff --git a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm index 0f9b924e62f..83aeb86f846 100644 --- a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm +++ b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm @@ -14,6 +14,7 @@ produce_reagents = list(/decl/material/liquid/nutriment/honey = 1) produce_material = /decl/material/solid/organic/wax +/* /decl/insect_species/wasps name_singular = "wasp" name_plural = "wasps" @@ -24,3 +25,4 @@ sting_amount = 5 swarm_color = COLOR_BRONZE swarm_type = /obj/effect/insect_swarm/pollinator // tarantula hunter... +*/ \ No newline at end of file From 815c22139d712e284f63aeb7bd11054334e8f602 Mon Sep 17 00:00:00 2001 From: mistakenot4892 Date: Mon, 13 Oct 2025 10:32:08 +1100 Subject: [PATCH 14/79] Addressing some comments on beewrite PR. --- code/__defines/misc.dm | 1 + .../objects/items/devices/chameleonproj.dm | 5 +- code/game/objects/structures/flora/plant.dm | 5 +- .../modules/hydroponics/trays/tray_process.dm | 3 +- code/modules/mob/living/living.dm | 3 + code/modules/mob/mob_automove.dm | 1 - mods/content/beekeeping/hives/_hive.dm | 168 ------------------ .../beekeeping/hives/hive_extension.dm | 8 +- mods/content/beekeeping/hives/hive_swarm.dm | 73 ++++---- .../hives/insect_species/_insects.dm | 16 +- mods/content/beekeeping/icons/swarm.dmi | Bin 1016 -> 1124 bytes 11 files changed, 70 insertions(+), 213 deletions(-) delete mode 100644 mods/content/beekeeping/hives/_hive.dm diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 6a1de9936b7..9fc564c109c 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -407,3 +407,4 @@ // In theory, one pollen every 5 seconds (at time of writing) #define POLLEN_PER_SECOND 0.2 #define POLLEN_PRODUCTION_MULT (POLLEN_PER_SECOND * (SSplants.wait / 10)) +#define MAX_POLLEN_PER_FLOWER 10 diff --git a/code/game/objects/items/devices/chameleonproj.dm b/code/game/objects/items/devices/chameleonproj.dm index fc743829cee..83f05a1b7de 100644 --- a/code/game/objects/items/devices/chameleonproj.dm +++ b/code/game/objects/items/devices/chameleonproj.dm @@ -102,7 +102,7 @@ density = FALSE anchored = TRUE is_spawnable_type = FALSE - movement_handlers = list(/datum/movement_handler/delay/chameleon_projector) + movement_handlers = list(/datum/movement_handler/delay/chameleon_projector = list(2.5 SECONDS)) var/obj/item/chameleon/master = null /obj/effect/dummy/chameleon/Initialize(mapload, var/obj/item/chameleon/projector) @@ -152,9 +152,6 @@ if(!my_turf.get_supporting_platform() && !(locate(/obj/structure/lattice) in loc)) disrupted() -/datum/movement_handler/delay/chameleon_projector - delay = 2.5 SECONDS - /datum/movement_handler/delay/chameleon_projector/MayMove(mob/mover, is_external) return host.loc?.has_gravity() ? ..() : MOVEMENT_STOP diff --git a/code/game/objects/structures/flora/plant.dm b/code/game/objects/structures/flora/plant.dm index c56c1886479..278a60e8f01 100644 --- a/code/game/objects/structures/flora/plant.dm +++ b/code/game/objects/structures/flora/plant.dm @@ -7,7 +7,7 @@ var/dead = FALSE var/sampled = FALSE var/datum/seed/plant - var/harvestable = 0 + var/harvestable = 0 // Note that this is a counter, not a bool. var/pollen = 0 /obj/structure/flora/plant/large @@ -17,7 +17,7 @@ /obj/structure/flora/plant/Process() if(plant?.produces_pollen <= 0) return PROCESS_KILL - if(pollen < 10) + if(pollen < MAX_POLLEN_PER_FLOWER) pollen += plant.produces_pollen * POLLEN_PRODUCTION_MULT /* Notes for future work moving logic off hydrotrays onto plants themselves: @@ -164,6 +164,7 @@ icon_state = "flower5" is_spawnable_type = TRUE +// Only contains roundstart plants, this is meant to be a mapping helper. /obj/structure/flora/plant/random_flower/proc/get_flower_variants() var/static/list/flower_variants if(isnull(flower_variants)) diff --git a/code/modules/hydroponics/trays/tray_process.dm b/code/modules/hydroponics/trays/tray_process.dm index d6df7e1c6ca..2b835ea3eb6 100644 --- a/code/modules/hydroponics/trays/tray_process.dm +++ b/code/modules/hydroponics/trays/tray_process.dm @@ -65,8 +65,9 @@ mutate((rand(100) < 15) ? 2 : 1) mutation_level = 0 - if(pollen < 10) + if(pollen < MAX_POLLEN_PER_FLOWER) pollen += seed?.produces_pollen * POLLEN_PRODUCTION_MULT + to_world("\ref[src] has pollen [pollen] ([seed?.produces_pollen] * [POLLEN_PRODUCTION_MULT])") // Maintain tray nutrient and water levels. if(seed.get_trait(TRAIT_REQUIRES_NUTRIENTS) && seed.get_trait(TRAIT_NUTRIENT_CONSUMPTION) > 0 && nutrilevel > 0 && prob(25)) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 55e94083cb9..cc265798515 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2020,3 +2020,6 @@ default behaviour is: //Pixel projectiles need a client, so we need a way to pass who the last user was for view calcs /mob/living/proc/get_effective_gunner() return src + +/mob/living/proc/is_playing_dead() + return stat || current_posture?.prone || (status_flags & FAKEDEATH) diff --git a/code/modules/mob/mob_automove.dm b/code/modules/mob/mob_automove.dm index f552d4f7c1e..c74d72c44e9 100644 --- a/code/modules/mob/mob_automove.dm +++ b/code/modules/mob/mob_automove.dm @@ -25,7 +25,6 @@ /mob/failed_automove() ..() stop_automove() - _automove_target = null return FALSE /mob/start_automove(target, movement_type, datum/automove_metadata/metadata) diff --git a/mods/content/beekeeping/hives/_hive.dm b/mods/content/beekeeping/hives/_hive.dm deleted file mode 100644 index 990d148a16f..00000000000 --- a/mods/content/beekeeping/hives/_hive.dm +++ /dev/null @@ -1,168 +0,0 @@ -/obj/machinery/beehive - name = "apiary" - icon = 'mods/content/beekeeping/icons/beekeeping.dmi' - icon_state = "beehive-0" - desc = "A wooden box designed specifically to house our buzzling buddies. Far more efficient than traditional hives. Just insert a frame and a queen, close it up, and you're good to go!" - density = TRUE - anchored = TRUE - layer = BELOW_OBJ_LAYER - - var/closed = 0 - var/bee_count = 0 // Percent - var/smoked = 0 // Timer - var/honeycombs = 0 // Percent - var/frames = 0 - var/maxFrames = 5 - -/obj/machinery/beehive/Initialize() - . = ..() - update_icon() - -/obj/machinery/beehive/on_update_icon() - overlays.Cut() - icon_state = "beehive-[closed]" - if(closed) - overlays += "lid" - if(frames) - overlays += "empty[frames]" - if(honeycombs >= 100) - overlays += "full[round(honeycombs / 100)]" - if(!smoked) - switch(bee_count) - if(1 to 20) - overlays += "bees1" - if(21 to 40) - overlays += "bees2" - if(41 to 60) - overlays += "bees3" - if(61 to 80) - overlays += "bees4" - if(81 to 100) - overlays += "bees5" - -/obj/machinery/beehive/get_examine_strings(mob/user, distance, infix, suffix) - . = ..() - if(!closed) - . += "The lid is open." - -/obj/machinery/beehive/attackby(var/obj/item/used_item, var/mob/user) - if(IS_CROWBAR(used_item)) - closed = !closed - user.visible_message("\The [user] [closed ? "closes" : "opens"] \the [src].", "You [closed ? "close" : "open"] \the [src].") - update_icon() - return TRUE - else if(IS_WRENCH(used_item)) - anchored = !anchored - user.visible_message("\The [user] [anchored ? "wrenches" : "unwrenches"] \the [src].", "You [anchored ? "wrench" : "unwrench"] \the [src].") - return TRUE - else if(istype(used_item, /obj/item/bee_smoker)) - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before smoking the bees.") - return TRUE - user.visible_message("\The [user] smokes the bees in \the [src].", "You smoke the bees in \the [src].") - smoked = 30 - update_icon() - return TRUE - else if(istype(used_item, /obj/item/hive_frame/crafted)) - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before inserting \the [used_item].") - return TRUE - if(frames >= maxFrames) - to_chat(user, "There is no place for an another frame.") - return TRUE - var/obj/item/hive_frame/crafted/H = used_item - if(REAGENT_TOTAL_VOLUME(H.reagents)) - to_chat(user, "\The [used_item] is full with beeswax and honey, empty it in the extractor first.") - return TRUE - ++frames - user.visible_message("\The [user] loads \the [used_item] into \the [src].", "You load \the [used_item] into \the [src].") - update_icon() - qdel(used_item) - return TRUE - else if(istype(used_item, /obj/item/bee_pack)) - var/obj/item/bee_pack/B = used_item - if(B.full && bee_count) - to_chat(user, "\The [src] already has bees inside.") - return TRUE - if(!B.full && bee_count < 90) - to_chat(user, "\The [src] is not ready to split.") - return TRUE - if(!B.full && !smoked) - to_chat(user, "Smoke \the [src] first!") - return TRUE - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before moving the bees.") - return TRUE - if(B.full) - user.visible_message("\The [user] puts the queen and the bees from \the [used_item] into \the [src].", "You put the queen and the bees from \the [used_item] into \the [src].") - bee_count = 20 - B.empty() - else - user.visible_message("\The [user] puts bees and larvae from \the [src] into \the [used_item].", "You put bees and larvae from \the [src] into \the [used_item].") - bee_count /= 2 - B.fill() - update_icon() - return TRUE - else if(istype(used_item, /obj/item/scanner/plant)) - to_chat(user, "Scan result of \the [src]...") - to_chat(user, "Beehive is [bee_count ? "[round(bee_count)]% full" : "empty"].[bee_count > 90 ? " Colony is ready to split." : ""]") - if(frames) - to_chat(user, "[frames] frames installed, [round(honeycombs / 100)] filled.") - if(honeycombs < frames * 100) - to_chat(user, "Next frame is [round(honeycombs % 100)]% full.") - else - to_chat(user, "No frames installed.") - if(smoked) - to_chat(user, "The hive is smoked.") - return TRUE - else if(IS_SCREWDRIVER(used_item)) - if(bee_count) - to_chat(user, "You can't dismantle \the [src] with these bees inside.") - return TRUE - to_chat(user, "You start dismantling \the [src]...") - playsound(loc, 'sound/items/Screwdriver.ogg', 50, 1) - if(do_after(user, 30, src)) - user.visible_message("\The [user] dismantles \the [src].", "You dismantle \the [src].") - new /obj/item/beehive_assembly(loc) - qdel(src) - return TRUE - return FALSE // this should probably not be a machine, so don't do any component interactions - -/obj/machinery/beehive/physical_attack_hand(var/mob/user) - if(closed) - return FALSE - . = TRUE - if(honeycombs < 100) - to_chat(user, "There are no filled honeycombs.") - return - if(!smoked && bee_count) - to_chat(user, "The bees won't let you take the honeycombs out like this, smoke them first.") - return - user.visible_message("\The [user] starts taking the honeycombs out of \the [src].", "You start taking the honeycombs out of \the [src]...") - while(honeycombs >= 100 && do_after(user, 30, src)) - new /obj/item/hive_frame/crafted/filled(loc) - honeycombs -= 100 - --frames - update_icon() - if(honeycombs < 100) - to_chat(user, "You take all filled honeycombs out.") - -/obj/machinery/beehive/Process() - if(closed && !smoked && bee_count) - pollinate_flowers() - update_icon() - smoked = max(0, smoked - 1) - if(!smoked && bee_count) - bee_count = min(bee_count * 1.005, 100) - update_icon() - -/obj/machinery/beehive/proc/pollinate_flowers() - var/coef = bee_count / 100 - var/trays = 0 - for(var/obj/machinery/portable_atmospherics/hydroponics/H in view(7, src)) - if(H.seed && !H.dead) - H.plant_health += 0.05 * coef - if(H.pollen >= 1) - H.pollen-- - trays++ - honeycombs = min(honeycombs + 0.1 * coef * min(trays, 5), frames * 100) diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index c315dd59e2e..40c44df2505 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -125,7 +125,7 @@ return FALSE var/reserve = 0 for(var/obj/item/frame in holder) - reserve += frame.reagents?.total_volume + reserve += REAGENT_TOTAL_VOLUME(frame.reagents) if(reserve >= amt) return TRUE return FALSE @@ -141,9 +141,9 @@ amt -= raw_reserves raw_reserves = 0 for(var/obj/item/frame in holder) - if(!frame.reagents?.total_volume) + if(!REAGENT_TOTAL_VOLUME(frame.reagents)) continue - var/consume = min(amt, frame.reagents.total_volume) + var/consume = min(amt, REAGENT_TOTAL_VOLUME(frame.reagents)) frame.reagents.remove_any(consume) amt -= consume if(amt <= 0) @@ -181,7 +181,7 @@ var/list/holder_contents = hive.get_contained_external_atoms() for(var/obj/item/hive_frame/frame in holder_contents) - if(!frame.reagents || (frame.reagents.total_volume >= frame.reagents.maximum_volume)) + if(!frame.reagents || (REAGENT_TOTAL_VOLUME(frame.reagents) >= REAGENT_MAXIMUM_VOLUME(frame.reagents))) continue var/fill_cost = REAGENTS_FREE_SPACE(frame.reagents) if(has_material(FRAME_FILL_MATERIAL_COST) && has_reserves(fill_cost)) diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 544b7f33406..084ef4461d8 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -6,7 +6,7 @@ default_pixel_z = 8 layer = ABOVE_HUMAN_LAYER pass_flags = PASS_FLAG_TABLE - movement_handlers = list(/datum/movement_handler/delay/insect_swarm) + movement_handlers = list(/datum/movement_handler/delay/insect_swarm = list(1 SECOND)) /// Current movement target for automove (ie. hive, flowers or victim) VAR_PRIVATE/atom/move_target @@ -18,16 +18,11 @@ var/swarm_agitation = 0 /// Percentage value; if it drops to 0, the swarm will be destroyed. var/swarm_intensity = 1 - /// if more states are added to swarm.dmi, increase this - var/const/MAX_SWARM_STATE = 6 /// Cooldown timer for next tick. VAR_PRIVATE/next_work = 0 /// Time that smoke will wear off. var/smoked_until = 0 -/datum/movement_handler/delay/insect_swarm - delay = 1 SECOND - /datum/movement_handler/delay/insect_swarm/DoMove(direction, mob/mover, is_external) ..() step(host, direction) @@ -46,8 +41,7 @@ if(!istype(owner)) PRINT_STACK_TRACE("Insect swarm created with invalid hive: '[owner]'") return INITIALIZE_HINT_QDEL - color = insect_type.swarm_color - icon = insect_type.swarm_icon + update_transform() update_swarm() LAZYDISTINCTADD(owner.swarms, src) START_PROCESSING(SSobj, src) @@ -61,9 +55,29 @@ STOP_PROCESSING(SSobj, src) return ..() +// Resolves the current swarm amount to a coarser value used for icon state selection. +/obj/effect/insect_swarm/proc/get_swarm_state() + return ceil((swarm_intensity / insect_type.max_swarm_intensity) * insect_type.max_swarm_state) + +/obj/effect/insect_swarm/on_update_icon() + . = ..() + color = insect_type.swarm_color + icon = insect_type.swarm_icon + icon_state = num2text(get_swarm_state()) + if(is_smoked()) + icon_state = "[icon_state]_smoked" + +/obj/effect/insect_swarm/update_transform() + . = ..() + // Some icon variation via transform. + if(prob(75)) + var/matrix/swarm_transform = transform || matrix() + swarm_transform.Turn(pick(90, 180, 270)) + transform = swarm_transform + /obj/effect/insect_swarm/proc/update_swarm() - icon_state = num2text(ceil((swarm_intensity / insect_type.max_swarm_intensity) * MAX_SWARM_STATE)) - if(icon_state == "1") + update_icon() + if(get_swarm_state() == 1) SetName(insect_type.name_singular) desc = insect_type.insect_desc gender = NEUTER @@ -72,21 +86,13 @@ desc = insect_type.swarm_desc gender = PLURAL - // Some icon variation via transform. - if(prob(75)) - var/matrix/swarm_transform = matrix() - swarm_transform.Turn(pick(90, 180, 270)) - /obj/effect/insect_swarm/proc/is_agitated() return QDELETED(owner) || (swarm_agitation > 0 && !is_smoked()) /obj/effect/insect_swarm/proc/find_sting_target() for(var/mob/living/victim in view(7, src)) - if(!victim.simulated || victim.stat || victim.current_posture?.prone) - continue - if(victim.isSynthetic()) - continue - return victim + if(victim.simulated && !victim.is_playing_dead()) + return victim /obj/effect/insect_swarm/proc/merge(obj/effect/insect_swarm/other_swarm) @@ -189,23 +195,24 @@ break return FALSE +/obj/effect/insect_swarm/failed_automove() + ..() + stop_automove() + return FALSE + /obj/effect/insect_swarm/get_automove_target(datum/automove_metadata/metadata) return move_target /obj/effect/insect_swarm/stop_automove() - SHOULD_CALL_PARENT(FALSE) move_target = null - //. = ..() // TODO work out why they're not automoving - walk(src, 0) + . = ..() + +/obj/effect/insect_swarm/can_do_automated_move(variant_move_delay) + return !is_smoked() /obj/effect/insect_swarm/start_automove(target, movement_type, datum/automove_metadata/metadata) - SHOULD_CALL_PARENT(FALSE) move_target = target - //. = ..() // TODO work out why they're not automoving - if(move_target) - walk_to(src, move_target, 0, 7) - else - walk(src, 0) + . = ..() /obj/effect/insect_swarm/proc/handle_hive_behavior() @@ -272,6 +279,10 @@ merge(other_swarm) return +/obj/effect/insect_swarm/DoMove(direction, mob/mover, is_external) + . = ..() + to_world("swarm tried to move: [.]") + /obj/effect/insect_swarm/pollinator var/pollen = 0 @@ -349,10 +360,10 @@ else start_automove(owner.holder) -// TODO: update icon (twitching on ground?) -// TODO: lower agitation /obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) smoked_until = max(smoked_until, world.time + smoke_time) + swarm_agitation = round(swarm_agitation * 0.75) + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon), TRUE), smoke_time, (TIMER_UNIQUE|TIMER_OVERRIDE)) /obj/effect/insect_swarm/proc/is_smoked() return world.time < smoked_until \ No newline at end of file diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm index 37e4481748c..b6902a51a0a 100644 --- a/mods/content/beekeeping/hives/insect_species/_insects.dm +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -22,6 +22,7 @@ var/swarm_type = /obj/effect/insect_swarm var/max_swarm_growth_intensity = 50 var/max_swarm_intensity = 100 + var/max_swarm_state = 6 // Venom delivered by swarms whens stinging a victim. var/sting_reagent @@ -72,6 +73,17 @@ if(!istype(produce_material, /decl/material)) . += "non-material product material type: '[produce_material]'" + if(!swarm_icon) + . += "null swarm icon" + else + for(var/i = 0 to max_swarm_state) + var/check_state = num2text(i) + if(!check_state_in_icon(check_state, swarm_icon)) + . += "missing active icon_state '[check_state]'" + check_state = "[check_state]_smoked" + if(!check_state_in_icon(check_state, swarm_icon)) + . += "missing smoked icon_state '[check_state]'" + /decl/insect_species/proc/fill_hive_frame(obj/item/frame) if(!istype(frame) || QDELETED(frame)) @@ -81,7 +93,7 @@ if(frame_space <= 0) return FALSE - if(frame.reagents?.maximum_volume && length(produce_reagents)) + if(REAGENT_MAXIMUM_VOLUME(frame.reagents) && length(produce_reagents)) var/reagent_split = max(1, floor(min(REAGENTS_FREE_SPACE(frame.reagents), 20) / length(produce_reagents))) for(var/reagent in produce_reagents) frame.reagents.add_reagent(reagent, max(1, (reagent_split * produce_reagents[reagent])), defer_update = TRUE) @@ -168,7 +180,7 @@ if(!swarm) var/comb_count = 0 for(var/obj/item/hive_frame/frame in hive) - if(frame.reagents && frame.reagents.total_volume >= frame.reagents.maximum_volume) + if(REAGENT_TOTAL_VOLUME(frame.reagents) >= REAGENT_MAXIMUM_VOLUME(frame.reagents)) comb_count++ if(length(hive_metadata.swarms) < comb_count) swarm = new swarm_type(hive.loc, src, hive_metadata) diff --git a/mods/content/beekeeping/icons/swarm.dmi b/mods/content/beekeeping/icons/swarm.dmi index 1d8ea6f1b1ae8e237aaaefaf5352ad590422c65c..e55d936c1f15331e32068155b7bb6ea87076b676 100644 GIT binary patch delta 940 zcmV;d15^C?2jmEl7Yg760{{R3=mU4sks&Jqw2@7Wf04pFzBo5OJ2i!HuoJ7$h**Wj z#40o)R-q}e3e5;8R90~Ha{;Gc0N-xA%A#`K)&KwlLrFwIRCt{2o9lstAPj&FE|7DA zbOZOF?m(bCd=nJg=KEnqGzlR<(7Ub^1VIqK2W)MC(3+1A0cvf4AP9mW2*Ni4tqu4* zRBHo1e-LVIfN&O>8Lo!wVX9$-AP9mW2)_WeHh|BQb5m+kCZLO19%RDv^0TxN?5r z_O!g=&TDvC?Wx-@GXk9KTL%Z+o2ct91a)T%P5S`2W8?U;4Bb$-b+l7I*h6?rvG>3N z=k|lxnI1eHHOw%Z55%j`T1H$lSEV;qLwRLL6{T*0lQQh6;@IEu2wsufZmyzf+m3V? ze=$ty%yKclcoGie1;LBN)m6ac#e+d}dWd8J5Y-ds zrV_VIrkV0$|AT!#zk)uwvp@ap1_p!8f8=6;>MfQ!mr=$o0>0vDby7}>@mH3!>xlr?HxnL>UzJvCH-LYh>zk7Nb*Xdgw3IMn`K4e`h?P z>A@`ObPt&pgVk;>^oa`4TQB3~LcKJJz<6p-MZNr8$wSD<{*(=J6}&nI7aC2r9;3;< z%0KW0A=5lg{#+Mc+0f^phhU%Nsia*+u?zc54;e2r2ma!PzlBPmt{RcevuIe_eirU& z=e^(O>Rpkzff5Td&`Qm`w|slqf9v5)=XzvJnGJszmQ32O)POih_c;~BY9=+057-Ks zS-V}1t(7Ye`x0{{R3@2_&?ks&JqmXS@2f4P(uT>V_Y5d;9b!D;2Pce3gL z00Q+%L_t(|ob8+2xq~1KfDJB?;{7pj@s%?NE2!bF8!Z!l74R{}_wgE4MY8xP&MP`Pp;d+=#7$FFPAPB-QK(!6v z^W@r;e`*`B!}GVV=STOJ-@0D^gW8jz+6FACF{zwSM%rqWJP-sy5N_tlCHaL6`;XA& zM({Y$1>wu6xCuN^KH|nz2_0oB*!S1LKRRb0q(+XkWySGA^!eWu-OFizhyxuopNUI^ zAK*t;R`q#-$y&*F+(tPvuLnPzA9y^z-f-tNfAm&+?Dory04Mv_Ul*TL^IP2=$lXj#FaL z_&oA*R_{;U0_kiYqFF#IpvVJg_X&AQ(eE}U^#eo!LDRVUJBmBqBhEFA)f5%Vps)>H zuD4$UQ=hXKok}x8f4((1T3a~zfMx`fsM8~4dJI;(xzR={KyST_moxR!C<4Q&e>oP_ zPftZJA*1?JHprFt>fl{yG}(3xCifzL;PXPpd7P$mebFbW&q1eP8`Y_#T}7b_+e8l; zFEbN=^~T>yBT!R}z~)6XtZhFt_q5B=Z&UT|lDL5q3Nz4$mgQ(o{bjF*3!R@MW2|iW zyRc}|?qdVOAT5kqNKNAdvO^}4C9wqkC#|(-GADh!$QIO4k$)9N6-H98v2*|c002ov JPDHLkV1hfkhZq0= From 1d403a0c79848cbb6a19621608e350d64c25082c Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 4 May 2026 15:27:32 -0400 Subject: [PATCH 15/79] Make alists display properly in VV --- code/modules/admin/view_variables/view_variables.dm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm index 19efad19a39..4d248d8255d 100644 --- a/code/modules/admin/view_variables/view_variables.dm +++ b/code/modules/admin/view_variables/view_variables.dm @@ -175,6 +175,14 @@ var/global/list/view_variables_no_assoc = list("verbs", "contents","screen","ima else if(istype(value, /client)) var/client/C = value vtext = "\ref[C] - [C] ([C.type])" + else if(istype(value, /alist)) + var/alist/AL = value + vtext = "/alist ([AL.len])" + if(!(varname in view_variables_dont_expand) && AL.len > 0 && AL.len < 100) + extra += "
    " + for (var/key, entry in AL) + extra += "
  • [make_view_variables_value(key)] -> [make_view_variables_value(entry)]
  • " + extra += "
" else if(islist(value)) var/list/L = value vtext = "/list ([L.len])" From dbf67deb82bec1517e4aedd3d4848b445a3dbef4 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 4 May 2026 15:27:47 -0400 Subject: [PATCH 16/79] Fix structure plants not generating pollen --- code/game/objects/structures/flora/plant.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/objects/structures/flora/plant.dm b/code/game/objects/structures/flora/plant.dm index 278a60e8f01..f4dfac197bc 100644 --- a/code/game/objects/structures/flora/plant.dm +++ b/code/game/objects/structures/flora/plant.dm @@ -14,7 +14,7 @@ opacity = TRUE density = TRUE -/obj/structure/flora/plant/Process() +/obj/structure/flora/plant/process_plants() if(plant?.produces_pollen <= 0) return PROCESS_KILL if(pollen < MAX_POLLEN_PER_FLOWER) From fcbdd298273a9be160f502c81b535a7473c6a1ae Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 4 May 2026 15:28:06 -0400 Subject: [PATCH 17/79] Fix mob stop_effect movement handler typepath --- code/datums/movement/mob.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/datums/movement/mob.dm b/code/datums/movement/mob.dm index 996c6215085..ee52cd4ff8c 100644 --- a/code/datums/movement/mob.dm +++ b/code/datums/movement/mob.dm @@ -120,7 +120,7 @@ next_move += max(0, delay) // Stop effect -/datum/movement_handler/mob/DoMove(direction, mob/mover, is_external) +/datum/movement_handler/mob/stop_effect/DoMove(direction, mob/mover, is_external) if(MayMove(mover, is_external) == MOVEMENT_STOP) return MOVEMENT_HANDLED From c94e3d1362e8c727c424a4d0db527bd4b6d99724 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 4 May 2026 15:28:30 -0400 Subject: [PATCH 18/79] Improve and fix automove functionality --- code/datums/movement/automove.dm | 2 +- code/datums/movement/automove_controller.dm | 97 ++++++++++++++------- code/game/atoms_movable.dm | 11 ++- code/modules/mob/mob.dm | 11 ++- code/modules/mob/mob_automove.dm | 2 +- 5 files changed, 84 insertions(+), 39 deletions(-) diff --git a/code/datums/movement/automove.dm b/code/datums/movement/automove.dm index a07d65d0324..6c3533f758f 100644 --- a/code/datums/movement/automove.dm +++ b/code/datums/movement/automove.dm @@ -37,4 +37,4 @@ /// Generalized entrypoint for checking CanMove and such on /mob. /atom/movable/proc/can_do_automated_move(variant_move_delay) - return FALSE + return MayMove(src) diff --git a/code/datums/movement/automove_controller.dm b/code/datums/movement/automove_controller.dm index b413d999849..e593f4315c9 100644 --- a/code/datums/movement/automove_controller.dm +++ b/code/datums/movement/automove_controller.dm @@ -1,11 +1,32 @@ /// Implements automove logic; can be overridden on mob procs if you want to vary the logic from the below. /decl/automove_controller - var/completion_signal = FALSE // Set to TRUE if you want movement to stop processing when the atom reaches its target. - var/failure_signal = FALSE // Set to TRUE if you want movement to stop processing when the atom fails to move. + // these could be proper bools but i assumed they were set up this way for a reason, like supporting other return values in the future + var/completion_signal = FALSE // Set to PROCESS_KILL if you want movement to stop processing when the atom reaches its target. + var/failure_signal = FALSE // Set to PROCESS_KILL if you want movement to stop processing when the atom fails to move. var/try_avoid_obstacles = TRUE // Will try to move 90 degrees around an obstacle. -/decl/automove_controller/proc/handle_mover(atom/movable/mover, datum/automove_metadata/metadata) +/decl/automove_controller/proc/check_move_completion(atom/movable/mover, datum/automove_metadata/metadata) + // Null target means abandon pathing, regardless of return signals. + var/atom/target = mover.get_automove_target(metadata) + if(!istype(target)) + return TRUE + // Cease automovement if we're already at the target. + var/acceptable_move_dist = isnull(metadata?.acceptable_distance) ? mover.get_acceptable_automove_distance_from_target() : metadata.acceptable_distance + var/current_distance = get_dist(mover, target) + if(metadata?.avoid_target) + return current_distance >= acceptable_move_dist + else + if(get_turf(mover) == get_turf(target)) + return TRUE + if(ismovable(target) && (target.density && mover.density) && mover.Adjacent(target)) + return TRUE + if(current_distance <= acceptable_move_dist) + return TRUE + return FALSE + +/// Return PROCESS_KILL to terminate automovement. Will return completion_signal when the atom reaches its target and failure_signal if it fails to move. +/decl/automove_controller/proc/handle_mover(atom/movable/mover, datum/automove_metadata/metadata) // Cease automovement if we got an invalid mover.. if(!istype(mover)) return PROCESS_KILL @@ -19,48 +40,60 @@ if(ismob(mover)) var/mob/mover_mob = mover if(mover_mob.moving) - return TRUE + return - // Cease automovement if we're already at the target. - var/avoid_target = metadata?.avoid_target - if(!avoid_target && (get_turf(mover) == get_turf(target) || (ismovable(target) && mover.Adjacent(target)))) + if(check_move_completion(mover, metadata)) mover.finished_automove() return completion_signal - // Cease movement if we're close enough to the target. - var/acceptable_move_dist = isnull(metadata?.acceptable_distance) ? mover.get_acceptable_automove_distance_from_target() : metadata.acceptable_distance - if(avoid_target ? (get_dist(mover, target) >= acceptable_move_dist) : (get_dist(mover, target) <= acceptable_move_dist)) - mover.finished_automove() - return completion_signal + // Skip automovement if we aren't allowed to move yet. + // This is for checks that are expected to fail sometimes (movedelay, incapacitation, etc), so we don't send the failure signal when this happens. + if(!mover.can_do_automated_move(metadata?.move_delay)) + return - // Cease automovement if we failed to move a turf. - if(mover.can_do_automated_move(metadata?.move_delay)) - if(avoid_target) - target = get_edge_target_turf(target, get_dir(target, mover)) - - // Note for future coders: SelfMove() only confirms if a handler handled the move, not if the atom moved. - var/old_loc = mover.loc + var/avoid_target = metadata?.avoid_target + if(avoid_target) + target = get_edge_target_turf(target, get_dir(target, mover)) - // Try to move directly. - var/target_dir = get_dir(mover, target) - if(!target_dir) - if(avoid_target) - target_dir = pick(global.cardinal) - else - return TRUE // no idea how we would get into this position + // Note for future coders: SelfMove() only confirms if a handler handled the move, not if the atom moved. + var/old_loc = mover.loc - if(mover.SelfMove(target_dir) && (old_loc != mover.loc)) - mover.handle_post_automoved(old_loc) - return (mover.get_automove_target() == mover.loc) // We may have transitioned to the next step in a path. + // Try to move directly. + var/target_dir = get_dir(mover, target) + if(!target_dir) + if(avoid_target) + target_dir = pick(global.cardinal) + else + return // no idea how we would get into this position + var/old_next_move_time = mover.get_next_move_time() // to reset to later, so obstacle bumps don't let us ignore move delay + if(mover.SelfMove(target_dir) && (old_loc != mover.loc)) + mover.handle_post_automoved(old_loc) + // check if we're done, and if so, return the completion signal + if(check_move_completion(mover, metadata)) + mover.finished_automove() + return completion_signal + return // we moved, so we didn't fail, but we also aren't finished yet + else if(try_avoid_obstacles) // Try to move around any obstacle. var/static/list/_alt_dir_rot = list(45, -45) for(var/alt_dir in shuffle(_alt_dir_rot)) - mover.reset_movement_delay() + mover.set_next_move_time(old_next_move_time) if(mover.SelfMove(turn(target_dir, alt_dir)) && (old_loc != mover.loc)) - return TRUE + mover.handle_post_automoved(old_loc) + // check if we're done, and if so, return the completion signal + if(check_move_completion(mover, metadata)) + mover.finished_automove() + return completion_signal + return // see above; we succeeded on the retry but aren't done moving mover.failed_automove() + return failure_signal + +/decl/automove_controller/stop_on_completion + completion_signal = PROCESS_KILL - return failure_signal +/decl/automove_controller/stop_on_fail_or_completion + completion_signal = PROCESS_KILL + failure_signal = PROCESS_KILL \ No newline at end of file diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 628b5880042..47c5406dd73 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -583,9 +583,16 @@ throwing = null /atom/movable/proc/reset_movement_delay() + set_next_move_time(world.time) + +/atom/movable/proc/get_next_move_time() + var/datum/movement_handler/delay/delay = locate() in movement_handlers + return delay?.next_move + +/atom/movable/proc/set_next_move_time(new_time) var/datum/movement_handler/delay/delay = locate() in movement_handlers - if(istype(delay)) - delay.next_move = world.time + if(delay) + delay.next_move = new_time /atom/movable/get_affecting_weather() var/turf/my_turf = get_turf(src) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 612a4e888b4..9ddb5ed172a 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1463,10 +1463,15 @@ var/global/const/ACTION_DANGER_ALL = 2 var/decl/butchery_data/butchery_decl = GET_DECL(butchery_data) . = butchery_decl?.meat_name || name -/mob/reset_movement_delay() +// we change the base type of our delay handler... +/mob/get_next_move_time() var/datum/movement_handler/mob/delay/delay = locate() in movement_handlers - if(istype(delay)) - delay.next_move = world.time + return delay?.next_move + +/mob/set_next_move_time(new_time) + var/datum/movement_handler/mob/delay/delay = locate() in movement_handlers + if(delay) + delay.next_move = new_time /mob/proc/do_attack_windup_checking(atom/target) return TRUE diff --git a/code/modules/mob/mob_automove.dm b/code/modules/mob/mob_automove.dm index c74d72c44e9..543bf5dfd30 100644 --- a/code/modules/mob/mob_automove.dm +++ b/code/modules/mob/mob_automove.dm @@ -62,4 +62,4 @@ // We do some early checking here to avoid doing the same checks repeatedly by calling SelfMove(). /mob/can_do_automated_move(variant_move_delay) - . = MayMove() && !incapacitated() && (!istype(ai) || ai.can_do_automated_move()) + . = ..() && !incapacitated() && (!istype(ai) || ai.can_do_automated_move()) From 04cc751cc37dfdd5ef33aadb518ee28b64b32dcd Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 4 May 2026 15:28:49 -0400 Subject: [PATCH 19/79] Fix bee automove --- mods/content/beekeeping/hives/hive_swarm.dm | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 084ef4461d8..3e00fd4241b 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -195,9 +195,9 @@ break return FALSE -/obj/effect/insect_swarm/failed_automove() +/obj/effect/insect_swarm/finished_automove() ..() - stop_automove() + next_work = world.time // we'be a busy bee, check to for new work once you reach your destination return FALSE /obj/effect/insect_swarm/get_automove_target(datum/automove_metadata/metadata) @@ -208,12 +208,15 @@ . = ..() /obj/effect/insect_swarm/can_do_automated_move(variant_move_delay) - return !is_smoked() + return ..() && !is_smoked() /obj/effect/insect_swarm/start_automove(target, movement_type, datum/automove_metadata/metadata) move_target = target . = ..() +/obj/effect/insect_swarm/get_default_automove_controller_type() + return /decl/automove_controller/stop_on_fail_or_completion + /obj/effect/insect_swarm/proc/handle_hive_behavior() var/atom/movable/hive = owner?.holder @@ -230,7 +233,7 @@ stop_automove() return if(!hive_has_swarm() && loc != hive.loc) - start_automove(owner.holder) + start_automove(hive) return do_work() @@ -279,10 +282,6 @@ merge(other_swarm) return -/obj/effect/insect_swarm/DoMove(direction, mob/mover, is_external) - . = ..() - to_world("swarm tried to move: [.]") - /obj/effect/insect_swarm/pollinator var/pollen = 0 @@ -292,26 +291,28 @@ if(world.time < next_work) return + var/atom/movable/hive = owner?.holder + + // Move to move target (hive or flowers) + if(move_target) + if(!(move_target in view(src, 7))) // no longer able to see our move target + stop_automove() + // don't bail early if we just stopped automove, that would introduce stutter as it'd take one tick to decide what to do next + else + // let us automove, don't restart it + return + // Unload pollen into hive. if(pollen) - if(loc == get_turf(owner.holder)) + if(loc == get_turf(hive)) owner.add_reserves(pollen) pollen = 0 next_work = world.time + 5 SECONDS stop_automove() else - start_automove(owner.holder) + start_automove(hive) return - // Move to flowers. - if(move_target) - if(get_turf(move_target) == loc || !(move_target in view(src, 7))) - move_target = null - stop_automove() - else - start_automove(move_target) - return - // Harvest from flowers in our loc. for(var/obj/machinery/portable_atmospherics/hydroponics/flower in loc) if(!flower.pollen) @@ -358,7 +359,7 @@ if(closest_target) start_automove(closest_target) else - start_automove(owner.holder) + start_automove(hive) /obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) smoked_until = max(smoked_until, world.time + smoke_time) From 510eed2062ab497c4805359be50472f10c224ed0 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Wed, 13 May 2026 21:21:27 +1000 Subject: [PATCH 20/79] Tweaks to beewrite after testing. --- code/controllers/subsystems/initialization/customitems.dm | 2 ++ code/controllers/subsystems/jobs.dm | 2 +- code/modules/hydroponics/trays/tray_process.dm | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/code/controllers/subsystems/initialization/customitems.dm b/code/controllers/subsystems/initialization/customitems.dm index d571851c618..d3bdc56698b 100644 --- a/code/controllers/subsystems/initialization/customitems.dm +++ b/code/controllers/subsystems/initialization/customitems.dm @@ -49,6 +49,8 @@ SUBSYSTEM_DEF(customitems) //gets the relevant list for the key from the listlist if it exists, check to make sure they are meant to have it and then calls the giving function /datum/controller/subsystem/customitems/proc/equip_custom_items(mob/living/human/M) + if(!istype(M) || !M.ckey) + return var/list/key_list = custom_items_by_ckey[M.ckey] if(!length(key_list)) return diff --git a/code/controllers/subsystems/jobs.dm b/code/controllers/subsystems/jobs.dm index 886d58c7a9c..63bc4995bee 100644 --- a/code/controllers/subsystems/jobs.dm +++ b/code/controllers/subsystems/jobs.dm @@ -565,7 +565,7 @@ SUBSYSTEM_DEF(jobs) job.post_equip_job_title(H, alt_title || job_title) - H.client.show_location_blurb(30) + H.client?.show_location_blurb(30) return H diff --git a/code/modules/hydroponics/trays/tray_process.dm b/code/modules/hydroponics/trays/tray_process.dm index 2b835ea3eb6..8491df96a2c 100644 --- a/code/modules/hydroponics/trays/tray_process.dm +++ b/code/modules/hydroponics/trays/tray_process.dm @@ -67,7 +67,6 @@ if(pollen < MAX_POLLEN_PER_FLOWER) pollen += seed?.produces_pollen * POLLEN_PRODUCTION_MULT - to_world("\ref[src] has pollen [pollen] ([seed?.produces_pollen] * [POLLEN_PRODUCTION_MULT])") // Maintain tray nutrient and water levels. if(seed.get_trait(TRAIT_REQUIRES_NUTRIENTS) && seed.get_trait(TRAIT_NUTRIENT_CONSUMPTION) > 0 && nutrilevel > 0 && prob(25)) From 00ff4700bc2efcfd7bf478e469149ec09cc0dab9 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Wed, 13 May 2026 21:33:43 +1000 Subject: [PATCH 21/79] Tweaking bee pain and venom amount for stinging. --- code/modules/mob/living/human/human.dm | 8 +++---- code/modules/mob/living/living.dm | 2 +- code/modules/mob/new_player/new_player.dm | 5 +++- .../beekeeping/hives/hive_extension.dm | 2 +- mods/content/beekeeping/hives/hive_swarm.dm | 3 ++- .../hives/insect_species/_insects.dm | 23 ++++++++++++++----- .../insect_species/insects_pollinators.dm | 4 ++-- mods/content/beekeeping/items.dm | 2 +- mods/content/beekeeping/materials.dm | 6 +++-- mods/content/beekeeping/trading.dm | 2 +- 10 files changed, 37 insertions(+), 20 deletions(-) diff --git a/code/modules/mob/living/human/human.dm b/code/modules/mob/living/human/human.dm index 4e60c2cc91d..5369b167113 100644 --- a/code/modules/mob/living/human/human.dm +++ b/code/modules/mob/living/human/human.dm @@ -688,11 +688,11 @@ if(!affecting) to_chat(user, SPAN_WARNING("\The [src] is missing that limb.")) - return 0 + return FALSE if(BP_IS_PROSTHETIC(affecting)) to_chat(user, SPAN_WARNING("That limb is prosthetic.")) - return 0 + return FALSE . = CAN_INJECT for(var/slot in list(slot_head_str, slot_wear_mask_str, slot_wear_suit_str, slot_w_uniform_str, slot_gloves_str, slot_shoes_str)) @@ -701,8 +701,8 @@ if(istype(C, /obj/item/clothing/suit/space)) . = INJECTION_PORT //it was going to block us, but it's a space suit so it doesn't because it has some kind of port else - to_chat(user, "There is no exposed flesh or thin material on [src]'s [affecting.name] to inject into.") - return 0 + to_chat(user, SPAN_WARNING("There is no exposed flesh or thin material on [src]'s [affecting.name] to inject into.")) + return FALSE /mob/living/human/print_flavor_text(var/shrink = 1) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index cc265798515..14727789f10 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -271,7 +271,7 @@ default behaviour is: gear_tree |= storage_contents /mob/living/proc/can_inject(var/mob/user, var/target_zone) - return 1 + return TRUE /mob/living/proc/get_organ_target() var/mob/shooter = src diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 0b7f6c091e9..56fdd28e45b 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -228,7 +228,10 @@ INITIALIZE_IMMEDIATE(/mob/new_player) if(!character) return 0 - character = SSjobs.equip_job_title(character, job.title, 1) //equips the human + character = SSjobs.equip_job_title(character, job.title, 1) //equips the human + if(!character) + return 0 + SScustomitems.equip_custom_items(character) if(job.do_spawn_special(character, src, TRUE)) //This replaces the AI spawn logic with a proc stub. Refer to silicon.dm for the spawn logic. diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm index 40c44df2505..5e91d1c1c23 100644 --- a/mods/content/beekeeping/hives/hive_extension.dm +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -210,7 +210,7 @@ for(var/obj/effect/insect_swarm/swarm as anything in swarms) . += swarm.swarm_intensity -/datum/extension/insect_hive/proc/smoked_by(mob/user, atom/source, smoke_time = 10 SECONDS) +/datum/extension/insect_hive/proc/smoked_by(mob/user, atom/source, smoke_time = 1 MINUTE) smoked_until = max(smoked_until, world.time + smoke_time) // this is a little weird due to telekinetic bee smoking but so it goes for(var/obj/effect/insect_swarm/swarm as anything in swarms) diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm index 3e00fd4241b..c05a7d9972d 100644 --- a/mods/content/beekeeping/hives/hive_swarm.dm +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -361,9 +361,10 @@ else start_automove(hive) -/obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) +/obj/effect/insect_swarm/proc/was_smoked(smoke_time = 1 MINUTE) smoked_until = max(smoked_until, world.time + smoke_time) swarm_agitation = round(swarm_agitation * 0.75) + update_icon() addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon), TRUE), smoke_time, (TIMER_UNIQUE|TIMER_OVERRIDE)) /obj/effect/insect_swarm/proc/is_smoked() diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm index b6902a51a0a..d05460b9a5e 100644 --- a/mods/content/beekeeping/hives/insect_species/_insects.dm +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -27,6 +27,8 @@ // Venom delivered by swarms whens stinging a victim. var/sting_reagent var/sting_amount + var/per_sting_reagents = 0.1 + var/per_sting_pain = 1 /decl/insect_species/Initialize() if(produce_material) @@ -110,7 +112,7 @@ if(!swarm.is_agitated() && !prob(max(1, round(swarm.swarm_intensity/4)))) return FALSE var/base_sting_chance = (sting_amount * clamp(round(swarm.swarm_intensity/10), 1, 10)) - var/sting_mult = swarm.is_agitated() ? max(base_sting_chance, 65) : base_sting_chance + var/sting_mult = swarm.is_agitated() ? max(base_sting_chance, 15) : base_sting_chance for(var/mob/living/victim in loc) if(!victim.simulated || victim.stat || victim.current_posture?.prone) continue @@ -118,11 +120,20 @@ var/obj/item/organ/external/affecting = victim.get_organ(pick(global.all_limb_tags)) if(!affecting || BP_IS_PROSTHETIC(affecting) || BP_IS_CRYSTAL(affecting)) continue - if(injected_reagents && victim.can_inject(victim, affecting.organ_tag)) - to_chat(victim, SPAN_DANGER("\A [swarm] stings you [sting_mult <= sting_amount * 2 ? "" : "multiple times"] on your [affecting.name]!")) - injected_reagents.add_reagent(sting_reagent, sting_mult) - affecting.add_pain(sting_mult) - . = TRUE + if(!injected_reagents || !victim.can_inject(null, affecting.organ_tag)) + continue + + to_chat(victim, SPAN_DANGER("\A [swarm] stings you [sting_mult <= sting_amount * 2 ? "" : "multiple times"] on your [affecting.name]!")) + + var/sting_venom = (per_sting_reagents * sting_mult) - REAGENT_VOLUME(injected_reagents, sting_reagent) + if(sting_venom > 0) + injected_reagents.add_reagent(sting_reagent, sting_venom) + + var/sting_pain = (per_sting_pain * sting_mult) - victim.getHalLoss() + if(sting_pain > 0) + affecting.add_pain(sting_pain) + + . = TRUE /decl/insect_species/proc/can_spawn_in_flora(var/obj/structure/flora) diff --git a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm index 83aeb86f846..460ca2ef16b 100644 --- a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm +++ b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm @@ -10,7 +10,7 @@ swarm_color = COLOR_GOLD swarm_type = /obj/effect/insect_swarm/pollinator sting_reagent = /decl/material/liquid/bee_venom - sting_amount = 1 + sting_amount = 0.2 produce_reagents = list(/decl/material/liquid/nutriment/honey = 1) produce_material = /decl/material/solid/organic/wax @@ -22,7 +22,7 @@ swarm_desc = "A swarm of humming wasps." insect_desc = "A solitary wasp." sting_reagent = /decl/material/liquid/cyanide - sting_amount = 5 + sting_amount = 1 swarm_color = COLOR_BRONZE swarm_type = /obj/effect/insect_swarm/pollinator // tarantula hunter... */ \ No newline at end of file diff --git a/mods/content/beekeeping/items.dm b/mods/content/beekeeping/items.dm index f520e5085ea..0c7774e6f1f 100644 --- a/mods/content/beekeeping/items.dm +++ b/mods/content/beekeeping/items.dm @@ -20,7 +20,7 @@ if(!smoked && isturf(A)) for(var/obj/effect/insect_swarm/swarm in A) - swarm.was_smoked() + swarm.was_smoked(smoke_time = 1 MINUTE) smoked = TRUE if(smoked) diff --git a/mods/content/beekeeping/materials.dm b/mods/content/beekeeping/materials.dm index 496d5f9b394..d5fab8c75d2 100644 --- a/mods/content/beekeeping/materials.dm +++ b/mods/content/beekeeping/materials.dm @@ -13,8 +13,10 @@ metabolism = REM * 0.25 exoplanet_rarity_plant = MAT_RARITY_UNCOMMON exoplanet_rarity_gas = MAT_RARITY_EXOTIC + var/pain_mult = 10 + var/pain_threshold = 100 /decl/material/liquid/bee_venom/affect_blood(mob/living/M, removed, datum/reagents/holder) . = ..() - if(istype(M)) - M.adjustHalLoss(max(1, ceil(removed * 10))) + if(istype(M) && M.getHalLoss() < pain_threshold) + M.adjustHalLoss(max(1, ceil(removed * pain_mult))) diff --git a/mods/content/beekeeping/trading.dm b/mods/content/beekeeping/trading.dm index c63d01f412c..5e8a8dc18b1 100644 --- a/mods/content/beekeeping/trading.dm +++ b/mods/content/beekeeping/trading.dm @@ -1,6 +1,6 @@ /datum/trader/trading_beacon/manufacturing/New() LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/smoker, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/smoker, TRADER_THIS_TYPE) LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) LAZYSET(possible_trading_items, /obj/item/stack/material/plank/mapped/wood/ten, TRADER_THIS_TYPE) ..() From e8b3c908b9eb26eb8f0c6dfe1f163afc0a15c06c Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Fri, 15 May 2026 14:30:35 +1000 Subject: [PATCH 22/79] Adding a subsystem to handle clickdragging as mouse events are somewhat unreliable for fine-grained stuff. Adding cursor updates to mousedrag procs in preparation for melee attack PR. --- code/_onclick/MouseDrag.dm | 39 -------- code/_onclick/mouse_drag.dm | 91 +++++++++++++++++++ code/controllers/subsystems/clickdrag.dm | 54 +++++++++++ code/game/objects/items/__item.dm | 24 ++++- code/modules/client/client_procs.dm | 19 ++-- .../clothing/spacesuits/rig/modules/combat.dm | 9 ++ code/modules/mechs/_mech.dm | 31 +++++++ code/modules/mechs/equipment/_equipment.dm | 9 -- .../mechs/equipment/combat_projectile.dm | 53 ----------- .../modules/mechs/equipment/mounted_system.dm | 9 ++ code/modules/mechs/mech_interaction.dm | 46 ---------- code/modules/mob/living/inventory.dm | 18 ++-- code/modules/mob/mob.dm | 4 + code/modules/projectiles/{gun.dm => _gun.dm} | 69 +------------- code/modules/projectiles/autofire.dm | 37 ++++++++ .../projectiles/guns/energy/special.dm | 2 + .../projectiles/guns/launcher/bows/_bow.dm | 45 --------- .../guns/launcher/bows/bow_drawing.dm | 57 ++++++++++++ .../guns/launcher/bows/bow_interaction.dm | 5 +- .../projectiles/guns/launcher/foam_gun.dm | 2 +- .../projectiles/guns/projectile/automatic.dm | 10 +- mods/content/sealant_gun/sealant_gun.dm | 1 + nebula.dme | 6 +- 23 files changed, 355 insertions(+), 285 deletions(-) delete mode 100644 code/_onclick/MouseDrag.dm create mode 100644 code/_onclick/mouse_drag.dm create mode 100644 code/controllers/subsystems/clickdrag.dm rename code/modules/projectiles/{gun.dm => _gun.dm} (92%) create mode 100644 code/modules/projectiles/autofire.dm diff --git a/code/_onclick/MouseDrag.dm b/code/_onclick/MouseDrag.dm deleted file mode 100644 index 6f7e0ee7c3e..00000000000 --- a/code/_onclick/MouseDrag.dm +++ /dev/null @@ -1,39 +0,0 @@ -//If we intercept it return true else return false -/atom/proc/RelayMouseDrag(atom/src_object, atom/over_object, src_location, over_location, src_control, over_control, params, mob/user) - return FALSE - -/atom/proc/RelayMouseDown(atom/object, location, control, params, mob/user) - return FALSE - -/atom/proc/RelayMouseUp(atom/object, location, control, params, mob/user) - return FALSE - -/mob/proc/OnMouseDrag(atom/src_object, atom/over_object, src_location, over_location, src_control, over_control, params) - if(loc) - var/atom/A = loc - if(A.RelayMouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params, src)) - return - - var/obj/item/gun/gun = get_active_held_item() - if(check_intent(I_FLAG_HARM) && istype(over_object) && (isturf(over_object) || isturf(over_object.loc)) && !incapacitated() && istype(gun)) - gun.set_autofire(over_object, src) - -/mob/proc/OnMouseDown(atom/object, location, control, params) - if(loc) - var/atom/A = loc - if(A.RelayMouseDown(object, location, control, params, src)) - return - - var/obj/item/gun/gun = get_active_held_item() - if(check_intent(I_FLAG_HARM) && istype(object) && (isturf(object) || isturf(object.loc)) && !incapacitated() && istype(gun)) - gun.set_autofire(object, src) - -/mob/proc/OnMouseUp(atom/object, location, control, params) - if(loc) - var/atom/A = loc - if(A.RelayMouseUp(object, location, control, params, src)) - return - - var/obj/item/gun/gun = get_active_held_item() - if(istype(gun)) - gun.clear_autofire() diff --git a/code/_onclick/mouse_drag.dm b/code/_onclick/mouse_drag.dm new file mode 100644 index 00000000000..06a79c5a596 --- /dev/null +++ b/code/_onclick/mouse_drag.dm @@ -0,0 +1,91 @@ +/atom/proc/relayed_mouse_down(mob/user, object, location, control, params) + return null + +/atom/proc/relayed_mouse_held(mob/user, atom/target) + return null + +/atom/proc/relayed_mouse_up(mob/user, atom/target) + return null + +/mob/proc/on_mouse_down(object, location, control, params) + + // We are mouse down outside the map or on a screen element, assume it is not relevant. + if(!isatom(object) || istype(object, /obj/screen)) + return FALSE + + // Do not do this for things that are not in the world. + var/atom/atom = object + if(!isturf(atom) && !isturf(atom.loc)) + return FALSE + + // Debounce, we might already be holding. + if(_is_holding_mouse) + return FALSE + + // Ignore right click and middle click currently. + // Might be worth handling these in the future. + var/list/modifiers = params2list(params) + if(modifiers["middle"] || modifiers["right"]) + return FALSE + + // Keep track of when we started holding the mouse down so that we can check if we hold it long enough to start the drag behavior. + _started_mouse_down = world.time + + // Might be inside an exosuit or such that has its own handling for these inputs. + . = loc?.relayed_mouse_down(src, object, location, control, params) + if(isnull(.)) + + // Handle our actual 'drag beginning' logic. + var/obj/item/held = get_active_held_item() + . = istype(held) && held.wielder_mouse_drag_down(src, object, location, control, params) + + if(.) + update_mouse_pointer() + SSclickdrag.active_wielders[src] = TRUE + _is_holding_mouse = TRUE + +/mob/proc/on_mouse_held() + + if(!_is_holding_mouse) + return FALSE + + // Grace period before we start holding (rather than a single click) + if(world.time < _started_mouse_down + MOUSE_DRAG_DELAY) + return TRUE + + var/atom/mouse_over = _last_mouse_over_atom?.resolve() + if(QDELETED(mouse_over) || !istype(mouse_over)) + mouse_over = null + + // Might be inside an exosuit or such that has its own handling for these inputs. + . = loc?.relayed_mouse_held(src, mouse_over) + if(isnull(.)) + var/obj/item/held = get_active_held_item() + . = istype(held) && held.wielder_mouse_drag_held(src, mouse_over) + + if(.) + update_mouse_pointer() + set_dir(get_dir(src, mouse_over)) + else + on_mouse_up() + +/mob/proc/on_mouse_up(remove_from_processing = TRUE) + + if(!_is_holding_mouse) + return FALSE + + // Don't block the follow-up Click() if this wasn't an 'official' drag. + if(world.time >= _started_mouse_down + MOUSE_DRAG_DELAY) + var/atom/mouse_over = _last_mouse_over_atom?.resolve() + if(QDELETED(mouse_over) || !istype(mouse_over)) + mouse_over = null + . = loc?.relayed_mouse_up(src, mouse_over) + if(isnull(.)) + var/obj/item/held = get_active_held_item() + . = istype(held) && held.wielder_mouse_drag_up(src, mouse_over) + + update_mouse_pointer() + _is_holding_mouse = FALSE + SSclickdrag.active_wielders -= src + if(remove_from_processing && length(SSclickdrag.processing_wielders)) + SSclickdrag.processing_wielders -= src diff --git a/code/controllers/subsystems/clickdrag.dm b/code/controllers/subsystems/clickdrag.dm new file mode 100644 index 00000000000..97164a2fa64 --- /dev/null +++ b/code/controllers/subsystems/clickdrag.dm @@ -0,0 +1,54 @@ +SUBSYSTEM_DEF(clickdrag) + name = "Clickdrag" + wait = 1 + flags = SS_TICKER | SS_NO_INIT + + var/tmp/list/active_wielders = list() + var/tmp/active_wielders_copied_yet = FALSE + var/tmp/list/processing_wielders + +/datum/controller/subsystem/clickdrag/stat_entry() + ..("W:[active_wielders.len]") + +/datum/controller/subsystem/clickdrag/fire(resumed = 0) + + if(!resumed) + active_wielders_copied_yet = FALSE + + if(!active_wielders_copied_yet) + active_wielders_copied_yet = TRUE + processing_wielders = active_wielders.Copy() + + var/mob/wielder + var/i = 0 + while(i < processing_wielders.len) + i++ + wielder = processing_wielders[i] + if(!wielder.on_mouse_held()) + wielder.on_mouse_up(remove_from_processing = FALSE) // we will do this via our list iteration anyway + if (MC_TICK_CHECK) + processing_wielders.Cut(1, i+1) + return + processing_wielders.Cut() + +/client + // (BOOL) Flag for whether or not the next Click() should be blocked - Click() is called immediately after MouseUp() which isn't desirable. + VAR_PRIVATE/tmp/_block_next_click = FALSE + +/mob + // (DATUM) Tracker for clickdrag subsystem, + VAR_PRIVATE/tmp/weakref/_last_mouse_over_atom + // (BOOL) Flag for keeping track of if we're already processing or not. + VAR_PRIVATE/tmp/_is_holding_mouse = FALSE + // (INT) Time that we started holding the mouse down. + VAR_PRIVATE/tmp/_started_mouse_down = 0 + // (FLOAT) Delay before a hold is considered a hold rather than a single click. + VAR_PRIVATE/const/MOUSE_DRAG_DELAY = 0.25 SECONDS + +/client/MouseEntered(object,location,control,params) + UNLINT(mob?._last_mouse_over_atom = weakref(object)) + . = ..() + +/client/MouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params) + UNLINT(mob?._last_mouse_over_atom = weakref(over_object)) + . = ..() diff --git a/code/game/objects/items/__item.dm b/code/game/objects/items/__item.dm index 1bed5d4cf0f..dfc848c8b32 100644 --- a/code/game/objects/items/__item.dm +++ b/code/game/objects/items/__item.dm @@ -1353,4 +1353,26 @@ modules/mob/living/human/life.dm if you die, you will be zoomed out. qdel(src) /obj/item/proc/pick_attack_verb() - return DEFAULTPICK(attack_verb, attack_verb) || "attacked" // if it's not a list, return itself or just "attacked" \ No newline at end of file + return DEFAULTPICK(attack_verb, attack_verb) || "attacked" // if it's not a list, return itself or just "attacked" + +/obj/item/equipped(mob/user, slot) + if(user?.get_active_held_item() == src) + user.on_mouse_up() + . = ..() + +/obj/item/dropped(mob/user) + if(user?.get_active_held_item() == src) + user.on_mouse_up() + . = ..() + +// Called on initial mouse down event from wielding mob. Return TRUE to begin processing every 1ds. +/obj/item/proc/wielder_mouse_drag_down(mob/user, object, location, control, params) + return FALSE + +// Called every 1ds while mouse is down with an item that returned TRUE to wielder_mouse_drag_down(). Return FALSE to end processing. +/obj/item/proc/wielder_mouse_drag_held(mob/user, atom/target) + return FALSE + +// Called on mouse up event from wielding mob. +/obj/item/proc/wielder_mouse_drag_up(mob/user, atom/target) + return FALSE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 5a4b8a07540..5631eb2ae6b 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -430,23 +430,16 @@ var/global/list/localhost_addresses = list( if(world.byond_version >= 511 && byond_version >= 511 && client_fps >= CLIENT_MIN_FPS && client_fps <= CLIENT_MAX_FPS) vars["fps"] = client_fps -/client/MouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params) - . = ..() - var/mob/living/M = mob - if(istype(M)) - M.OnMouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params) - /client/MouseUp(object, location, control, params) . = ..() - var/mob/living/M = mob - if(istype(M)) - M.OnMouseUp(object, location, control, params) + if(mob?.on_mouse_up()) + _block_next_click = TRUE /client/MouseDown(object, location, control, params) . = ..() var/mob/living/M = mob if(istype(M) && !M.in_throw_mode) - M.OnMouseDown(object, location, control, params) + M.on_mouse_down(object, location, control, params) /client/verb/SetWindowIconSize(var/val as num|text) set hidden = 1 @@ -599,6 +592,12 @@ var/global/const/MAX_VIEW = 41 winset(src, "mainwindow.split", "splitter=[pct]") /client/Click(atom/A) + + // Mouse drag safeguard against a trailing Click() called after MouseUp(). + if(_block_next_click) + _block_next_click = FALSE + return + if(!user_acted(src)) return diff --git a/code/modules/clothing/spacesuits/rig/modules/combat.dm b/code/modules/clothing/spacesuits/rig/modules/combat.dm index 6ba6b837d81..c8412202284 100644 --- a/code/modules/clothing/spacesuits/rig/modules/combat.dm +++ b/code/modules/clothing/spacesuits/rig/modules/combat.dm @@ -237,6 +237,15 @@ gun.Fire(target,holder.wearer) return 1 +/obj/item/rig_module/mounted/wielder_mouse_drag_held(mob/user, atom/target) + return istype(gun) ? gun.wielder_mouse_drag_held(user, target) : ..() + +/obj/item/rig_module/mounted/wielder_mouse_drag_up(mob/user, atom/target) + return istype(gun) ? gun.wielder_mouse_drag_up(user, target) : ..() + +/obj/item/rig_module/mounted/wielder_mouse_drag_down(mob/user, object, location, control, params) + return istype(gun) ? gun.wielder_mouse_drag_down(user, object, location, control, params) : ..() + /obj/item/rig_module/mounted/lcannon name = "mounted laser cannon" diff --git a/code/modules/mechs/_mech.dm b/code/modules/mechs/_mech.dm index 957fe6f236a..c49b2db28d0 100644 --- a/code/modules/mechs/_mech.dm +++ b/code/modules/mechs/_mech.dm @@ -260,3 +260,34 @@ if(current_user) return FALSE return ..() + +// Handling for auto-fire mechanic +/mob/living/exosuit/mob_can_autofire(obj/item/gun/autofiring, atom/autofiring_at) + if(!(autofiring in selected_system)) // Make sure the gun is still selected. + return FALSE + return ..() + +/mob/living/exosuit/proc/relayed_pilot_check(mob/user) + if(!user || incapacitated() || user.incapacitated()) + return FALSE + if(!(user in pilots) && user != src) + return FALSE + if(!selected_system) + return FALSE + return TRUE + +// TODO: make mechs use inventory slots so we can just call on_mouse_foo(). +/mob/living/exosuit/relayed_mouse_down(mob/user, object, location, control, params) + if(!relayed_pilot_check(user)) + return ..() + . = selected_system.wielder_mouse_drag_down(src, object, location, control, params) + +/mob/living/exosuit/relayed_mouse_held(mob/user, atom/target) + if(!relayed_pilot_check(user)) + return ..() + return selected_system.wielder_mouse_drag_held(src, target) + +/mob/living/exosuit/relayed_mouse_up(mob/user, atom/target) + if(!relayed_pilot_check(user)) + return ..() + return selected_system.wielder_mouse_drag_up(src, target) diff --git a/code/modules/mechs/equipment/_equipment.dm b/code/modules/mechs/equipment/_equipment.dm index c98994742c5..7dde002884e 100644 --- a/code/modules/mechs/equipment/_equipment.dm +++ b/code/modules/mechs/equipment/_equipment.dm @@ -72,15 +72,6 @@ /obj/item/mech_equipment/proc/get_effective_obj() return src -/obj/item/mech_equipment/proc/MouseDragInteraction() - return 0 - -/obj/item/mech_equipment/proc/MouseDownInteraction() - return 0 - -/obj/item/mech_equipment/proc/MouseUpInteraction() - return 0 - /obj/item/mech_equipment/mob_can_unequip(mob/user, slot, disable_warning = FALSE, dropping = FALSE) . = ..() if(. && owner) diff --git a/code/modules/mechs/equipment/combat_projectile.dm b/code/modules/mechs/equipment/combat_projectile.dm index a717954a893..fa562fcfe1a 100644 --- a/code/modules/mechs/equipment/combat_projectile.dm +++ b/code/modules/mechs/equipment/combat_projectile.dm @@ -109,56 +109,3 @@ material = /decl/material/solid/metal/steel ammo_type = /obj/item/ammo_casing/rifle max_ammo = 300 - -// Handling for auto-fire mechanic -/mob/living/exosuit/can_autofire(obj/item/gun/autofiring, atom/autofiring_at) - if(autofiring.autofiring_by != src) - return FALSE - var/client/C = current_user ? current_user.client : client - - if(!C || !C.mob || C.mob.incapacitated()) - return FALSE - - if(!(autofiring_at in view(C.view, src))) - return FALSE - if(!(get_dir(src, autofiring_at) & dir)) - return FALSE - if(!(autofiring in selected_system)) // Make sure the gun is still selected. - return FALSE - return TRUE - -/obj/item/mech_equipment/mounted_system/projectile/MouseDownInteraction(atom/object, location, control, params, mob/user) - var/obj/item/gun/gun = holding - if(istype(object) && (isturf(object) || isturf(object.loc)) && istype(gun)) - if(user != src) - if(!user.incapacitated()) - gun.set_autofire(object, owner, FALSE) // Passed gun-firer is still the exosuit since all checks need to be done on the suit. - owner.current_user = user - else - if(!owner.incapacitated()) - gun.set_autofire(object, owner, FALSE) - owner.current_user = null - -/obj/item/mech_equipment/mounted_system/projectile/MouseUpInteraction(atom/object, location, control, params, mob/user) - var/obj/item/gun/gun = holding - if(istype(gun)) - gun.clear_autofire() - if(owner) // In case the owning exosuit has been gibbed etc. - owner.current_user = null - -/obj/item/mech_equipment/mounted_system/projectile/MouseDragInteraction(atom/src_object, atom/over_object, src_location, over_location, src_control, over_control, params, mob/user) - var/obj/item/gun/gun = holding - if(!owner) - gun?.clear_autofire() - return - if(!istype(gun)) - owner?.current_user = null - return - if(istype(over_object) && (isturf(over_object) || isturf(over_object.loc))) - if(user.incapacitated() || (user != owner && user != owner.current_user)) - gun.clear_autofire() - return - gun.set_autofire(over_object, owner, FALSE) - return - - gun.clear_autofire() diff --git a/code/modules/mechs/equipment/mounted_system.dm b/code/modules/mechs/equipment/mounted_system.dm index d8a62f70a8f..5bf0cce9c3b 100644 --- a/code/modules/mechs/equipment/mounted_system.dm +++ b/code/modules/mechs/equipment/mounted_system.dm @@ -39,3 +39,12 @@ /obj/item/mech_equipment/mounted_system/get_hardpoint_maptext() return (holding ? holding.get_hardpoint_maptext() : null) + +/obj/item/mech_equipment/mounted_system/wielder_mouse_drag_held(mob/user, atom/target) + return (holding ? holding.wielder_mouse_drag_held(user, target) : ..()) + +/obj/item/mech_equipment/mounted_system/wielder_mouse_drag_up(mob/user, atom/target) + return (holding ? holding.wielder_mouse_drag_up(user, target) : ..()) + +/obj/item/mech_equipment/mounted_system/wielder_mouse_drag_down(mob/user, object, location, control, params) + return (holding ? holding.wielder_mouse_drag_down(user, object, location, control, params) : ..()) diff --git a/code/modules/mechs/mech_interaction.dm b/code/modules/mechs/mech_interaction.dm index eb900899d02..abc0708cb39 100644 --- a/code/modules/mechs/mech_interaction.dm +++ b/code/modules/mechs/mech_interaction.dm @@ -9,52 +9,6 @@ return TRUE . = ..() -/mob/living/exosuit/RelayMouseDrag(atom/src_object, atom/over_object, src_location, over_location, src_control, over_control, params, mob/user) - if(user && (user in pilots) && user.loc == src) - return OnMouseDrag(src_object, over_object, src_location, over_location, src_control, over_control, params, user) - return ..() - -/mob/living/exosuit/OnMouseDrag(atom/src_object, atom/over_object, src_location, over_location, src_control, over_control, params, mob/user) - if(!user || incapacitated() || user.incapacitated()) - return FALSE - - if(!(user in pilots) && user != src) - return FALSE - - //This is handled at active module level really, it is the one who has to know if it's supposed to act - if(selected_system) - return selected_system.MouseDragInteraction(src_object, over_object, src_location, over_location, src_control, over_control, params, user) - -/mob/living/exosuit/RelayMouseDown(atom/object, location, control, params, mob/user) - if(user && (user in pilots) && user.loc == src) - return OnMouseDown(object, location, control, params, user) - return ..() - -/mob/living/exosuit/OnMouseDown(atom/object, location, control, params, mob/user) - if(!user || incapacitated() || user.incapacitated()) - return FALSE - - if(!(user in pilots) && user != src) - return FALSE - - if(selected_system) - return selected_system.MouseDownInteraction(object, location, control, params, user) - -/mob/living/exosuit/RelayMouseUp(atom/object, location, control, params, mob/user) - if(user && (user in pilots) && user.loc == src) - return OnMouseUp(object, location, control, params, user) - return ..() - -/mob/living/exosuit/OnMouseUp(atom/object, location, control, params, mob/user) - if(!user || incapacitated() || user.incapacitated()) - return FALSE - - if(!(user in pilots) && user != src) - return FALSE - - if(selected_system) - return selected_system.MouseUpInteraction(object, location, control, params, user) - /datum/click_handler/default/mech/OnClick(var/atom/A, var/params) var/mob/living/exosuit/E = user.loc if(!istype(E)) diff --git a/code/modules/mob/living/inventory.dm b/code/modules/mob/living/inventory.dm index c4cc217a30e..675b6eb423b 100644 --- a/code/modules/mob/living/inventory.dm +++ b/code/modules/mob/living/inventory.dm @@ -67,13 +67,17 @@ /mob/living/select_held_item_slot(var/slot) . = ..() var/last_slot = get_active_held_item_slot() - if(slot != last_slot && (slot in get_held_item_slots())) - _held_item_slot_selected = slot - if(istype(hud_used)) - hud_used.update_hand_elements() - var/obj/item/I = get_active_held_item() - if(istype(I)) - I.on_active_hand() + if(slot == last_slot) + return + on_mouse_up() + if(!(slot in get_held_item_slots())) + return + _held_item_slot_selected = slot + if(istype(hud_used)) + hud_used.update_hand_elements() + var/obj/item/I = get_active_held_item() + if(istype(I)) + I.on_active_hand() // Defer proc for the sake of delimbing root limbs with multiple graspers (serpentid) /mob/living/proc/queue_hand_rebuild() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 612a4e888b4..85a7325d9f3 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1613,3 +1613,7 @@ var/global/const/ACTION_DANGER_ALL = 2 /mob/proc/get_background_datum(cat_type) return global.using_map.default_background_info[cat_type] + +// Check if this mob can full-auto fire a gun at a target. +/mob/proc/mob_can_autofire(obj/item/gun/gun, atom/target) + return TRUE // TODO: dexterity check? That will be handled by the item itself probably. diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/_gun.dm similarity index 92% rename from code/modules/projectiles/gun.dm rename to code/modules/projectiles/_gun.dm index 3e61a8a5001..205e1136bcd 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/_gun.dm @@ -23,9 +23,10 @@ else settings[propname] = propvalue -/datum/firemode/proc/apply_to(obj/item/gun/gun) +/datum/firemode/proc/apply_firemode_to(obj/item/gun/gun) for(var/propname in settings) - gun.vars[propname] = settings[propname] + if(propname in gun.vars) + gun.vars[propname] = settings[propname] //Parent gun type. Guns are weapons that can be aimed at mobs and act over a distance /obj/item/gun @@ -83,11 +84,6 @@ var/has_safety = TRUE var/safety_icon //overlay to apply to gun based on safety state, if any - var/autofire_enabled = FALSE - var/atom/autofiring_at - var/mob/autofiring_by - var/autofiring_timer - // Spam prevention var/last_fire_message_type var/last_fire_message_time @@ -104,58 +100,12 @@ if(scope_zoom) verbs += /obj/item/gun/proc/scope -/obj/item/gun/Destroy() - // autofire timer is automatically cleaned up - autofiring_at = null - autofiring_by = null - . = ..() - /obj/item/gun/is_held_twohanded(mob/living/wielder) return one_hand_penalty > 0 && ..() /obj/item/gun/preserve_in_cryopod(var/obj/machinery/cryopod/pod) return TRUE -/obj/item/gun/proc/set_autofire(var/atom/fire_at, var/mob/fire_by, var/autoturn = TRUE) - . = TRUE - if(!istype(fire_at) || !istype(fire_by)) - . = FALSE - else if(QDELETED(fire_at) || QDELETED(fire_by) || QDELETED(src)) - . = FALSE - else if(!autofire_enabled) - . = FALSE - if(.) - autofiring_at = fire_at - autofiring_by = fire_by - if(!autofiring_timer) - autofiring_timer = addtimer(CALLBACK(src, PROC_REF(handle_autofire), autoturn), burst_delay, (TIMER_STOPPABLE | TIMER_LOOP | TIMER_UNIQUE | TIMER_OVERRIDE)) - else - clear_autofire() - -/obj/item/gun/proc/clear_autofire() - autofiring_at = null - autofiring_by = null - if(autofiring_timer) - deltimer(autofiring_timer) - autofiring_timer = null - -/obj/item/gun/proc/handle_autofire(autoturn) - set waitfor = FALSE - . = TRUE - if(QDELETED(autofiring_at) || QDELETED(autofiring_by)) - . = FALSE - else if(!autofiring_by.can_autofire(src, autofiring_at)) - . = FALSE - if(!.) - clear_autofire() - else if(can_autofire()) - try_autofire(autoturn) - -/obj/item/gun/proc/try_autofire(autoturn) - if(autoturn) - autofiring_by.set_dir(get_dir(src, autofiring_at)) - Fire(autofiring_at, autofiring_by, null, (get_dist(autofiring_at, autofiring_by) <= 1), FALSE, FALSE) - /obj/item/gun/update_twohanding() if(one_hand_penalty) update_icon() // In case item_state is set somewhere else. @@ -269,7 +219,6 @@ check_accidents(user) update_icon() . = ..() - clear_autofire() /obj/item/gun/proc/Fire(atom/target, atom/movable/firer, clickparams, pointblank = FALSE, reflex = FALSE, set_click_cooldown = TRUE, target_zone = BP_CHEST) if(!firer || !target) @@ -660,7 +609,7 @@ sel_mode = next_mode var/datum/firemode/new_mode = firemodes[sel_mode] - new_mode.apply_to(src) + new_mode.apply_firemode_to(src) playsound(loc, selector_sound, 50, 1) return new_mode @@ -716,9 +665,6 @@ return TRUE return FALSE -/obj/item/gun/proc/can_autofire() - return (autofire_enabled && world.time >= next_fire_time) - /obj/item/gun/proc/check_accidents(mob/living/user, message = "[user] fumbles with \the [src] and it goes off!",skill_path = SKILL_WEAPONS, fail_chance = 20, no_more_fail = SKILL_EXPERT, factor = 2) if(istype(user) && !safety() && user.skill_fail_prob(skill_path, fail_chance, no_more_fail, factor) && special_check(user)) user.visible_message(SPAN_WARNING(message)) @@ -744,13 +690,6 @@ if(M.aiming) M.aiming.toggle_active(FALSE, TRUE) -/mob/proc/can_autofire(var/obj/item/gun/autofiring, var/atom/autofiring_at) - if(!client || !(autofiring_at in view(client.view,src))) - return FALSE - if(get_active_held_item() != autofiring || incapacitated()) - return FALSE - return TRUE - /obj/item/gun/get_quick_interaction_handler(mob/user) return GET_DECL(/decl/interaction_handler/gun/toggle_safety) diff --git a/code/modules/projectiles/autofire.dm b/code/modules/projectiles/autofire.dm new file mode 100644 index 00000000000..5577984a6d7 --- /dev/null +++ b/code/modules/projectiles/autofire.dm @@ -0,0 +1,37 @@ +/obj/item/gun + var/autofire_enabled = FALSE + var/autofire_delay = 0.1 SECOND + var/next_autofire + +/obj/item/gun/proc/gun_can_autofire() + return (autofire_enabled && world.time >= next_fire_time) + +/obj/item/gun/proc/autofire_check(mob/user, atom/target) + if(!gun_can_autofire()) + return FALSE + if(!istype(user)) + return FALSE + if(!user.check_intent(I_FLAG_HARM)) + return FALSE + if(user.incapacitated()) + return FALSE + if(!user.mob_can_autofire(src, target)) + return FALSE + if(!istype(target) || (!isturf(target) && !isturf(target.loc))) + return FALSE + return TRUE + +/obj/item/gun/wielder_mouse_drag_down(mob/user, object, location, control, params) + if(autofire_check(user, object)) + return TRUE + return FALSE + +/obj/item/gun/wielder_mouse_drag_held(mob/user, atom/target) + next_fire_time = world.time // Reset so we aren't held to a timer. + if(!autofire_check(user, target)) + return FALSE + if(world.time < next_autofire) + return TRUE + next_autofire = world.time + autofire_delay + Fire(target, user, null, (get_dist(target, user) <= 1), FALSE, FALSE) + return TRUE diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm index d69f9bdef70..1c2b99a4587 100644 --- a/code/modules/projectiles/guns/energy/special.dm +++ b/code/modules/projectiles/guns/energy/special.dm @@ -144,6 +144,8 @@ use_external_power = 1 max_shots = 4 has_safety = FALSE + autofire_enabled = TRUE + autofire_delay = 0.5 SECONDS /obj/item/gun/energy/plasmacutter/proc/slice(var/mob/M = null) var/obj/item/cell/power_supply = get_cell() diff --git a/code/modules/projectiles/guns/launcher/bows/_bow.dm b/code/modules/projectiles/guns/launcher/bows/_bow.dm index d4182d48fdd..5fee0efa549 100644 --- a/code/modules/projectiles/guns/launcher/bows/_bow.dm +++ b/code/modules/projectiles/guns/launcher/bows/_bow.dm @@ -46,51 +46,6 @@ /// How big is this bow when unstrung? Uses initial w_class if unset. var/unstrung_w_class -/obj/item/gun/launcher/bow/set_autofire(var/atom/fire_at, var/mob/fire_by, var/autoturn = TRUE) - if(!autofire_enabled || autofiring_at) - return ..() - . = ..() - if(ismob(fire_by)) - if(!get_loaded_arrow(fire_by) && fire_by.skill_check(SKILL_WEAPONS, SKILL_ADEPT)) - load_available_ammo(fire_by) - if(check_can_draw(fire_by)) - tension = 0 - next_tension_step = world.time + get_draw_time(fire_by) - fire_by.set_dir(get_dir(fire_by, fire_at)) - show_draw_message(fire_by) - update_icon() - -/obj/item/gun/launcher/bow/try_autofire(autoturn) - if(!autofire_enabled) - return ..() - var/mob/wielder = loc - if(!ismob(wielder) || !check_can_draw(wielder)) - clear_autofire() - else - wielder.set_dir(get_dir(wielder, autofiring_at)) - if(world.time >= next_tension_step && tension < max_tension) - next_tension_step = world.time + get_draw_time(wielder) - tension++ - if(tension == max_tension) - show_max_draw_message(wielder) - else - show_working_draw_message(wielder) - update_icon() - -/obj/item/gun/launcher/bow/clear_autofire() - if(!autofire_enabled) - return ..() - var/mob/living/wielder = loc - if(tension && istype(wielder) && !wielder.incapacitated() && wielder.get_active_held_item() == src && get_loaded_arrow()) - wielder.set_dir(get_dir(wielder, autofiring_at)) - Fire(autofiring_at, autofiring_by, null, (get_dist(autofiring_at, autofiring_by) <= 1), FALSE, FALSE) - . = ..() - if(tension) - if(istype(wielder)) - show_cancel_draw_message(wielder) - tension = 0 - update_icon() - /obj/item/gun/launcher/bow/handle_click_empty(atom/movable/firer) if(check_fire_message_spam("click")) to_chat(firer, SPAN_WARNING("\The [src] has nothing loaded.")) diff --git a/code/modules/projectiles/guns/launcher/bows/bow_drawing.dm b/code/modules/projectiles/guns/launcher/bows/bow_drawing.dm index ccef4f510c1..b3a15416bd0 100644 --- a/code/modules/projectiles/guns/launcher/bows/bow_drawing.dm +++ b/code/modules/projectiles/guns/launcher/bows/bow_drawing.dm @@ -63,3 +63,60 @@ show_working_draw_message(user) continue_drawing(user) update_icon() + +/obj/item/gun/launcher/bow/wielder_mouse_drag_down(mob/user, object, location, control, params) + if(drawing_bow) + return FALSE + . = ..() + +// Nock an arrow, or continue to draw the string back. +// We do this here so we don't instantly nock an arrow even if this is not a proper drag yet. +// DO NOT CALL PARENT, default full auto behavior is to fire while held. +/obj/item/gun/launcher/bow/wielder_mouse_drag_held(mob/user, atom/target) + + if(!autofire_enabled) + return FALSE + + // High skills mean you automatically nock an arrow before you draw. + if(tension <= 0 && !get_loaded_arrow(user) && user.skill_check(SKILL_WEAPONS, SKILL_ADEPT)) + load_available_ammo(user) + + if(!check_can_draw(user)) + return FALSE + + // Start drawing. + if(!drawing_bow) + drawing_bow = TRUE + tension = 0 + next_tension_step = world.time + get_draw_time(user) + if(user && isatom(target)) + user.set_dir(get_dir(user, target)) + show_draw_message(user) + update_icon() + return TRUE + + // Already drawing - keep drawing. + if(world.time >= next_tension_step && tension < max_tension) + next_tension_step = world.time + get_draw_time(user) + tension++ + if(tension == max_tension) + show_max_draw_message(user) + else + show_working_draw_message(user) + update_icon() + return TRUE + +// Fire! +/obj/item/gun/launcher/bow/wielder_mouse_drag_up(mob/user, atom/target) + if(!autofire_enabled || !istype(target)) + return FALSE + if(tension && istype(user) && !user.incapacitated() && user.get_active_held_item() == src && get_loaded_arrow()) + user.set_dir(get_dir(user, target)) + Fire(target, user, null, (get_dist(target, user) <= 1), FALSE, FALSE) + if(tension) + if(istype(user)) + show_cancel_draw_message(user) + tension = 0 + update_icon() + drawing_bow = FALSE + return TRUE diff --git a/code/modules/projectiles/guns/launcher/bows/bow_interaction.dm b/code/modules/projectiles/guns/launcher/bows/bow_interaction.dm index 14eaf835981..ac2434a56e7 100644 --- a/code/modules/projectiles/guns/launcher/bows/bow_interaction.dm +++ b/code/modules/projectiles/guns/launcher/bows/bow_interaction.dm @@ -84,8 +84,9 @@ /obj/item/gun/launcher/bow/proc/relax_tension(mob/user) tension = 0 update_icon() - if(autofire_enabled) - clear_autofire() + // Cancel any drag fire. + if(autofire_enabled && user.get_active_held_item() == src) + user.on_mouse_up() else if(user) show_string_relax_message(user) diff --git a/code/modules/projectiles/guns/launcher/foam_gun.dm b/code/modules/projectiles/guns/launcher/foam_gun.dm index 6a351feeda8..7350c7128ae 100644 --- a/code/modules/projectiles/guns/launcher/foam_gun.dm +++ b/code/modules/projectiles/guns/launcher/foam_gun.dm @@ -86,7 +86,7 @@ icon = 'icons/obj/guns/foam/machine_gun.dmi' w_class = ITEM_SIZE_NORMAL fire_delay = 0 - autofire_enabled = 1 + autofire_enabled = TRUE one_hand_penalty = 3 max_darts = 30 burst_delay = 1 diff --git a/code/modules/projectiles/guns/projectile/automatic.dm b/code/modules/projectiles/guns/projectile/automatic.dm index 1614d25961e..9646296d5ff 100644 --- a/code/modules/projectiles/guns/projectile/automatic.dm +++ b/code/modules/projectiles/guns/projectile/automatic.dm @@ -149,9 +149,9 @@ burst = 3 burst_accuracy = list(0,-1,-1) dispersion = list(0.0, 0.6, 1.0) + autofire_enabled = TRUE fire_delay = 0 - autofire_enabled = 1 mag_insert_sound = 'sound/weapons/guns/interaction/batrifle_magin.ogg' mag_remove_sound = 'sound/weapons/guns/interaction/batrifle_magout.ogg' @@ -177,8 +177,8 @@ return FALSE return TRUE -/obj/item/gun/projectile/automatic/machine/set_autofire(atom/fire_at, mob/fire_by, autoturn) - if(!special_check(fire_by)) +/obj/item/gun/projectile/automatic/machine/wielder_mouse_drag_down(mob/user, object, location, control, params) + if(!special_check(user)) return FALSE . = ..() if(. && !spin_up_time) @@ -186,7 +186,7 @@ sound_token = play_looping_sound(src, "machine_gun", 'sound/mecha/hydraulic.ogg', volume = 30) spin_up_time = world.time -/obj/item/gun/projectile/automatic/machine/clear_autofire() +/obj/item/gun/projectile/automatic/machine/wielder_mouse_drag_up(mob/user, atom/target) . = ..() spin_up_time = null - QDEL_NULL(sound_token) \ No newline at end of file + QDEL_NULL(sound_token) diff --git a/mods/content/sealant_gun/sealant_gun.dm b/mods/content/sealant_gun/sealant_gun.dm index 23a753cc010..79e7a251d4f 100644 --- a/mods/content/sealant_gun/sealant_gun.dm +++ b/mods/content/sealant_gun/sealant_gun.dm @@ -4,6 +4,7 @@ icon = 'mods/content/sealant_gun/icons/sealant_gun.dmi' icon_state = ICON_STATE_WORLD autofire_enabled = TRUE + autofire_delay = 0.5 SECONDS has_safety = FALSE waterproof = TRUE w_class = ITEM_SIZE_GARGANTUAN diff --git a/nebula.dme b/nebula.dme index 92e4d634b7f..a30d4dfdb40 100644 --- a/nebula.dme +++ b/nebula.dme @@ -176,7 +176,7 @@ #include "code\_onclick\drag_drop.dm" #include "code\_onclick\ghost.dm" #include "code\_onclick\item_attack.dm" -#include "code\_onclick\MouseDrag.dm" +#include "code\_onclick\mouse_drag.dm" #include "code\_onclick\other_mobs.dm" #include "code\_onclick\rig.dm" #include "code\_onclick\hud\_defines.dm" @@ -273,6 +273,7 @@ #include "code\controllers\subsystems\ambience.dm" #include "code\controllers\subsystems\ao.dm" #include "code\controllers\subsystems\atoms.dm" +#include "code\controllers\subsystems\clickdrag.dm" #include "code\controllers\subsystems\configuration.dm" #include "code\controllers\subsystems\daycycle.dm" #include "code\controllers\subsystems\disposals.dm" @@ -3492,8 +3493,9 @@ #include "code\modules\power\solar\solar_control.dm" #include "code\modules\power\solar\solar_panel.dm" #include "code\modules\power\solar\tracker.dm" +#include "code\modules\projectiles\_gun.dm" #include "code\modules\projectiles\ammunition.dm" -#include "code\modules\projectiles\gun.dm" +#include "code\modules\projectiles\autofire.dm" #include "code\modules\projectiles\projectile.dm" #include "code\modules\projectiles\secure.dm" #include "code\modules\projectiles\ammunition\boxes.dm" From 13e4f2c2dc4d8bc6bf671694b2d1745716f5cc90 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Fri, 22 May 2026 09:04:48 +1000 Subject: [PATCH 23/79] Automatic changelog generation for PR #5373 [ci skip] --- html/changelogs/AutoChangeLog-pr-5373.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-5373.yml diff --git a/html/changelogs/AutoChangeLog-pr-5373.yml b/html/changelogs/AutoChangeLog-pr-5373.yml new file mode 100644 index 00000000000..54f648ad7be --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-5373.yml @@ -0,0 +1,6 @@ +author: MistakeNot4892 +changes: + - {tweak: Mech plasmacutter is full auto and mech AR full auto should now + work.} + - {tweak: Autofire should be more responsive in general.} +delete-after: true From 31208360e8b26b5d8de8bd56d2c25fa313158c06 Mon Sep 17 00:00:00 2001 From: MistakeNot4892 Date: Sun, 17 May 2026 19:53:08 +1000 Subject: [PATCH 24/79] Temporary commit to make girders craftable with metal sheets. --- code/game/objects/structures/girders.dm | 3 ++- code/modules/crafting/stack_recipes/recipes_steel.dm | 6 ++++++ code/modules/crafting/stack_recipes/recipes_struts.dm | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 83ebf5a6823..861e57b76ef 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -9,7 +9,8 @@ material_alteration = MAT_FLAG_ALTERATION_NAME | MAT_FLAG_ALTERATION_COLOR tool_interaction_flags = (TOOL_INTERACTION_ANCHOR | TOOL_INTERACTION_DECONSTRUCT) max_health = 100 - parts_amount = 2 + material = /decl/material/solid/metal/steel + parts_amount = 5 parts_type = /obj/item/stack/material/rods var/cover = 50 diff --git a/code/modules/crafting/stack_recipes/recipes_steel.dm b/code/modules/crafting/stack_recipes/recipes_steel.dm index 8f15300c367..5f95cd08341 100644 --- a/code/modules/crafting/stack_recipes/recipes_steel.dm +++ b/code/modules/crafting/stack_recipes/recipes_steel.dm @@ -108,3 +108,9 @@ /decl/stack_recipe/steel/furniture/drill_brace result_type = /obj/structure/drill_brace + +// Temporary recipe until dev merge. +/decl/stack_recipe/steel/girder + result_type = /obj/structure/girder + required_wall_support_value = 10 + available_to_map_tech_level = MAP_TECH_LEVEL_SPACE diff --git a/code/modules/crafting/stack_recipes/recipes_struts.dm b/code/modules/crafting/stack_recipes/recipes_struts.dm index e6fe0f9b67f..a6692f00ea5 100644 --- a/code/modules/crafting/stack_recipes/recipes_struts.dm +++ b/code/modules/crafting/stack_recipes/recipes_struts.dm @@ -36,7 +36,6 @@ /decl/stack_recipe/rods/girder result_type = /obj/structure/girder required_wall_support_value = 10 - req_amount = 5 * SHEET_MATERIAL_AMOUNT // Arbitrary value since girders return weird matter values. available_to_map_tech_level = MAP_TECH_LEVEL_SPACE /decl/stack_recipe/rods/wall_frame From 5458f3ae5ec9ae7f16cd02dc48a3d5e9d8aa0703 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Fri, 22 May 2026 02:32:07 +0000 Subject: [PATCH 25/79] Automatic changelog generation [ci skip] --- html/changelog.html | 7 +++++++ html/changelogs/.all_changelog.yml | 4 ++++ html/changelogs/AutoChangeLog-pr-5373.yml | 6 ------ 3 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-5373.yml diff --git a/html/changelog.html b/html/changelog.html index 796f54f9fd8..45343c1006a 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -52,6 +52,13 @@ -->
+

22 May 2026

+

MistakeNot4892 updated:

+
    +
  • Mech plasmacutter is full auto and mech AR full auto should now work.
  • +
  • Autofire should be more responsive in general.
  • +
+

27 March 2026

Typhin updated:

    diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml index 83328d67f5a..f044af70e15 100644 --- a/html/changelogs/.all_changelog.yml +++ b/html/changelogs/.all_changelog.yml @@ -15070,3 +15070,7 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py. 2026-03-27: Typhin: - tweak: Prevented Garlic Oil from dealing TOX damage +2026-05-22: + MistakeNot4892: + - tweak: Mech plasmacutter is full auto and mech AR full auto should now work. + - tweak: Autofire should be more responsive in general. diff --git a/html/changelogs/AutoChangeLog-pr-5373.yml b/html/changelogs/AutoChangeLog-pr-5373.yml deleted file mode 100644 index 54f648ad7be..00000000000 --- a/html/changelogs/AutoChangeLog-pr-5373.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: MistakeNot4892 -changes: - - {tweak: Mech plasmacutter is full auto and mech AR full auto should now - work.} - - {tweak: Autofire should be more responsive in general.} -delete-after: true From 51dc453c5574d8e54865c21f87bd027ec78db9be Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Wed, 27 May 2026 23:46:41 -0400 Subject: [PATCH 26/79] Clean up unused/defunct message server code --- code/game/machinery/message_server.dm | 32 +++++---------------------- code/modules/events/ion_storm.dm | 10 --------- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/code/game/machinery/message_server.dm b/code/game/machinery/message_server.dm index 9ddf2a4d9f3..8fbc655edc4 100644 --- a/code/game/machinery/message_server.dm +++ b/code/game/machinery/message_server.dm @@ -48,22 +48,10 @@ var/global/list/message_servers = list() active_power_usage = 100 var/list/datum/data_rc_msg/rc_msgs = list() - var/active = 1 + var/active = TRUE var/power_failure = 0 // Reboot timer after power outage var/decryptkey = "password" - /// Spam filtering stuff. Messages having theese tokens will be rejected by server. Case sensitive. - var/list/spamfilter = list( - "You have won", - "your prize", - "male enhancement", - "shitcurity", - "are happy to inform you", - "account number", - "enter your PIN" - ) - var/spamfilter_limit = MESSAGE_SERVER_DEFAULT_SPAM_LIMIT //Maximal amount of tokens - stat_immune = 0 uncreated_component_parts = null construct_state = /decl/machine_construction/default/panel_closed @@ -80,7 +68,7 @@ var/global/list/message_servers = list() /obj/machinery/network/message_server/Process() ..() if(active && (stat & (BROKEN|NOPOWER))) - active = 0 + active = FALSE power_failure = 10 update_icon() return @@ -88,7 +76,7 @@ var/global/list/message_servers = list() return else if(power_failure > 0) if(!(--power_failure)) - active = 1 + active = TRUE update_icon() /obj/machinery/network/message_server/proc/send_rc_message(var/recipient = "",var/sender = "",var/message = "",var/stamp = "", var/id_auth = "", var/priority = 1) @@ -99,6 +87,8 @@ var/global/list/message_servers = list() if (stamp) authmsg += "[stamp]
    " . = FALSE + if(!active) + return // message suppressed but still saved on the message server var/datum/extension/network_device/network_device = get_extension(src, /datum/extension/network_device) var/datum/computer_network/network = network_device?.get_network() @@ -129,22 +119,12 @@ var/global/list/message_servers = list() /obj/machinery/network/message_server/interface_interact(mob/user) if(!CanInteract(user, DefaultTopicState())) return FALSE - to_chat(user, "You toggle PDA message passing from [active ? "On" : "Off"] to [active ? "Off" : "On"]") + to_chat(user, "You toggle message passing from [active ? "On" : "Off"] to [active ? "Off" : "On"]") active = !active power_failure = 0 update_icon() return TRUE -/obj/machinery/network/message_server/attackby(obj/item/used_item, mob/user) - if (active && !(stat & (BROKEN|NOPOWER)) && (spamfilter_limit < MESSAGE_SERVER_DEFAULT_SPAM_LIMIT*2) && \ - istype(used_item,/obj/item/stock_parts/circuitboard/message_monitor)) - spamfilter_limit += round(MESSAGE_SERVER_DEFAULT_SPAM_LIMIT / 2) - qdel(used_item) - to_chat(user, "You install additional memory and processors into \the [src]. Its filtering capabilities been enhanced.") - return TRUE - else - return ..() - /obj/machinery/network/message_server/on_update_icon() icon_state = initial(icon_state) if(panel_open) diff --git a/code/modules/events/ion_storm.dm b/code/modules/events/ion_storm.dm index 48702089f43..0a7a83f4471 100644 --- a/code/modules/events/ion_storm.dm +++ b/code/modules/events/ion_storm.dm @@ -108,16 +108,6 @@ S.add_ion_law(law) S.show_laws() - for(var/z in affecting_z) - var/obj/machinery/network/message_server/MS = get_message_server_for_z(z) - if(MS) - MS.spamfilter.Cut() - var/i - for (i = 1, i <= MS.spamfilter_limit, i++) - MS.spamfilter += pick("kitty","HONK","rev","malf","liberty","freedom","drugs", "[global.using_map.station_short]", \ - "admin","ponies","heresy","meow","Pun Pun","monkey","Ian","moron","pizza","message","spam",\ - "director", "Hello", "Hi!"," ","nuke","crate","dwarf","xeno") - /datum/event/ionstorm/tick() if(botEmagChance) for(var/mob/living/bot/bot in global.living_mob_list_) From afa62c7da1b88e77c35ef51698586c0d15130c4e Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Wed, 27 May 2026 23:47:30 -0400 Subject: [PATCH 27/79] Improve requests console UI slightly --- code/game/machinery/requests_console.dm | 2 +- nano/templates/request_console.tmpl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm index 3452f128f8b..681270aa8cb 100644 --- a/code/game/machinery/requests_console.dm +++ b/code/game/machinery/requests_console.dm @@ -107,7 +107,7 @@ var/global/req_console_information = list() data["message"] = message data["recipient"] = recipient - data["priortiy"] = priority + data["priority"] = priority data["msgStamped"] = msgStamped data["msgVerified"] = msgVerified data["announceAuth"] = announceAuth diff --git a/nano/templates/request_console.tmpl b/nano/templates/request_console.tmpl index 698f7b162c9..ee7fc4f0a2c 100644 --- a/nano/templates/request_console.tmpl +++ b/nano/templates/request_console.tmpl @@ -66,7 +66,7 @@ Used In File(s): \code\game\machinery\requests_console.dm
    Message sent successfully.
    {{:helper.link('Continue', 'arrowthick-1-e', { 'setScreen' : 0 })}}
    {{else data.screen == 5}} -
    An Error occurred. Message not sent.
    +
    An error occurred and your message could not be sent. Retry in 30 seconds. If the issue persists, contact your system administrator for assistance.
    {{:helper.link('Continue', 'arrowthick-1-e', { 'setScreen' : 0 })}}
    {{else data.screen == 6}}
    @@ -104,9 +104,9 @@ Used In File(s): \code\game\machinery\requests_console.dm
    {{else}} {{if data.newmessagepriority == 1}} -
    There are new messages
    +
    There are new messages.
    {{else data.newmessagepriority == 2}} -
    NEW PRIORITY MESSAGES
    +
    NEW PRIORITY MESSAGE!
    {{/if}}
    {{:helper.link('View Messages', data.newmessagepriority ? 'mail-closed' : 'mail-open', { 'setScreen' : 6 })}}

    From 90e50ebad84cb76928f0e35935f61f9e5df8b948 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Wed, 27 May 2026 23:54:14 -0400 Subject: [PATCH 28/79] Make the base law upload console an abstract type --- code/game/machinery/computer/law.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/game/machinery/computer/law.dm b/code/game/machinery/computer/law.dm index 992d2b211b5..6e15e0e1df3 100644 --- a/code/game/machinery/computer/law.dm +++ b/code/game/machinery/computer/law.dm @@ -1,4 +1,5 @@ /obj/machinery/computer/upload + abstract_type = /obj/machinery/computer/upload name = "unused upload console" icon_keyboard = "rd_key" icon_screen = "command" From 25e3e51677ef73ab1f997c08d4b549e082f3555c Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Fri, 29 May 2026 02:14:51 +0000 Subject: [PATCH 29/79] Automatic changelog generation [ci skip] --- html/changelog.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index 45343c1006a..2ee1ce5c883 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -58,12 +58,6 @@

    MistakeNot4892 updated:

  • Mech plasmacutter is full auto and mech AR full auto should now work.
  • Autofire should be more responsive in general.
- -

27 March 2026

-

Typhin updated:

-
    -
  • Prevented Garlic Oil from dealing TOX damage
  • -
From 6c767b46aace81caaceedac8050945b93e0f11d5 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Fri, 29 May 2026 20:09:32 +1000 Subject: [PATCH 30/79] Automatic changelog generation for PR #5385 [ci skip] --- html/changelogs/AutoChangeLog-pr-5385.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-5385.yml diff --git a/html/changelogs/AutoChangeLog-pr-5385.yml b/html/changelogs/AutoChangeLog-pr-5385.yml new file mode 100644 index 00000000000..ef2f7cadb4b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-5385.yml @@ -0,0 +1,6 @@ +author: Penelope Haze +changes: + - {tweak: Deactivating message passing on the message server now prevents + requests console messages from being received. They will still be logged + for admins and on the in-game message server console.} +delete-after: true From 2f36ff5d010b218eaf6caf84dbb5f8da5fa8d141 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Sat, 30 May 2026 02:10:36 +0000 Subject: [PATCH 31/79] Automatic changelog generation [ci skip] --- html/changelog.html | 6 ++++++ html/changelogs/.all_changelog.yml | 5 +++++ html/changelogs/AutoChangeLog-pr-5385.yml | 6 ------ 3 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-5385.yml diff --git a/html/changelog.html b/html/changelog.html index 2ee1ce5c883..185b3d9def1 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -52,6 +52,12 @@ -->
+

30 May 2026

+

Penelope Haze updated:

+
    +
  • Deactivating message passing on the message server now prevents requests console messages from being received. They will still be logged for admins and on the in-game message server console.
  • +
+

22 May 2026

MistakeNot4892 updated:

    diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml index f044af70e15..2fb6a977c18 100644 --- a/html/changelogs/.all_changelog.yml +++ b/html/changelogs/.all_changelog.yml @@ -15074,3 +15074,8 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py. MistakeNot4892: - tweak: Mech plasmacutter is full auto and mech AR full auto should now work. - tweak: Autofire should be more responsive in general. +2026-05-30: + Penelope Haze: + - tweak: Deactivating message passing on the message server now prevents requests + console messages from being received. They will still be logged for admins and + on the in-game message server console. diff --git a/html/changelogs/AutoChangeLog-pr-5385.yml b/html/changelogs/AutoChangeLog-pr-5385.yml deleted file mode 100644 index ef2f7cadb4b..00000000000 --- a/html/changelogs/AutoChangeLog-pr-5385.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: Penelope Haze -changes: - - {tweak: Deactivating message passing on the message server now prevents - requests console messages from being received. They will still be logged - for admins and on the in-game message server console.} -delete-after: true From aa1569a3904a5299baf68f8cc425a379560abc1f Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Sat, 20 Dec 2025 20:21:57 -0500 Subject: [PATCH 32/79] Make breaker boxes no longer a power subtype --- .../items/circuitboards/machinery/power.dm | 2 +- .../programs/engineering/rcon_console.dm | 12 ++++---- code/modules/power/breaker_box.dm | 28 +++++++++---------- code/modules/power/cable/cable.dm | 2 +- maps/away/bearcat/bearcat-2.dmm | 2 +- maps/away/derelict/derelict-station.dmm | 2 +- maps/exodus/exodus-2.dmm | 14 +++++----- maps/tradeship/tradeship-2.dmm | 2 +- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/code/game/objects/items/circuitboards/machinery/power.dm b/code/game/objects/items/circuitboards/machinery/power.dm index d8cdc9faab6..7bb30358f80 100644 --- a/code/game/objects/items/circuitboards/machinery/power.dm +++ b/code/game/objects/items/circuitboards/machinery/power.dm @@ -29,7 +29,7 @@ /obj/item/stock_parts/circuitboard/breaker name = "circuitboard (breaker box)" - build_path = /obj/machinery/power/breakerbox + build_path = /obj/machinery/breakerbox board_type = "machine" origin_tech = @'{"powerstorage":4,"engineering":4}' req_components = list( diff --git a/code/modules/modular_computers/file_system/programs/engineering/rcon_console.dm b/code/modules/modular_computers/file_system/programs/engineering/rcon_console.dm index 224d0cdccc5..ca6513aba20 100644 --- a/code/modules/modular_computers/file_system/programs/engineering/rcon_console.dm +++ b/code/modules/modular_computers/file_system/programs/engineering/rcon_console.dm @@ -44,7 +44,7 @@ // BREAKER DATA (simplified view) var/list/breakerlist[0] - for(var/obj/machinery/power/breakerbox/BR in known_breakers) + for(var/obj/machinery/breakerbox/BR in known_breakers) breakerlist.Add(list(list( "RCON_tag" = BR.RCon_tag, "enabled" = BR.on @@ -90,8 +90,8 @@ SMES.set_output(outputset) if(href_list["toggle_breaker"]) - var/obj/machinery/power/breakerbox/toggle = null - for(var/obj/machinery/power/breakerbox/breaker in known_breakers) + var/obj/machinery/breakerbox/toggle = null + for(var/obj/machinery/breakerbox/breaker in known_breakers) if(breaker.RCon_tag == href_list["toggle_breaker"]) toggle = breaker if(toggle) @@ -129,7 +129,7 @@ known_SMESs = sortTim(known_SMESs, /proc/cmp_rcon_tag_asc) known_breakers = new /list() - for(var/obj/machinery/power/breakerbox/breaker in SSmachines.machinery) + for(var/obj/machinery/breakerbox/breaker in SSmachines.machinery) if(can_connect_to(breaker)) known_breakers.Add(breaker) @@ -145,6 +145,6 @@ var/obj/machinery/power/smes/buildable/SMES = M return SMES.RCon_tag && SMES.RCon_tag != "NO_TAG" && SMES.RCon - if(istype(M, /obj/machinery/power/breakerbox)) - var/obj/machinery/power/breakerbox/breaker = M + if(istype(M, /obj/machinery/breakerbox)) + var/obj/machinery/breakerbox/breaker = M return breaker.RCon_tag != "NO_TAG" \ No newline at end of file diff --git a/code/modules/power/breaker_box.dm b/code/modules/power/breaker_box.dm index 2cd7d48af02..e7eb827fd59 100644 --- a/code/modules/power/breaker_box.dm +++ b/code/modules/power/breaker_box.dm @@ -3,7 +3,7 @@ // Requires 5 seconds to toggle and can be toggled once a minute // Used for advanced grid control (read: Substations) -/obj/machinery/power/breakerbox +/obj/machinery/breakerbox name = "breaker box" icon = 'icons/obj/power.dmi' icon_state = "bbox_off" @@ -13,7 +13,7 @@ construct_state = /decl/machine_construction/default/panel_closed stat_immune = 0 uncreated_component_parts = null - base_type = /obj/machinery/power/breakerbox + base_type = /obj/machinery/breakerbox var/icon_state_on = "bbox_on" var/icon_state_off = "bbox_off" @@ -23,27 +23,27 @@ /// If world.time < lock_time, system is locked for interactions. var/lock_time = 0 -/obj/machinery/power/breakerbox/activated +/obj/machinery/breakerbox/activated icon_state = parent_type::icon_state_on // Enabled on server startup. Used in substations to keep them in bypass mode. -/obj/machinery/power/breakerbox/activated/Initialize() +/obj/machinery/breakerbox/activated/Initialize() ..() return INITIALIZE_HINT_LATELOAD -/obj/machinery/power/breakerbox/activated/LateInitialize() +/obj/machinery/breakerbox/activated/LateInitialize() set_state(TRUE) . = ..() -/obj/machinery/power/breakerbox/get_examine_strings(mob/user, distance, infix, suffix) +/obj/machinery/breakerbox/get_examine_strings(mob/user, distance, infix, suffix) . = ..() if(on) . += SPAN_GOOD("It seems to be online.") else . += SPAN_WARNING("It seems to be offline.") -/obj/machinery/power/breakerbox/proc/try_toggle_state(mob/living/user, digital = FALSE) - if(lock_time < world.time) +/obj/machinery/breakerbox/proc/try_toggle_state(mob/living/user, digital = FALSE) + if(world.time < lock_time) // maybe rename this unlock_time to make it clearer it's the time it unlocks at to_chat(user, SPAN_WARNING("System locked. Please try again later.")) return TRUE @@ -68,13 +68,13 @@ busy = FALSE return TRUE -/obj/machinery/power/breakerbox/attack_ai(mob/living/silicon/ai/user) +/obj/machinery/breakerbox/attack_ai(mob/living/silicon/ai/user) return try_toggle_state(user, digital = TRUE) -/obj/machinery/power/breakerbox/physical_attack_hand(mob/user) +/obj/machinery/breakerbox/physical_attack_hand(mob/user) return try_toggle_state(user, digital = FALSE) -/obj/machinery/power/breakerbox/attackby(obj/item/used_item, mob/user) +/obj/machinery/breakerbox/attackby(obj/item/used_item, mob/user) if(IS_MULTITOOL(used_item)) var/newtag = input(user, "Enter new RCON tag. Use \"NO_TAG\" to disable RCON or leave empty to cancel.", "SMES RCON system") as text if(!CanPhysicallyInteract(user)) @@ -85,11 +85,11 @@ return TRUE return ..() -/obj/machinery/power/breakerbox/on_update_icon() +/obj/machinery/breakerbox/on_update_icon() . = ..() icon_state = on ? icon_state_on : icon_state_off -/obj/machinery/power/breakerbox/proc/set_state(state) +/obj/machinery/breakerbox/proc/set_state(state) on = state update_icon() if(on) @@ -121,7 +121,7 @@ qdel(C) // Used by RCON to toggle the breaker box. -/obj/machinery/power/breakerbox/proc/auto_toggle() +/obj/machinery/breakerbox/proc/auto_toggle() if(lock_time > world.time) return FALSE // still on cooldown set_state(!on) diff --git a/code/modules/power/cable/cable.dm b/code/modules/power/cable/cable.dm index 00cc9a0e2d8..1b39dda6262 100644 --- a/code/modules/power/cable/cable.dm +++ b/code/modules/power/cable/cable.dm @@ -44,7 +44,7 @@ var/global/list/obj/structure/cable/all_cables = list() var/d1 var/d2 var/datum/powernet/powernet - var/obj/machinery/power/breakerbox/breaker_box + var/obj/machinery/breakerbox/breaker_box /obj/structure/cable/drain_power(var/drain_check, var/surge, var/amount = 0) diff --git a/maps/away/bearcat/bearcat-2.dmm b/maps/away/bearcat/bearcat-2.dmm index 64fa7037ab3..d661d6b5134 100644 --- a/maps/away/bearcat/bearcat-2.dmm +++ b/maps/away/bearcat/bearcat-2.dmm @@ -4191,7 +4191,7 @@ /turf/floor/usedup, /area/ship/scrap/maintenance/power) "ib" = ( -/obj/machinery/power/breakerbox/activated, +/obj/machinery/breakerbox/activated, /obj/structure/cable{ icon_state = "1-2" }, diff --git a/maps/away/derelict/derelict-station.dmm b/maps/away/derelict/derelict-station.dmm index 46a25943bbf..4f933e1973d 100644 --- a/maps/away/derelict/derelict-station.dmm +++ b/maps/away/derelict/derelict-station.dmm @@ -2916,7 +2916,7 @@ /turf/floor/tiled/dark/airless, /area/constructionsite) "kt" = ( -/obj/machinery/power/breakerbox, +/obj/machinery/breakerbox, /turf/floor/plating/airless, /area/constructionsite) "ku" = ( diff --git a/maps/exodus/exodus-2.dmm b/maps/exodus/exodus-2.dmm index b7aebcc8585..931f8121119 100644 --- a/maps/exodus/exodus-2.dmm +++ b/maps/exodus/exodus-2.dmm @@ -6631,7 +6631,7 @@ /turf/wall/prepainted, /area/exodus/maintenance/substation/security) "anR" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Security Substation Bypass" }, /turf/floor/plating, @@ -13766,7 +13766,7 @@ /turf/floor/plating, /area/exodus/maintenance/substation/civilian_east) "aDv" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Civilian East Substation Bypass" }, /turf/floor/plating, @@ -16993,7 +16993,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{ dir = 9 }, -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Civilian West Substation Bypass" }, /turf/floor/plating, @@ -24402,7 +24402,7 @@ /turf/floor/plating, /area/exodus/maintenance/locker) "bay" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Medical Substation Bypass" }, /turf/floor/plating, @@ -32916,7 +32916,7 @@ /turf/floor/tiled/white, /area/exodus/medical/exam_room) "bsa" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Command Substation Bypass" }, /obj/machinery/light, @@ -51093,7 +51093,7 @@ /turf/floor/plating, /area/exodus/maintenance/cargo) "ccA" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Research Substation Bypass" }, /turf/floor/plating, @@ -53849,7 +53849,7 @@ /turf/floor/tiled/white/monotile, /area/exodus/medical/surgery2) "cib" = ( -/obj/machinery/power/breakerbox/activated{ +/obj/machinery/breakerbox/activated{ RCon_tag = "Engineering Substation Bypass" }, /turf/floor/plating, diff --git a/maps/tradeship/tradeship-2.dmm b/maps/tradeship/tradeship-2.dmm index 765635bb94f..63c009143c5 100644 --- a/maps/tradeship/tradeship-2.dmm +++ b/maps/tradeship/tradeship-2.dmm @@ -8057,7 +8057,7 @@ /turf/wall/titanium, /area/ship/trade/shuttle/rescue) "Za" = ( -/obj/machinery/power/breakerbox/activated, +/obj/machinery/breakerbox/activated, /obj/structure/cable{ icon_state = "1-2" }, From 3373f630c715d56a2f34831601d490224c01f43a Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Fri, 26 Jun 2026 21:09:52 -0400 Subject: [PATCH 33/79] Add map migration for breakerbox repath --- tools/map_migrations/5229_breakerbox.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 tools/map_migrations/5229_breakerbox.txt diff --git a/tools/map_migrations/5229_breakerbox.txt b/tools/map_migrations/5229_breakerbox.txt new file mode 100644 index 00000000000..74f298b033c --- /dev/null +++ b/tools/map_migrations/5229_breakerbox.txt @@ -0,0 +1 @@ +/obj/machinery/power/breakerbox/@SUBTYPES : /obj/machinery/breakerbox{@OLD} \ No newline at end of file From ab2f6f04c8668266e235fe4ac0ff0b4e572e004b Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sat, 27 Jun 2026 16:02:54 -0500 Subject: [PATCH 34/79] lighting: Rewrite corner z-bleed to explicitly build a stack of corners --- code/__defines/lighting.dm | 5 + code/game/turfs/turf_changing.dm | 2 +- code/modules/lighting/ambient_turf.dm | 17 +- code/modules/lighting/lighting_corner.dm | 366 +++++++++++++---------- code/modules/lighting/lighting_source.dm | 4 +- code/modules/lighting/lighting_turf.dm | 3 +- 6 files changed, 230 insertions(+), 167 deletions(-) diff --git a/code/__defines/lighting.dm b/code/__defines/lighting.dm index d9c934a0fb5..6e0c142608a 100644 --- a/code/__defines/lighting.dm +++ b/code/__defines/lighting.dm @@ -25,6 +25,11 @@ #define TURF_IS_AMBIENT_LIT_UNSAFE(T) (T:ambient_active) #define TURF_IS_AMBIENT_LIT(T) (isturf(T) && TURF_IS_AMBIENT_LIT_UNSAFE(T)) +// The relation of these is important. +#define LIGHTING_CORNER_GENERATE_UP 1 +#define LIGHTING_CORNER_GENERATE_BOTH 0 +#define LIGHTING_CORNER_GENERATE_DOWN -1 + // If I were you I'd leave this alone. #define LIGHTING_BASE_MATRIX \ list \ diff --git a/code/game/turfs/turf_changing.dm b/code/game/turfs/turf_changing.dm index 0dfa901cf98..2b5d18ba136 100644 --- a/code/game/turfs/turf_changing.dm +++ b/code/game/turfs/turf_changing.dm @@ -152,7 +152,7 @@ var/new_z_opacity = z_flags & ZM_ALLOW_LIGHTING if (new_z_opacity != old_z_opacity) for (var/datum/lighting_corner/corn in corners) - corn.rebuild_ztraversal(!new_z_opacity) + corn.generate_z_connections() var/tidlu = TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) if ((old_opacity != opacity) || (tidlu != old_dynamic_lighting) || force_lighting_update) diff --git a/code/modules/lighting/ambient_turf.dm b/code/modules/lighting/ambient_turf.dm index 60992f1fcb5..aeceed3dbd6 100644 --- a/code/modules/lighting/ambient_turf.dm +++ b/code/modules/lighting/ambient_turf.dm @@ -18,7 +18,11 @@ ambient_light = isnull(color) ? ambient_light : color ambient_light_multiplier = isnull(multiplier) ? ambient_light_multiplier : multiplier - update_ambient_light() + // If we haven't initialized our corners yet, do that instead to avoid ambience double-init. + if (!corners || !lighting_corners_initialised) + generate_missing_corners() + else + update_ambient_light() /// Replace one ambient light with another. This is effectively a delta update, but it can be used to pretend that our one channel is doing color blending. /turf/proc/replace_ambient_light(old_color, new_color, old_multiplier, new_multiplier = 0) @@ -79,13 +83,12 @@ ambient_light_old_g += lg ambient_light_old_b += lb - if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(src)) - if (!corners || !lighting_corners_initialised) - generate_missing_corners() + if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) && (!corners || !lighting_corners_initialised)) + generate_missing_corners() - // This list can contain nulls on things like space turfs -- they only have their neighbors' corners. - for (var/datum/lighting_corner/C in corners) - C.update_ambient_lumcount(lr, lg, lb, !update) + // This list can contain nulls on things like space turfs -- they only have their neighbors' corners. + for (var/datum/lighting_corner/C in corners) + C.update_ambient_lumcount(lr, lg, lb, !update) if (!ambient_active) SSlighting.total_ambient_turfs += 1 diff --git a/code/modules/lighting/lighting_corner.dm b/code/modules/lighting/lighting_corner.dm index ea6f99e9a91..f131ac2983b 100644 --- a/code/modules/lighting/lighting_corner.dm +++ b/code/modules/lighting/lighting_corner.dm @@ -22,6 +22,11 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, var/turf/t4 var/t4i + /// If a connection for z-lights exists, the corner above us. + var/datum/lighting_corner/above_corner + /// If a connection for z-lights exists, the corner below us. + var/datum/lighting_corner/below_corner + var/list/datum/light_source/affecting // Light sources affecting us. var/active = FALSE // TRUE if one of our masters has dynamic lighting. @@ -34,7 +39,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, var/self_g = 0 var/self_b = 0 - // The intensity we're inheriting from the turf below us, if we're a Z-turf. This is a sum of all below turfs in the Z-stack. + // The intensity we're inheriting from the turfs below us, if we're a Z-turf. This is a sum of all below turfs. var/below_r = 0 var/below_g = 0 var/below_b = 0 @@ -44,7 +49,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, var/ambient_g = 0 var/ambient_b = 0 - // The turf above us' ambient + // The turf above us' ambient values. var/above_ambient_r = 0 var/above_ambient_g = 0 var/above_ambient_b = 0 @@ -61,7 +66,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, var/cache_b = 0 var/cache_mx = 0 -/datum/lighting_corner/New(turf/new_turf, diagonal, oi) +/datum/lighting_corner/New(turf/new_turf, diagonal, oi, direction = LIGHTING_CORNER_GENERATE_BOTH) SSlighting.total_lighting_corners += 1 var/has_ambience = FALSE @@ -125,9 +130,10 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, if (TURF_IS_AMBIENT_LIT_UNSAFE(T)) has_ambience = TRUE - update_active() if (has_ambience) init_ambient() + generate_z_connections(direction) + update_active() #define OVERLAY_PRESENT(T) (T && T.lighting_overlay) @@ -144,6 +150,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, #define UPDATE_APPARENT(T, CH) T.apparent_##CH = T.self_##CH + T.below_##CH + T.ambient_##CH + T.above_ambient_##CH +// Configure ambient lighting for *just* this corner. This deliberately does not handle Z-propagation, that's managed by generate_z_connections(). /datum/lighting_corner/proc/init_ambient() var/sum_r = 0 var/sum_g = 0 @@ -171,7 +178,182 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, sum_g /= 4 sum_b /= 4 - update_ambient_lumcount(sum_r, sum_g, sum_b) + ambient_r += sum_r + ambient_g += sum_g + ambient_b += sum_b + + UPDATE_APPARENT(src, r) + UPDATE_APPARENT(src, g) + UPDATE_APPARENT(src, b) + + if (!needs_update) + needs_update = TRUE + SSlighting.corner_queue += src + +/datum/lighting_corner/proc/generate_z_connections(direction = LIGHTING_CORNER_GENERATE_BOTH) + /* + ZM_ALLOW_LIGHTING means that a z-turf is lighting-connected to the turf below it. + So: + Upward: check if above and above is ALLOW_LIGHTING + Downward: check if self is ALLOW_LIGHTING and below + + This corner will be shared by all four of its turfs, so it doesn't matter which condition passes. + The above/below corners should be created iff one of the masters is considered dynamically lit, including dynamic promotion. No other condition matters, and light is not + allowed to transmit through a static lit turf, even if it is marked as ZM_ALLOW_LIGHTING. + */ + + // BOTH is 0, so it's true for both conditions. + // Sometimes we need to only generate going upwards (or downwards); if this corner was created by another corner using this proc, then generating downward is invalid and causes infinite recursion. + // We still need to scan downward (or upward) to find the new connection in this case though. + #define GOING_UP (direction > LIGHTING_CORNER_GENERATE_DOWN) + #define GOING_DOWN (direction < LIGHTING_CORNER_GENERATE_UP) + + var/turf/T + + var/datum/lighting_corner/old_above_corner = above_corner + var/datum/lighting_corner/old_below_corner = below_corner + + /* + This brick is responsible for finding the corner that's directly above us, and forcibly generating the corner if it doesn't exist yet. + It's just the same block of code repeated four times (for each master), plus the case of there now being no above corner, but previously having had one. + We also only initialize the one corner we need rather than all four since there's no benefit to initializing them all -- if a true light needs them, it'll make them itself. + */ + if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(above_corner = T.corners?[t1i]) && GOING_UP) + if (!T.corners) + T.corners = new(4) + T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_UP) + above_corner = T.corners[t1i] + else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(above_corner = T.corners?[t2i]) && GOING_UP) + if (!T.corners) + T.corners = new(4) + T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_UP) + above_corner = T.corners[t2i] + else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(above_corner = T.corners?[t3i]) && GOING_UP) + if (!T.corners) + T.corners = new(4) + T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_UP) + above_corner = T.corners[t3i] + else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(above_corner = T.corners?[t4i]) && GOING_UP) + if (!T.corners) + T.corners = new(4) + T.corners[t4i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t4i], t4i, LIGHTING_CORNER_GENERATE_UP) + above_corner = T.corners[t4i] + else if (above_corner) // connected -> disconnected transition + /* + The corner directly above us contains the sum of the light that comes from us and everything below us, which is conveniently everything that we need to remove. + We iterate up through the stack removing only the above_corner's light contribution, as to not disturb light sourced from other turfs higher in the stack. + */ + for (var/datum/lighting_corner/corn = above_corner; corn; corn = corn.above_corner) + corn.below_r -= above_corner.below_r + corn.below_g -= above_corner.below_g + corn.below_b -= above_corner.below_b + + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) + + if (!corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn + + above_corner.below_corner = null + above_corner = null + + if (!old_above_corner && above_corner) // disconnected -> connected transition + if (!(apparent_r == apparent_g == apparent_b == 0)) + for (var/datum/lighting_corner/corn = above_corner; corn; corn = corn.above_corner) + // We can't just steal the precomputed value from the above like we can in the removal case: our effect on the turf above us is our own self-light plus the light below us. + corn.below_r += src.below_r + src.self_r + corn.below_g += src.below_g + src.self_g + corn.below_b += src.below_b + src.self_b + + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) + + if (!corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn + + // As above, so below. The ordering here is a bit different from the above block, check the comment at the top of this proc. + if ((t1?.z_flags & ZM_ALLOW_LIGHTING) && (T = t1.below || GET_BELOW(t1)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(below_corner = T.corners?[t1i]) && GOING_DOWN) + if (!T.corners) + T.corners = new(4) + T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_DOWN) + below_corner = T.corners[t1i] + else if ((t2?.z_flags & ZM_ALLOW_LIGHTING) && (T = t2.below || GET_BELOW(t2)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(below_corner = T.corners?[t2i]) && GOING_DOWN) + if (!T.corners) + T.corners = new(4) + T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_DOWN) + below_corner = T.corners[t2i] + else if ((t3?.z_flags & ZM_ALLOW_LIGHTING) && (T = t3.below || GET_BELOW(t3)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(below_corner = T.corners?[t3i]) && GOING_DOWN) + if (!T.corners) + T.corners = new(4) + T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_DOWN) + below_corner = T.corners[t3i] + else if ((t4?.z_flags & ZM_ALLOW_LIGHTING) && (T = t4.below || GET_BELOW(t4)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (!(below_corner = T.corners?[t4i]) && GOING_DOWN) + if (!T.corners) + T.corners = new(4) + T.corners[t4i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t4i], t4i, LIGHTING_CORNER_GENERATE_DOWN) + below_corner = T.corners[t4i] + else if (below_corner) // connected -> disconnected transition + /* + Similar case to above, but not quite the same. + The corner below us' `above_ambient_*` var contins both our contributed light as well as turfs above us, so just subtract that instead of combining both vars manually. + */ + for (var/datum/lighting_corner/corn = below_corner; corn; corn = corn.below_corner) + corn.above_ambient_r -= below_corner.above_ambient_r + corn.above_ambient_g -= below_corner.above_ambient_g + corn.above_ambient_b -= below_corner.above_ambient_b + + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) + + if (!corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn + + below_corner.above_corner = null + below_corner = null + + if (!old_below_corner && below_corner) // disconnected -> connected transition + // quick and dirty heuristic to avoid checking a bunch of different vars, it's still valid if we needlessly run this + if (!(apparent_r == apparent_g == apparent_b == 0)) + for (var/datum/lighting_corner/corn = below_corner; corn; corn = corn.below_corner) + // As with above, we can't just steal the precomputed value from our neighbor. Our effect will be the sum effect of turfs above us, plus our effect. + corn.above_ambient_r += src.above_ambient_r + src.ambient_r + corn.above_ambient_g += src.above_ambient_g + src.ambient_g + corn.above_ambient_b += src.above_ambient_b + src.ambient_b + + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) + + if (!corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn + + if (above_corner) + ASSERT(x == above_corner.x) + ASSERT(y == above_corner.y) + ASSERT(z == above_corner.z - 1) + + if (below_corner) + ASSERT(x == below_corner.x) + ASSERT(y == below_corner.y) + ASSERT(z == below_corner.z + 1) + +#undef GOING_UP +#undef GOING_DOWN // God that was a mess, now to do the rest of the corner code! Hooray! /datum/lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b, now = FALSE) @@ -186,29 +368,19 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) - var/turf/T - var/Ti - // Grab the first master that's a Z-turf, if one exists. - // The above var cannot be relied on due to init ordering, but we can use it if it is set. - if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = t1i - else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = t2i - else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = t3i - else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = t4i - else // Nothing above us that cares about below light. - T = null - - if (TURF_IS_DYNAMICALLY_LIT(T)) - do - if (!T.corners || !T.corners[Ti]) - T.generate_missing_corners() - - // Above corners never get instant updates; they're less important, so better to avoid risk of lag. - T.corners[Ti].update_below_lumcount(delta_r, delta_g, delta_b) - while ((T = T.above) && (T.z_flags & ZM_ALLOW_LIGHTING)) + for (var/datum/lighting_corner/corn = above_corner; corn; corn = corn.above_corner) + corn.below_r += delta_r + corn.below_g += delta_g + corn.below_b += delta_b + + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) + + // These are always queued, players are far less likely to notice these being a little behind. + if (!corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn // This needs to be down here instead of the above if so the lum values are properly updated. if (needs_update) @@ -220,25 +392,6 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, needs_update = TRUE SSlighting.corner_queue += src -/datum/lighting_corner/proc/update_below_lumcount(delta_r, delta_g, delta_b) - if (!(delta_r + delta_g + delta_b)) - return - - below_r += delta_r - below_g += delta_g - below_b += delta_b - - UPDATE_APPARENT(src, r) - UPDATE_APPARENT(src, g) - UPDATE_APPARENT(src, b) - - // This needs to be down here instead of the above if so the lum values are properly updated. - if (needs_update) - return - - needs_update = TRUE - SSlighting.corner_queue += src - /datum/lighting_corner/proc/update_ambient_lumcount(delta_r, delta_g, delta_b, skip_update = FALSE) ambient_r += delta_r @@ -249,46 +402,18 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) - var/turf/T - var/Ti - - if (t1) - T = t1 - Ti = t1i - else if (t2) - T = t2 - Ti = t2i - else if (t3) - T = t3 - Ti = t3i - else if (t4) - T = t4 - Ti = t4i - else - // This should be impossible to reach -- how do we exist without at least one master turf? - CRASH("Corner has no masters!") - - var/datum/lighting_corner/below = src - - // We init before Z-Mimic, cannot rely on above/below. - while ((T = GET_BELOW(T)) && ((below.t1?.z_flags | below.t2?.z_flags | below.t3?.z_flags | below.t4?.z_flags) & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) - if (!T.corners || !T.corners[Ti]) - T.generate_missing_corners() + for (var/datum/lighting_corner/corn = below_corner; corn; corn = corn.below_corner) + corn.above_ambient_r += delta_r + corn.above_ambient_g += delta_g + corn.above_ambient_b += delta_b - ASSERT(T.corners?.len) + UPDATE_APPARENT(corn, r) + UPDATE_APPARENT(corn, g) + UPDATE_APPARENT(corn, b) - below = T.corners[Ti] - below.above_ambient_r += delta_r - below.above_ambient_g += delta_g - below.above_ambient_b += delta_b - - UPDATE_APPARENT(below, r) - UPDATE_APPARENT(below, g) - UPDATE_APPARENT(below, b) - - if (!skip_update && !below.needs_update) - below.needs_update = TRUE - SSlighting.corner_queue += below + if (!skip_update && !corn.needs_update) + corn.needs_update = TRUE + SSlighting.corner_queue += corn if (needs_update || skip_update) return @@ -302,7 +427,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, var/lg = apparent_g var/lb = apparent_b - // Cache these values a head of time so 4 individual lighting overlays don't all calculate them individually. + // Cache these values ahead of time so 4 individual lighting overlays don't all calculate them individually. var/mx = max(lr, lg, lb) // Scale it so 1 is the strongest lum, if it is above 1. . = 1 // factor if (mx > 1) @@ -331,75 +456,6 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, Ov.needs_update = TRUE SSlighting.overlay_queue += Ov -// This is called when our turf's downward Z-opacity changes. -/datum/lighting_corner/proc/rebuild_ztraversal(new_opacity) - /* - If opacity transitions to off: - - Look down stack, removing below lights contributing to us - - Look up stack, updating turfs with our new below lighting value - If opacity transitions to on: - - Look down stack, add below contributors - - Look up stack, updating turfs with our new below lighting value - */ - below_r = below_g = below_b = 0 - var/turf/T = null - var/datum/lighting_corner/Tcorn = src - var/Ti - - if (!new_opacity) - for (;;) - if (Tcorn.t1 && (T = Tcorn.t1.below || GET_BELOW(Tcorn.t1)) && (T.above?.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t1i - else if (Tcorn.t2 && (T = Tcorn.t2.below || GET_BELOW(Tcorn.t2)) && (T.above?.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t2i - else if (Tcorn.t3 && (T = Tcorn.t3.below || GET_BELOW(Tcorn.t3)) && (T.above?.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t3i - else if (Tcorn.t4 && (T = Tcorn.t4.below || GET_BELOW(Tcorn.t4)) && (T.above?.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t4i - else // Nothing above us that cares about below light. - break - - Tcorn = T.corners[Ti] - below_r += Tcorn.apparent_r - below_g += Tcorn.apparent_g - below_b += Tcorn.apparent_b - - UPDATE_APPARENT(src, r) - UPDATE_APPARENT(src, g) - UPDATE_APPARENT(src, b) - - if (!needs_update) - needs_update = TRUE - SSlighting.corner_queue += src - - T = null - Tcorn = src - - for (;;) - if (Tcorn.t1 && (T = Tcorn.t1.above || GET_ABOVE(Tcorn.t1)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t1i - else if (Tcorn.t2 && (T = Tcorn.t2.above || GET_ABOVE(Tcorn.t2)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t2i - else if (Tcorn.t3 && (T = Tcorn.t3.above || GET_ABOVE(Tcorn.t3)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t3i - else if (Tcorn.t4 && (T = Tcorn.t4.above || GET_ABOVE(Tcorn.t4)) && (T.z_flags & ZM_ALLOW_LIGHTING)) - Ti = Tcorn.t4i - else // Nothing above us that cares about below light. - break - - Tcorn = T.corners[Ti] - Tcorn.below_r += apparent_r - Tcorn.below_g += apparent_g - Tcorn.below_b += apparent_b - - UPDATE_APPARENT(Tcorn, r) - UPDATE_APPARENT(Tcorn, g) - UPDATE_APPARENT(Tcorn, b) - - if (!Tcorn.needs_update) - Tcorn.needs_update = TRUE - SSlighting.corner_queue += Tcorn - /datum/lighting_corner/Destroy(force = FALSE) PRINT_STACK_TRACE("Someone [force ? "force-" : ""]deleted a lighting corner.") if (!force) diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm index 84bccc89757..550a467ba8e 100644 --- a/code/modules/lighting/lighting_source.dm +++ b/code/modules/lighting/lighting_source.dm @@ -358,8 +358,8 @@ continue Tcorners = T.corners - // These checks are inlined from generate_missing_corners. They must be kept in sync. - if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(T) || T.light_source_solo || T.light_source_multi || (T.z_flags & ZM_ALLOW_LIGHTING)) + // These checks are inlined from generate_missing_corners. They must be kept (roughly) in sync. This one intentionally does not check for ambient turfs. + if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) if (!T.lighting_corners_initialised) T.lighting_corners_initialised = TRUE diff --git a/code/modules/lighting/lighting_turf.dm b/code/modules/lighting/lighting_turf.dm index 5543d1166c1..f0713689d33 100644 --- a/code/modules/lighting/lighting_turf.dm +++ b/code/modules/lighting/lighting_turf.dm @@ -143,8 +143,7 @@ // This is inlined in lighting_source.dm. // Update it too if you change this. /turf/proc/generate_missing_corners() - // If a turf is dynamically lit, has a light source, or mimics lighting, it needs to have corners created. - if (!TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) && !light_source_solo && !light_source_multi && !(z_flags & ZM_ALLOW_LIGHTING)) + if (!TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) && !ambient_light) return lighting_corners_initialised = TRUE From 0def104cdc28c05f3fe76d6b5d29f3acc58ec790 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 29 Jun 2026 11:04:48 -0400 Subject: [PATCH 35/79] Add "overriding core code" section to modpack readme --- mods/README.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/mods/README.md b/mods/README.md index c6b7a62b11f..7c73056d41b 100644 --- a/mods/README.md +++ b/mods/README.md @@ -38,8 +38,103 @@ Modpacks have a defined, user-controlled load order, and cross-modpack compatibi ### Enabling Your Modpack Modpacks are enabled on a per-map basis. To activate a modpack, you `#include` the modpack's .dme in a map's .dme file. -### Overriding Stock Code -TODO: Actually write this section. It's distinct from the "How do I write upstream/core code with extension via modpacks in mind?" part because it's the *opposite,* this section is about overriding core code while the other section is about writing core code to be extended. Maybe do a basic explanation of side-overrides and associated footguns here. +### Overriding Core Code +Sometimes a modpack needs to change how existing non-modpack code behaves, rather than just add new content. Because DM lets you extend any type in any file, a modpack can re-declare an existing type and redefine its vars or procs. This is called a *side-override*: normal overrides are created deeper in the type hierarchy on a subtype, while side-overrides exist 'to the side' of the existing override(s) for a type. (Even though it's not on a parent- or child-type, we still call `..()` the "parent call" even inside a side-override.) + +This is the *opposite* of the approach described in "How do I write upstream/core code with extension via modpacks in mind?" below. That section is about writing core code so modpacks can hook into it without touching it; this section is about the cases where you have to touch it anyway. Prefer the extension approach when stock code already offers a hook (a decl subtype to add, a list to append to, a subtype to iterate over). Reach for a side-override only when there's no such entry point. + +By convention, overrides go in a file named `overrides.dm` (or `_overrides.dm` for a focused group, e.g. `living_overrides.dm`) and are `#include`d from the modpack's `.dme` like any other file. Keeping them in clearly-named files makes it obvious at a glance which stock behavior a modpack changes. Cleverly-designed modpacks will define their core code hooks/overrides separate from per-type value overrides, so they can change as little as possible for each type, making changes less brittle. + +#### Overriding a var +The simplest type of override just extends an existing type and changes variable values: + +```dm +// mods/content/fantasy/items/material_overrides.dm +// FRANCE ISN'T REAL +/obj/item/chems/drinks/bottle/champagne + name = "sparkling wine bottle" + +/decl/material/liquid/alcohol/champagne + name = "sparkling wine" + glass_name = "sparkling wine" + glass_desc = "Sparkling white wine, a favourite at noble and merchant parties." + lore_text = "Sparkling white wine, a favourite at noble and merchant parties." +``` + +This is safe and done entirely at compile-time without adding any new code; it just changes the initial value of vars that the existing type already declares. The only conflict risk is two modpacks setting the same var on the same type to different values, in which case the last one loaded wins. Some modpacks may intend this, while others may want to write a compatibility patch (see below). + +#### Overriding a proc +To change behavior, redefine the proc on the existing type. Most overrides should call `..()` so the stock implementation (and any other modpack's override of it) still runs: + +```dm +// mods/content/augments/passive/armor.dm +// override to add armor augment damage mods +/obj/item/organ/external/get_brute_mod(var/damage_flags) + . = ..() // run the stock proc, keep its result + var/obj/item/organ/internal/augment/armor/armor_augment = owner?.get_organ(BP_AUGMENT_CHEST_ARMOUR, /obj/item/organ/internal/augment/armor) + if(armor_augment) + . *= armor_augment.brute_mult +``` + +You can call `..()` at the start (to modify the result afterward), at the end (to run your logic first), or conditionally (to sometimes short-circuit and sometimes defer to stock): + +```dm +// mods/content/breath_holding/living_overrides.dm +// override to make a held breath take priority +/mob/living/get_breath(obj/item/organ/internal/lungs/lungs) + if(lungs?.holding_breath && lungs.held_breath) + return lungs.held_breath // intentionally skip the stock proc + return ..() +``` + +#### Overriding a static list getter (the injector pattern) +One common pattern in core code is the *static list getter,* used to avoid creating a new list every time the getter is called. This is much more efficient, but is a little more complex to override. Take this getter, for example: + +```dm +// code/game/objects/items/weapons/secrets_disk.dm +/obj/item/disk/secret_project/proc/get_secret_project_nouns() + var/static/list/nouns = list( + "a superluminal artillery cannon", "a fusion engine", "an atmospheric scrubber",\ + "a human cloning pod", "a microwave oven", "a wormhole generator", "a laser carbine", "an energy pistol",\ + "a wormhole", "a teleporter", "a huge mining drill", "a strange spacecraft", "a space station",\ + "a sleek-looking fighter spacecraft", "a ballistic rifle", "an energy sword", "an inanimate carbon rod" + ) + return nouns +``` + +We want to extend this by adding "a supermatter engine" to the list. A naive approach might be like this: + +```dm +//Example code not actually used +/obj/item/disk/secret_project/get_secret_project_nouns() + . = ..() + . += "a supermatter engine" +``` + +This works at first glance, if you call it once. However, because the getter uses a *static list,* it's saved between calls. That means it will be added every time we use the getter, which will quickly add a lot of duplicate entries to the list. Another naive fix for this would be using `|=` to avoid duplicates, but this is expensive because it checks if the item already exists in the list. Wouldn't it be nice to just add it once? + +For this, we use something called an injector, which uses a static var to track whether or not we've run our override before. If we're running it for the first time, we make all our changes to the static list returned by `..()`, and after that we set our tracking variable to ensure we never modify it again: + +```dm +// mods/content/supermatter/overrides/sm_strings.dm +/obj/item/disk/secret_project/get_secret_project_nouns() + var/static/sm_injected = FALSE + if(sm_injected) + return ..() + sm_injected = TRUE + . = ..() + . += "a supermatter engine" + return . +``` + +This also works for removing items from static lists, and may be useful for run-once code in other contexts as well. Another good example is in mods/content/corporate/items/random.dm. + +#### Footguns +- **Multiple side-overrides chain through `..()` in definition order.** Unlike a normal override, which lives on a new subtype deeper in the type tree, a side-override is defined directly on the existing type. If there are several side-overrides of `/mob/living/some_proc()`, they're all kept and chained: `..()` in the last-defined override calls the previous one, and so on down to the first, which then walks up the type tree to the base implementation. The order in which they run depends on the order they're defined in, so don't write a side-override that assumes it runs first, last, or in any particular position relative to another modpack's. (You can generally assume that it will run after the core definition, though.) +- **Extend, don't copy.** It may be easier to copy an existing proc definition, skip the parent call, and make a change somewhere in the middle. This is (almost) always a horrible idea, because you may not even notice something breaks when an update changes the definition you copied. The correct solution is to add it to an override that runs before or after the parent call, and if you *really* need to run it in the middle, consider adding a proc to the core code that your modpack can override (or split the existing proc into two or more). +- **Always call `..()` unless you really mean to break the chain.** Forgetting `..()` drops the base implementation *and* any earlier modpack's side-override, which can break unrelated core features and any other modpack that expected that proc to do its normal job. Only omit it when you genuinely intend to replace the behavior wholesale. +- **Don't depend on load order between modpacks.** A modpack may depend only on itself and stock code. If your override only makes sense when *another* modpack is also enabled, it's a cross-modpack interaction and belongs in `mods/~compatibility` (see below), which is loaded last and therefore modpack load-order agnostic. +- **Side-overrides are the most fragile thing to maintain across upstream changes.** When upstream code renames a proc, changes its signature, or alters what `..()` returns, your override breaks. The fewer side-overrides a modpack has, the less it breaks when upstream code is refactored. **There being no merge conflicts doesn't mean your code wasn't broken by an update!** ### Cross-modpack interactions Sometimes, a modpack that's enabled might need to do something in response to another modpack also being enabled. Compatibility patches allow for this to happen without the modpacks in question requiring a hard dependency on each other. From a4bdd5467725a721001669906517898a11e82c84 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 29 Jun 2026 17:16:50 -0400 Subject: [PATCH 36/79] Fix incorrect proc references --- code/game/objects/items/weapons/storage/secure.dm | 4 ++-- code/modules/augment/active/polytool.dm | 6 +++--- code/modules/augment/simple.dm | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/code/game/objects/items/weapons/storage/secure.dm b/code/game/objects/items/weapons/storage/secure.dm index 19eed200847..45721fdb72b 100644 --- a/code/game/objects/items/weapons/storage/secure.dm +++ b/code/game/objects/items/weapons/storage/secure.dm @@ -31,12 +31,12 @@ /obj/item/secure_storage/Initialize(ml, material_key) var/datum/extension/lockable/mylock = get_or_create_extension(src, lock_type) - events_repository.register(/decl/observ/lock_state_changed, mylock, src, /obj/item/secure_storage/proc/on_lock_state_changed) + events_repository.register(/decl/observ/lock_state_changed, mylock, src, PROC_REF(on_lock_state_changed)) . = ..() /obj/item/secure_storage/Destroy() var/datum/extension/lockable/mylock = get_extension(src, lock_type) - events_repository.unregister(/decl/observ/lock_state_changed, mylock, src, /obj/item/secure_storage/proc/on_lock_state_changed) + events_repository.unregister(/decl/observ/lock_state_changed, mylock, src, PROC_REF(on_lock_state_changed)) . = ..() /obj/item/secure_storage/proc/on_lock_state_changed(datum/extension/lockable/L, old_locked, new_locked) diff --git a/code/modules/augment/active/polytool.dm b/code/modules/augment/active/polytool.dm index ad72c0f405b..09d7ec605d6 100644 --- a/code/modules/augment/active/polytool.dm +++ b/code/modules/augment/active/polytool.dm @@ -14,9 +14,9 @@ var/obj/item/I = new path (src) I.canremove = FALSE items += I - events_repository.register(/decl/observ/moved, I, src, /obj/item/organ/internal/augment/active/polytool/proc/check_holding) - events_repository.register(/decl/observ/destroyed, I, src, /obj/item/organ/internal/augment/active/polytool/proc/check_holding) - events_repository.register(/decl/observ/item_unequipped, I, src, /obj/item/organ/internal/augment/active/polytool/proc/check_holding) + events_repository.register(/decl/observ/moved, I, src, PROC_REF(check_holding)) + events_repository.register(/decl/observ/destroyed, I, src, PROC_REF(check_holding)) + events_repository.register(/decl/observ/item_unequipped, I, src, PROC_REF(check_holding)) /obj/item/organ/internal/augment/active/polytool/Destroy() for(var/obj/item/item in items) diff --git a/code/modules/augment/simple.dm b/code/modules/augment/simple.dm index 14e48b5ca45..52410b05c91 100644 --- a/code/modules/augment/simple.dm +++ b/code/modules/augment/simple.dm @@ -12,9 +12,9 @@ holding.canremove = FALSE if(!origin_tech) origin_tech = holding.get_origin_tech() - events_repository.register(/decl/observ/moved, holding, src, /obj/item/organ/internal/augment/active/simple/proc/check_holding) - events_repository.register(/decl/observ/destroyed, holding, src, /obj/item/organ/internal/augment/active/simple/proc/check_holding) - events_repository.register(/decl/observ/item_unequipped, holding, src, /obj/item/organ/internal/augment/active/simple/proc/check_holding) + events_repository.register(/decl/observ/moved, holding, src, PROC_REF(check_holding)) + events_repository.register(/decl/observ/destroyed, holding, src, PROC_REF(check_holding)) + events_repository.register(/decl/observ/item_unequipped, holding, src, PROC_REF(check_holding)) /obj/item/organ/internal/augment/active/simple/proc/check_holding() From 843f53c78c8b8d5954393eef8cb5a989209b744e Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 29 Jun 2026 17:13:00 -0400 Subject: [PATCH 37/79] Add 'writing extensible code' section to modpack readme --- mods/README.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/mods/README.md b/mods/README.md index 7c73056d41b..3d154e8b1e6 100644 --- a/mods/README.md +++ b/mods/README.md @@ -157,7 +157,168 @@ Some modpacks extend other modpacks and make no sense to include on their own, i Modular code on a downstream with an upstream that does frequent refactors and rewrites is inevitably going to break when the upstream codebase does anything. Names change, so do assumptions, and even design directions might diverge so far that reconciling them will be hard or even impossible. There's not really getting around that, but we can at least mitigate it by designing stable interfaces and documenting changes. When upstream code is written with modularity in mind, downstreams have a much easier time adding content. ## How do I write upstream/core code with extension via modpacks in mind? -TODO: Actually write this section. Give examples like `/decl/atmos_grief_fix_step`, `/decl/human_examination`, the cocktails system, etc. Iterating over subtypes of a base type makes it easy for modpacks to add new code. Also maybe address some footguns like trying to make something modular before trying to make it actually work? Could also discuss the open-closed principle I guess, e.g. write code that gets *extended* rather than *modified* (so avoiding side-overrides where possible, etc.). +This is the counterpart of "Overriding Core Code" above. There, a modpack reaches into core code and changes it from the side; here, you're the one writing the core code, and your goal is to leave an *entry point* that modpacks can hook into without ever editing your code. The guiding idea is the open/closed principle: code should be open for extension but closed for modification. Every time a modpack can add a feature by writing a new file instead of side-overriding one of yours, that's one fewer thing that silently breaks when you refactor later. + +The single most useful tool for this is **iterating over decl subtypes.** Define an abstract decl as a hook point, write your core logic to enumerate every subtype of it and call into them, and modpacks extend the system simply by defining a new subtype. Nothing in core needs to know the modpack exists. + +### Pattern: action decls +Take the "fix atmospherics grief" admin tool. Core code defines an abstract decl with a small interface, then enumerates every subtype, sorts them, and calls each: + +```dm +// code/modules/admin/verbs/grief_fixers.dm +/decl/atmos_grief_fix_step + abstract_type = /decl/atmos_grief_fix_step + var/name + +/decl/atmos_grief_fix_step/proc/act() + return + +// ...elsewhere, the verb that runs them all: +var/list/steps = decls_repository.get_decls_of_subtype_unassociated(/decl/atmos_grief_fix_step) +steps = sortTim(steps.Copy(), /proc/cmp_decl_sort_value_asc) +for(var/decl/atmos_grief_fix_step/fix_step as anything in steps) + to_chat(usr, "[fix_step.name].") + fix_step.act() +``` + +A modpack can then add a step without touching any of the above. It just defines a new subtype and the core loop runs it in the specified order: + +```dm +// mods/content/supermatter/datums/sm_grief_fix.dm +/decl/atmos_grief_fix_step/supermatter + name = "Supermatter depowered" + sort_order = 0 + +/decl/atmos_grief_fix_step/supermatter/act() + // Depower the supermatter, as it would quickly blow up once we remove all gases from the pipes. + for(var/obj/structure/supermatter/S in SSsupermatter.processing) + S.power = 0 +``` + +Note the two things that make this clean: `abstract_type` marks the base as not-runnable so the enumeration only picks up real steps, and a `sort_order` var (read by the `cmp_decl_sort_value_asc` comparator) lets each subtype declare where it belongs in the sequence rather than relying on definition or load order. When you design a hook like this, give modpacks an explicit ordering knob instead of leaving order undefined. + +### Pattern: output builder decls +Enumerable decls don't have to *do* something; they can be useful just for a calculation or return value used in base-game code. Human examination text works this way. Base human examination code defines a stub decl whose whole purpose is to be subtyped by modpacks: + +```dm +// code/modules/mob/living/human/human_examine_decl.dm +/decl/human_examination //This is essentially a stub-method for modpacks to be able to add onto the human examination stuff + var/priority = 0 + +/decl/human_examination/proc/do_examine(mob/user, distance, mob/living/human/source, hideflags, decl/pronouns/pronouns) + return +``` + +Core's examine code enumerates these decls (sorted by `priority`) and appends whatever each returns. A modpack adds a line to the examine output by defining a subtype of `/decl/human_examination` and implementing `do_examine()`; see `mods/content/matchmaking/matchmaker.dm` for a working example. + +### Pattern: condition/recipe decls +Similarly to output builder decls (see prior section), the cocktails system (`code/modules/reagents/cocktails.dm`), chemical reaction system (`code\modules\reagents\reactions\_reaction.dm`), and stack recipe system (`code\modules\crafting\stack_recipes\_recipe.dm`) follow a similar idea: a base type that gets enumerated, so modpacks add new recipes by adding subtypes rather than editing a central list. By combining this with other principles, modpacks can extend, remove, or modify existing recipes, cocktails, reactions, etc. without needing to edit them directly. + +### Pattern: events (`/decl/observ`) +The decl patterns above allow core and modpack code to request extensible subtypes representing information, behavior, or conditions. Conversely, events allow modular code to request an update when something particular happens, and they're easily the most versatile tool for writing code that doesn't require side-overrides. When something notable happens, core code raises an event, and anything that cares can register to be notified. The code raising the event has no idea who's listening, and never needs a call added for each new listener. This is exactly what makes it good for modular code: a modpack can react to a core event (or even another modpack's event) without the event-issuing code containing a single reference to the consumer of that event. + +An event is a `/decl/observ` subtype. Defining one is just a declaration plus a doc comment describing the arguments listeners will receive: + +```dm +// code/datums/observation/death.dm +// Raised when: A mob dies. +// Arguments the called proc should expect: +// /mob/dying_mob: the mob that died. +/decl/observ/death + name = "Death" + expected_type = /mob +``` + +Events are typically raised with the `RAISE_EVENT` macro, passing the source as the first argument followed by any event-specific arguments. + +```dm +// code/datums/observation/death.dm +/mob/living/add_to_dead_mob_list() + . = ..() + if(.) + RAISE_EVENT(/decl/observ/death, src) +``` + +A modpack (or any object) hooks in by registering a callback through `events_repository`. The arguments are `(event_type, event_source, listener, proc_to_call)`; the listener's proc receives the event source plus whatever extra args the event documents. Crucially, you must **unregister** when you no longer care (and always before the listener is destroyed), or the listener will be forced to clean them up manually on deletion, which can be slow. This augment registers on the item it's holding and tears the registration down when that item goes away (through another event): + +```dm +// mods/content/augments/simple.dm +/obj/item/organ/internal/augment/active/simple/Initialize() + . = ..() + // ... + events_repository.register(/decl/observ/moved, holding, src, PROC_REF(check_holding)) + events_repository.register(/decl/observ/destroyed, holding, src, PROC_REF(check_holding)) + +/obj/item/organ/internal/augment/active/simple/proc/check_holding() + if(QDELETED(holding)) + events_repository.unregister(/decl/observ/moved, holding, src) + events_repository.unregister(/decl/observ/destroyed, holding, src) + holding = null +``` + +Pass `event_source` to register for events from one specific object; use `register_global(event_type, listener, proc_call)` to hear about that event from *every* source. Some high-traffic events forbid this for performance with the `OBSERVATION_NO_GLOBAL_REGISTRATIONS` flag, so check the event's definition; if writing something that may need that level of performance, `raise_event_non_global` can be used instead of `RAISE_EVENT`. + +When you're writing core code, raising an event is the right move whenever you can imagine *someone, someday* wanting to react to something (death, an item moving, a mob examining something) without you knowing who they are or why they want it. It costs one `RAISE_EVENT` line (and the overhead of dispatching events to listeners) and buys almost-unlimited extensibility in modpacks. + +The problem is it's easy to get overeager: every event has a small registration/dispatch cost, so raise them when needed rather than sprinkling them everywhere on the off chance. You can always make an upstream PR to add a new event when it's needed. + +As an aside, those familiar with TGstation's "DCS" system (datum, component, signal) will recognize this as very similar to TG's signals. They do functionally the same thing. + +### Pattern: extensions (`/datum/extension`) +Events let modpacks react to *moments*; extensions let them attach *state and behavior* to an object without subtyping it or piling vars onto its definition. An extension is a separate datum (`/datum/extension`) that hangs off a "holder" datum, keeping a self-contained feature's data and procs encapsulated in its own type instead of smeared across the holder's variable space. This is composition over inheritance: rather than making a new `/obj/item/chems/pill` subtype for "a pill that hides what it contains," you attach an `obfuscated_medication` extension to any pill. + +That separation of concerns is the whole point. The holder doesn't grow a var or a proc for the feature; the feature lives entirely in the extension, can be attached to several unrelated holder types (anything matching its `expected_type`), and can be added or removed at runtime. For a modpack this means adding a self-contained capability to a core object while not touching the core object's *definition* at all. This can even be useful in core code for functionality shared across types whose common ancestor is unacceptably early in the type hierarchy, like `/datum/extension/loaded_cell` (`code\datums\extensions\cell\cell.dm`) or `/datum/extension/padding` (`code\datums\extensions\padding\padding.dm`). + +An extension subtype sets `base_type` (attaching a second extension derived from the same `base_type` replaces the first) and `expected_type` (the holders it's allowed on, enforced at construction): + +```dm +// mods/content/bigpharma/extension.dm +/datum/extension/obfuscated_medication + base_type = /datum/extension/obfuscated_medication + expected_type = /obj/item + flags = EXTENSION_FLAG_IMMEDIATE + var/original_reagent + +/datum/extension/obfuscated_medication/pill + expected_type = /obj/item/chems/pill + +/datum/extension/obfuscated_medication/pill/update_appearance() + var/obj/item/pill = holder // every extension knows its holder + pill.icon_state = get_medication_icon_state_from_reagent_name(original_reagent, "pill", 1, 5) +``` + +You attach an extension to a holder via `set_extension(holder, extension_type, ...)`; any extra arguments are forwarded to the extension's `New()`/`post_construction()`. Then you can retrieve it with `get_extension(holder, base_type)`. By default extensions are lazy-loaded (only instantiated on first `get_extension`); set `EXTENSION_FLAG_IMMEDIATE` if it must exist the moment it's attached. There's also `has_extension()` (a cheap presence check that won't trigger lazy instantiation), `remove_extension()`, and `get_or_create_extension()`: + +```dm +// mods/content/augments/active/cyberbrain.dm +/obj/item/organ/internal/augment/active/cyberbrain/Initialize() + . = ..() + // ... + set_extension(src, /datum/extension/interactive/os/device/implant) + set_extension(src, /datum/extension/assembly/modular_computer/cyberbrain) + // ... + +/obj/item/organ/internal/augment/active/cyberbrain/proc/install_default_hardware() + var/datum/extension/assembly/assembly = get_extension(src, /datum/extension/assembly) + for(var/component_type in default_hardware) + assembly.try_install_component(null, new component_type(src)) +``` + +Note that the cyberbrain above attaches *two* unrelated extensions (a modular-computer assembly extension and the OS extension) to one organ. Each is a distinct concern with its own state, neither knows about the other, and neither required a new organ subtype. + +When you're writing core code, prefer an extension over adding vars/procs to a base type whenever the feature is **optional, self-contained, or only relevant to some instances.** This keeps the base type lean and gives modpacks a clean attachment point. Create a subtype instead when the behavior is intrinsic to what the object *is* rather than an add-on. As with all of these patterns, don't build an extension for something only one type will ever use and that isn't a separable concern; overengineering is the enemy of getting things done. + +As with observation events, those familiar with DCS will note that these are similar to components, with the caveat that explicitly checking for extensions and calling methods on them is perfectly acceptable. You can still avoid it through the use of events, and doing so will often lead to cleaner, more extensible code (after all, if you need to add a hook, chances are something else will too), but it is by no means mandatory or even preferred by all developers. + +### Other hooks worth leaving +- **Append to lists, don't replace them.** If core code builds a list that modpacks might want to add to, expose it (or build it from decl subtypes) so a modpack can contribute an entry. The static-list-getter injector pattern in "Overriding Core Code" above exists precisely *because* a getter didn't leave an easier entry point. Don't make modpacks resort to it if you can offer a cleaner hook. +- **Split a proc to create a hook.** If a modpack would otherwise need to side-override the middle of a long proc (the "Extend, don't copy" footgun), the right fix on the core side is to factor that middle out into its own overridable proc, so modpacks can override the small piece and call `..()`. +- **Add vars to a type for modpacks to fill in.** A core type can carry a var that core logic respects but only modpacks ever set, letting modpacks opt into behavior declaratively. + +### Footguns +- **Don't focus too hard on abstraction before getting something working.** It's tempting to design an elaborate system ahead of time to make implementation easier, but a structure for extension is only useful once you understand what that structure needs to accomplish. Implement a working feature first, *then* focus on the points modpacks might actually want to modify. Time spent abstracting and modularizing a system that's fundamentally broken is time wasted. +- **A stable interface is a promise.** Once modpacks (and downstreams) hook into your decl or proc, renaming it or changing its signature breaks them silently. Treat hook points as a small, deliberate API: keep them narrow, name them clearly, and **document them,** because changing them is more troublesome than changing ordinary internal code. +- **Be aware of subtype ordering.** If the order your subtypes run in matters, give them an explicit ordering var (like `sort_order`/`priority` above). Relying on enumeration or load order makes behavior depend on which modpacks happen to be enabled, which is exactly the kind of fragility this whole approach is meant to avoid. # Contribution Please contribute to this README/guide. It's currently unfinished and doesn't cover a lot of important things. Thanks. \ No newline at end of file From a17df4688a623084c054a002c9b19f423155f00a Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Mon, 29 Jun 2026 23:38:10 -0400 Subject: [PATCH 38/79] Fix self_message passed to non-mob visible_message --- .../structures/stool_bed_chair_nest_sofa/bedroll.dm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/game/objects/structures/stool_bed_chair_nest_sofa/bedroll.dm b/code/game/objects/structures/stool_bed_chair_nest_sofa/bedroll.dm index 27d710d9cdb..1b326dbcfda 100644 --- a/code/game/objects/structures/stool_bed_chair_nest_sofa/bedroll.dm +++ b/code/game/objects/structures/stool_bed_chair_nest_sofa/bedroll.dm @@ -61,14 +61,14 @@ /obj/structure/bed/bedroll/show_buckle_message(var/mob/buckled, var/mob/buckling) if(buckled == buckling) - visible_message( + buckled.visible_message( SPAN_NOTICE("\The [buckled] climbs into \the [src]."), SPAN_NOTICE("You climb into \the [src]."), SPAN_NOTICE("You hear a rustling sound.") ) else var/decl/pronouns/pronouns = buckled.get_pronouns() - visible_message( + buckled.visible_message( SPAN_NOTICE("\The [buckled] [pronouns.is] bundled into \the [src] by \the [buckling]."), SPAN_NOTICE("You are bundled into \the [src] by \the [buckling]."), SPAN_NOTICE("You hear a rustling sound.") @@ -76,13 +76,13 @@ /obj/structure/bed/bedroll/show_unbuckle_message(var/mob/buckled, var/mob/buckling) if(buckled == buckling) - visible_message( + buckled.visible_message( SPAN_NOTICE("\The [buckled] climbs out of \the [src]."), SPAN_NOTICE("You climb out of \the [src]."), SPAN_NOTICE("You hear a rustling sound.") ) else - visible_message( + buckled.visible_message( SPAN_NOTICE("\The [buckled] was pulled out of \the [src] by \the [buckling]."), SPAN_NOTICE("You were pulled out of \the [src] by \the [buckling]."), SPAN_NOTICE("You hear a rustling sound.") From 014ba380467cf9b7c53d79eb40d0db3b0551481b Mon Sep 17 00:00:00 2001 From: Lohikar Date: Tue, 30 Jun 2026 00:12:32 -0500 Subject: [PATCH 39/79] lighting: Fix cryptic comment --- code/__defines/lighting.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/__defines/lighting.dm b/code/__defines/lighting.dm index 6e0c142608a..0662b49a090 100644 --- a/code/__defines/lighting.dm +++ b/code/__defines/lighting.dm @@ -25,7 +25,7 @@ #define TURF_IS_AMBIENT_LIT_UNSAFE(T) (T:ambient_active) #define TURF_IS_AMBIENT_LIT(T) (isturf(T) && TURF_IS_AMBIENT_LIT_UNSAFE(T)) -// The relation of these is important. +// These are centered around zero to simplify logic; 'up' is 'is greater than -1', 'down' is 'is less than 0'. 0 matches both conditions. #define LIGHTING_CORNER_GENERATE_UP 1 #define LIGHTING_CORNER_GENERATE_BOTH 0 #define LIGHTING_CORNER_GENERATE_DOWN -1 From fd6f6ff0655e5fdb54abf12a9b14f98c4863429a Mon Sep 17 00:00:00 2001 From: Lohikar Date: Tue, 30 Jun 2026 00:51:57 -0500 Subject: [PATCH 40/79] lighting: Fix lights shining onto occluding dyn turfs from static ones (common case: flashlight on space facing wall) --- code/modules/lighting/lighting_source.dm | 12 ++++++++++-- code/modules/lighting/lighting_turf.dm | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm index 550a467ba8e..32cf79dec8b 100644 --- a/code/modules/lighting/lighting_source.dm +++ b/code/modules/lighting/lighting_source.dm @@ -357,9 +357,17 @@ if ((DETERMINANT(limit_a_x, limit_a_y, test_x, test_y) > 0) || DETERMINANT(test_x, test_y, limit_b_x, limit_b_y) > 0) continue - Tcorners = T.corners + // If we're shining a light from a static lit turf onto a dynamic lit one, we do actually want to create corners to light that turf. // These checks are inlined from generate_missing_corners. They must be kept (roughly) in sync. This one intentionally does not check for ambient turfs. - if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + is_dyn_or_adj = TURF_IS_DYNAMICALLY_LIT_UNSAFE(T) + if (!is_dyn_or_adj) + for (var/turf/Tneigh as anything in RANGE_TURFS(1, T)) + if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(Tneigh)) + is_dyn_or_adj = TRUE + break + + Tcorners = T.corners + if (is_dyn_or_adj) if (!T.lighting_corners_initialised) T.lighting_corners_initialised = TRUE diff --git a/code/modules/lighting/lighting_turf.dm b/code/modules/lighting/lighting_turf.dm index f0713689d33..77a57083bb8 100644 --- a/code/modules/lighting/lighting_turf.dm +++ b/code/modules/lighting/lighting_turf.dm @@ -143,7 +143,14 @@ // This is inlined in lighting_source.dm. // Update it too if you change this. /turf/proc/generate_missing_corners() - if (!TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) && !ambient_light) + var/is_dyn = TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) || ambient_light + if (!is_dyn) + for (var/turf/Tneigh as anything in RANGE_TURFS(1, src)) + if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(Tneigh)) + is_dyn = TRUE + break + + if (!is_dyn) return lighting_corners_initialised = TRUE From 7895524ed338ba1a509b1f1cc01b56acc9d946a0 Mon Sep 17 00:00:00 2001 From: Lohikar Date: Tue, 30 Jun 2026 00:54:45 -0500 Subject: [PATCH 41/79] lighting: Allow Z-light transmission through static turfs --- code/modules/lighting/lighting_corner.dm | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/code/modules/lighting/lighting_corner.dm b/code/modules/lighting/lighting_corner.dm index f131ac2983b..36926e35da4 100644 --- a/code/modules/lighting/lighting_corner.dm +++ b/code/modules/lighting/lighting_corner.dm @@ -198,8 +198,8 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, Downward: check if self is ALLOW_LIGHTING and below This corner will be shared by all four of its turfs, so it doesn't matter which condition passes. - The above/below corners should be created iff one of the masters is considered dynamically lit, including dynamic promotion. No other condition matters, and light is not - allowed to transmit through a static lit turf, even if it is marked as ZM_ALLOW_LIGHTING. + The above/below corners should be created if the master has a Z-connection and is ALLOW_LIGHTING, regardless of if it's actually dynamic. This allows light to shine + through Z-turfs that are themselves not dynamic. */ // BOTH is 0, so it's true for both conditions. @@ -218,25 +218,25 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, It's just the same block of code repeated four times (for each master), plus the case of there now being no above corner, but previously having had one. We also only initialize the one corner we need rather than all four since there's no benefit to initializing them all -- if a true light needs them, it'll make them itself. */ - if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t1i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t1i] - else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t2i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t2i] - else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t3i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t3i] - else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t4i]) && GOING_UP) if (!T.corners) T.corners = new(4) @@ -280,25 +280,25 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, SSlighting.corner_queue += corn // As above, so below. The ordering here is a bit different from the above block, check the comment at the top of this proc. - if ((t1?.z_flags & ZM_ALLOW_LIGHTING) && (T = t1.below || GET_BELOW(t1)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + if ((t1?.z_flags & ZM_ALLOW_LIGHTING) && (T = t1.below || GET_BELOW(t1))) if (!(below_corner = T.corners?[t1i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t1i] - else if ((t2?.z_flags & ZM_ALLOW_LIGHTING) && (T = t2.below || GET_BELOW(t2)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if ((t2?.z_flags & ZM_ALLOW_LIGHTING) && (T = t2.below || GET_BELOW(t2))) if (!(below_corner = T.corners?[t2i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t2i] - else if ((t3?.z_flags & ZM_ALLOW_LIGHTING) && (T = t3.below || GET_BELOW(t3)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if ((t3?.z_flags & ZM_ALLOW_LIGHTING) && (T = t3.below || GET_BELOW(t3))) if (!(below_corner = T.corners?[t3i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t3i] - else if ((t4?.z_flags & ZM_ALLOW_LIGHTING) && (T = t4.below || GET_BELOW(t4)) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) + else if ((t4?.z_flags & ZM_ALLOW_LIGHTING) && (T = t4.below || GET_BELOW(t4))) if (!(below_corner = T.corners?[t4i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) From b3a373c1ce7838e4a6cde5c5c263c2ec6c0d6f8e Mon Sep 17 00:00:00 2001 From: Lohikar Date: Tue, 30 Jun 2026 00:57:37 -0500 Subject: [PATCH 42/79] lighting: Fix paste errors --- code/modules/lighting/lighting_source.dm | 3 ++- code/modules/lighting/lighting_turf.dm | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm index 32cf79dec8b..911e71de89d 100644 --- a/code/modules/lighting/lighting_source.dm +++ b/code/modules/lighting/lighting_source.dm @@ -347,6 +347,7 @@ var/test_y var/should_do_wedge = light_angle && !facing_opaque + var/is_dyn_or_adj FOR_DVIEW(T, NONUNIT_CEILING(actual_range, 1), source_turf, 0) do if (should_do_wedge) // Directional lighting coordinate filter. @@ -361,7 +362,7 @@ // These checks are inlined from generate_missing_corners. They must be kept (roughly) in sync. This one intentionally does not check for ambient turfs. is_dyn_or_adj = TURF_IS_DYNAMICALLY_LIT_UNSAFE(T) if (!is_dyn_or_adj) - for (var/turf/Tneigh as anything in RANGE_TURFS(1, T)) + for (var/turf/Tneigh as anything in RANGE_TURFS(T, 1)) if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(Tneigh)) is_dyn_or_adj = TRUE break diff --git a/code/modules/lighting/lighting_turf.dm b/code/modules/lighting/lighting_turf.dm index 77a57083bb8..900edbcc531 100644 --- a/code/modules/lighting/lighting_turf.dm +++ b/code/modules/lighting/lighting_turf.dm @@ -145,7 +145,7 @@ /turf/proc/generate_missing_corners() var/is_dyn = TURF_IS_DYNAMICALLY_LIT_UNSAFE(src) || ambient_light if (!is_dyn) - for (var/turf/Tneigh as anything in RANGE_TURFS(1, src)) + for (var/turf/Tneigh as anything in RANGE_TURFS(src, 1)) if (TURF_IS_DYNAMICALLY_LIT_UNSAFE(Tneigh)) is_dyn = TRUE break From ac8abcedf1f747079deef5c4f1639479154b51fc Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sun, 5 Jul 2026 20:05:38 -0500 Subject: [PATCH 43/79] mapping: Fix ZM z-groups not being (re)generated, minor efficiency --- code/controllers/subsystems/mapping.dm | 22 ++++++++++++++++++---- code/modules/maps/reader.dm | 6 ++++-- code/modules/multiz/map_data.dm | 4 ++-- code/modules/overmap/ships/landable.dm | 5 +++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm index 1eb7ecbbf52..717518c3c64 100644 --- a/code/controllers/subsystems/mapping.dm +++ b/code/controllers/subsystems/mapping.dm @@ -256,20 +256,34 @@ SUBSYSTEM_DEF(mapping) planetoid_data_by_z.len = world.maxz connected_z_cache.Cut() + SSzcopy.calculate_zstack_limits() + //Update SSWeather's indexed lists, if we can. if(SSweather?.weather_by_z) SSweather.weather_by_z.len = world.maxz +/// This is equivalent to calling `increment_world_z_size()` in a loop, but more efficient. +/datum/controller/subsystem/mapping/proc/bulk_increment_world_z_size(num_z_levels, new_level_type, defer_setup = FALSE) + ASSERT(num_z_levels > 0) + var/old_max = world.maxz + world.maxz += num_z_levels + + reindex_lists() + + if (!new_level_type) + CRASH("Missing z-level data type for z[old_max] through z[old_max + num_z_levels]!") + + for (var/i in 1 to num_z_levels) + var/datum/level_data/level = new new_level_type(old_max + i, defer_setup) + level.initialize_new_level() + /datum/controller/subsystem/mapping/proc/increment_world_z_size(var/new_level_type, var/defer_setup = FALSE) world.maxz++ reindex_lists() - if(SSzcopy.zlev_maximums.len) - SSzcopy.calculate_zstack_limits() if(!new_level_type) - PRINT_STACK_TRACE("Missing z-level data type for z["[world.maxz]"]!") - return + CRASH("Missing z-level data type for z[world.maxz]!") var/datum/level_data/level = new new_level_type(world.maxz, defer_setup) level.initialize_new_level() diff --git a/code/modules/maps/reader.dm b/code/modules/maps/reader.dm index 1704be274b2..b3d125edaac 100644 --- a/code/modules/maps/reader.dm +++ b/code/modules/maps/reader.dm @@ -140,8 +140,10 @@ var/global/dmm_suite/preloader/_preloader = new if(zexpansion && !measureOnly) // don't actually expand the world if we're only measuring bounds if(cropMap) continue - while(world.maxz < zcrd) //create new z_levels if needed. - SSmapping.increment_world_z_size(level_data_type) + var/desired_levels = zcrd - world.maxz + if (desired_levels > 0) //create new z_levels if needed. + SSmapping.bulk_increment_world_z_size(level_data_type) + bounds[MAP_MINX] = min(bounds[MAP_MINX], clamp(xcrdStart, x_lower, x_upper)) bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) diff --git a/code/modules/multiz/map_data.dm b/code/modules/multiz/map_data.dm index 83d4f972daa..da960afb031 100644 --- a/code/modules/multiz/map_data.dm +++ b/code/modules/multiz/map_data.dm @@ -18,8 +18,8 @@ INITIALIZE_IMMEDIATE(/obj/abstract/map_data) z_levels.len = i z_levels[i] = src - if (length(SSzcopy.zlev_maximums)) - SSzcopy.calculate_zstack_limits() + SSzcopy.calculate_zstack_limits() + return ..() /obj/abstract/map_data/Destroy(forced) diff --git a/code/modules/overmap/ships/landable.dm b/code/modules/overmap/ships/landable.dm index 93adc809767..452b8f5b89d 100644 --- a/code/modules/overmap/ships/landable.dm +++ b/code/modules/overmap/ships/landable.dm @@ -63,9 +63,10 @@ // We autobuild our z levels. /obj/effect/overmap/visitable/ship/landable/find_z_levels() if(!use_mapped_z_levels) + var/initial_z = world.maxz + SSmapping.bulk_increment_world_z_size(multiz + 1, level_type) for(var/i = 0 to multiz) - SSmapping.increment_world_z_size(level_type) - map_z += world.maxz + map_z += initial_z + i + 1 else ..() From ba17a3a264a7fe21feb3739f635eadb5af51140e Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sun, 5 Jul 2026 20:06:26 -0500 Subject: [PATCH 44/79] zm: Adjust comment on calculate_zstack_limits() --- code/controllers/subsystems/zcopy.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/subsystems/zcopy.dm b/code/controllers/subsystems/zcopy.dm index e0dfe3f0b1c..e6290e99300 100644 --- a/code/controllers/subsystems/zcopy.dm +++ b/code/controllers/subsystems/zcopy.dm @@ -168,7 +168,7 @@ SUBSYSTEM_DEF(zcopy) // Flush the queue. fire(FALSE, TRUE) -// If you add a new Zlevel or change Z-connections, call this. +/// (Re)generate Z-group information. You should run this every time world.maxz (or z-connections) change. ZM's behavior is undefined between resizing the world and calling this proc. /datum/controller/subsystem/zcopy/proc/calculate_zstack_limits() zlev_maximums = new(world.maxz) var/start_zlev = 1 From 1b36e16353e73644d89b6fcf56012c08df51c046 Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sun, 5 Jul 2026 23:28:35 -0500 Subject: [PATCH 45/79] mapping: don't assume SSzcopy exists --- code/controllers/subsystems/mapping.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm index 717518c3c64..78253db4d1d 100644 --- a/code/controllers/subsystems/mapping.dm +++ b/code/controllers/subsystems/mapping.dm @@ -256,7 +256,7 @@ SUBSYSTEM_DEF(mapping) planetoid_data_by_z.len = world.maxz connected_z_cache.Cut() - SSzcopy.calculate_zstack_limits() + SSzcopy?.calculate_zstack_limits() //Update SSWeather's indexed lists, if we can. if(SSweather?.weather_by_z) From 6632034436db2a8c991a5e50e775b31f7e7ed26d Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sun, 5 Jul 2026 23:36:21 -0500 Subject: [PATCH 46/79] mapping: fix bad arg to BIWZS --- code/modules/maps/reader.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/maps/reader.dm b/code/modules/maps/reader.dm index b3d125edaac..87b26dd61cb 100644 --- a/code/modules/maps/reader.dm +++ b/code/modules/maps/reader.dm @@ -142,7 +142,7 @@ var/global/dmm_suite/preloader/_preloader = new continue var/desired_levels = zcrd - world.maxz if (desired_levels > 0) //create new z_levels if needed. - SSmapping.bulk_increment_world_z_size(level_data_type) + SSmapping.bulk_increment_world_z_size(desired_levels, level_data_type) bounds[MAP_MINX] = min(bounds[MAP_MINX], clamp(xcrdStart, x_lower, x_upper)) bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) From ac2b259415aeb275c75da2d1e8539f9aa570adc1 Mon Sep 17 00:00:00 2001 From: Tetra Zeta Date: Sun, 5 Jul 2026 22:44:51 -0600 Subject: [PATCH 47/79] sand resprites iconreplace --- icons/turf/flooring/sand.dmi | Bin 13412 -> 2019 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/turf/flooring/sand.dmi b/icons/turf/flooring/sand.dmi index 20339e3f1bcccea28314ae5db42648b9a1310140..ba1ad8a4c01e0ab98f235bc3b397801707c4fe78 100644 GIT binary patch delta 2017 zcmV<72Ojw3XyXr%7=Hu<0001a;d&kb000$GOjJd{z`(q}!rk4<@9@a||Hp@qs=&a& zXUDyX00001bW%=J06^y0W&i*HrFv9YbVOxyV{&P5bZKvH004NLjgiX=!Y~j;*U49e zb}xMiZrn(VbYXv?gl0?vnm}gM-#1v$m2`IZ4(GryQ>xQ&9)Bm*Uq<v>R-&E{PBmUc@RKwJNzW4^OUxR{NwPF(h00zrRL_t(&f$dsd znwvTdWk?oKdw;?^mf=0Ivw#Gg1;7{fzm+Q)60rF(gt>1~zYaY}9Lq))r*5OBNt%W= zO_elNgCJb;lvKG8%{xP0aGD2Pe!GYk%XU(4?D+3PY=1N4w17FIB@~8b2J@Fp8=M6W zX|c>o2Rr`z5L1RsAYqd*{LwHiCX(*Pr3!dTN2Dn%&(t~1N< zl35JE_z-*kLiV^NtSlGJnMv8EASo4G7KQe#Q51nY{^5rqnj_x1^;ZjLl()spd>E z@b~c}LUd5!YYAFmG3XX!4;bL$gbQ#o1AiYsAZ+Z!aY%N>7OHR_A}eK!3m9U>&@cGF z-+#wP_^lffHaMjC#UZ7%4M|U74%p>j+8^1ECw-*aDOc|yIsJj^b^-&;`XeSeC01R; z<&Iw#!LXnNZYL$K*ag`PhFO2a_~jz1=J%J&6X%?IJym~6WmN!d!h$+B2#}C)Tnm1^ zU+@lL(PC=-TQD`B2;VwxNNUtVwh^CWW`8UpYlpxOucj~V4t(KKMVhLBO5s}KFY4v{ zaD=NVq%AH}VwaSAaU`$y%SEl1KHy?6jyCH*&&coG*sT9NL;u;iF`@VLqW<#?{Rij9 zSnpx|kM!X{|9OV~n{#8V_wYdnACCu%k8vE23GsRN#)RG@!XBspZnrbyG`*eEY=5+d zTKsb}9P2$K9OC!~Q;b2{==kUp;`8o}vECyw(BqVXFt@Y~cT2=RpL!!;n~<&^#EA~{ zUcl%>=Z-*MMB0V$)_dUSy?|l-KktSD+=cUcFGy$#_XZ|qFvW`*>b(-)dM`wH_398C zB)5cDBYV9E)ID0g7qC&WBcZq!V1LxFk^{YmeY4YhAwoIIkUk2e?_j9+NV2qgFG#4} zt%9EfPFieM?}Z4t*KH^lzNGg;gc$D=+P~F!dXF}<)N;j?f!#)g6M7Jt@%LyOOc9Es ze`7)qDoI~ld|P@2p}SM2^IP`noq(0?OBem-MLheIsst3~nNP6_BgpV0cF*!t> z2K%FHEA-$di{n2gj8^EuP4;sPYng4fLJw}T!=JC=3O%^V4u?Np!`liyxXF$y^x!7T zR_MV^X3zir((L))U)s+KJ-E$Q=)p}E|Ah76Hk;6czPn0(mCJg`NIljfa-1bt&E&@4x;}sLlt3Y`8e2_KI zr!h7`N;C<@2Q6tH#yq+NlPZ*9*t`T;_=yl%9Wd7;D0Tzu*4K?g62*M?k9Yj zQ(J@PP6^h#3A4McL8?Vc@Ufc^I3-xu1yF*|{beI^+XpkqE8Nxj{<7)X3?&FnUF|1q z`(T$K*5Yo$VS-ePy9sR{#02dXH_L89yGgJQzBbElLc5E>)?fm-n|BlTyO_QxEBayR zeDK|7a3#%SMOr@$oe%a2QrlV44@2jJeS(G(3{N?9KIkPFe#@crK`+5DVd#9YOR%on z1jGF>bUp~eCivjlV_iQCoex6S2iruSqYVE8YQ0sC%djpg00000NkvXXu0mjfq~6<+ literal 13412 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 From 488389adfa54d885b8bb869e855f766cff28833a Mon Sep 17 00:00:00 2001 From: Tetra Zeta Date: Sun, 5 Jul 2026 23:42:41 -0600 Subject: [PATCH 48/79] sand has a color now by default --- code/game/turfs/flooring/flooring_sand.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/game/turfs/flooring/flooring_sand.dm b/code/game/turfs/flooring/flooring_sand.dm index 5b4f13501ea..9d6dad135c7 100644 --- a/code/game/turfs/flooring/flooring_sand.dm +++ b/code/game/turfs/flooring/flooring_sand.dm @@ -5,6 +5,7 @@ icon = 'icons/turf/flooring/sand.dmi' icon_base = "sand" icon_edge_layer = FLOOR_EDGE_SAND + color = "#ffd076" has_base_range = 4 turf_flags = TURF_FLAG_BACKGROUND | TURF_IS_HOLOMAP_PATH | TURF_FLAG_ABSORB_LIQUID force_material = /decl/material/solid/sand From 3c7e5bdea8c6a476f2758eedaa29b5bd66654629 Mon Sep 17 00:00:00 2001 From: Tetra Zeta Date: Mon, 6 Jul 2026 16:02:43 -0600 Subject: [PATCH 49/79] now we actually get the color from our forced_material top-most flooring decl --- code/game/turfs/flooring/flooring_sand.dm | 2 +- code/game/turfs/walls/wall_natural_subtypes.dm | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/code/game/turfs/flooring/flooring_sand.dm b/code/game/turfs/flooring/flooring_sand.dm index 9d6dad135c7..5e057b64eee 100644 --- a/code/game/turfs/flooring/flooring_sand.dm +++ b/code/game/turfs/flooring/flooring_sand.dm @@ -5,7 +5,7 @@ icon = 'icons/turf/flooring/sand.dmi' icon_base = "sand" icon_edge_layer = FLOOR_EDGE_SAND - color = "#ffd076" + color = null has_base_range = 4 turf_flags = TURF_FLAG_BACKGROUND | TURF_IS_HOLOMAP_PATH | TURF_FLAG_ABSORB_LIQUID force_material = /decl/material/solid/sand diff --git a/code/game/turfs/walls/wall_natural_subtypes.dm b/code/game/turfs/walls/wall_natural_subtypes.dm index dc072ac2519..549cab71e39 100644 --- a/code/game/turfs/walls/wall_natural_subtypes.dm +++ b/code/game/turfs/walls/wall_natural_subtypes.dm @@ -75,7 +75,6 @@ name = "sand"; \ icon = 'icons/turf/flooring/sand.dmi'; \ icon_state = "sand0"; \ - color = "#ae9e66"; \ _flooring = /decl/flooring/sand; \ } \ /turf/wall/natural/##ID { \ From 2e085deb64c461025e72e1e585d96241a8fa10d4 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sun, 5 Jul 2026 19:00:35 -0400 Subject: [PATCH 50/79] Move fishing into a modpack --- code/game/area/area_fishing.dm | 16 ----- code/game/area/areas.dm | 8 --- code/game/objects/items/__item.dm | 13 ---- code/game/turfs/turf.dm | 4 -- .../crafting/stack_recipes/recipes_planks.dm | 3 - .../designs/general/designs_general.dm | 6 -- .../{fishing/bait.dm => hydroponics/worm.dm} | 0 .../random_exoplanet/planet_types/grass.dm | 23 ------- .../random_exoplanet/random_planet_areas.dm | 8 --- code/modules/materials/_materials.dm | 4 +- .../solids/materials_solid_butchery.dm | 4 -- .../solids/materials_solid_organic.dm | 1 - .../guns/launcher/bows/bow_string.dm | 2 +- .../modules/reagents/chems/chems_nutriment.dm | 1 - code/modules/reagents/chems/chems_oil.dm | 1 - icons/obj/{fishing_line.dmi => bowstring.dmi} | Bin maps/modpack_testing/modpack_testing.dm | 1 + maps/shaded_hills/shaded_hills.dm | 1 + mods/content/fishing/_fishing.dm | 2 + mods/content/fishing/_fishing.dme | 13 ++++ mods/content/fishing/area_fishing.dm | 63 ++++++++++++++++++ mods/content/fishing/fishing_bait.dm | 37 ++++++++++ mods/content/fishing/fishing_designs.dm | 5 ++ .../content}/fishing/fishing_line.dm | 2 +- mods/content/fishing/fishing_recipes.dm | 2 + .../content}/fishing/fishing_rod.dm | 4 +- mods/content/fishing/icons/fishing_line.dmi | Bin 0 -> 280 bytes .../content/fishing/icons}/fishing_rod.dmi | Bin .../fishing/icons}/fishing_rod_advanced.dmi | Bin mods/content/fishing/turf_fishing.dm | 3 + nebula.dme | 5 +- 31 files changed, 133 insertions(+), 99 deletions(-) delete mode 100644 code/game/area/area_fishing.dm rename code/modules/{fishing/bait.dm => hydroponics/worm.dm} (100%) rename icons/obj/{fishing_line.dmi => bowstring.dmi} (100%) create mode 100644 mods/content/fishing/_fishing.dm create mode 100644 mods/content/fishing/_fishing.dme create mode 100644 mods/content/fishing/area_fishing.dm create mode 100644 mods/content/fishing/fishing_bait.dm create mode 100644 mods/content/fishing/fishing_designs.dm rename {code/modules => mods/content}/fishing/fishing_line.dm (93%) create mode 100644 mods/content/fishing/fishing_recipes.dm rename {code/modules => mods/content}/fishing/fishing_rod.dm (98%) create mode 100644 mods/content/fishing/icons/fishing_line.dmi rename {icons/obj => mods/content/fishing/icons}/fishing_rod.dmi (100%) rename {icons/obj => mods/content/fishing/icons}/fishing_rod_advanced.dmi (100%) create mode 100644 mods/content/fishing/turf_fishing.dm diff --git a/code/game/area/area_fishing.dm b/code/game/area/area_fishing.dm deleted file mode 100644 index 8370e39c449..00000000000 --- a/code/game/area/area_fishing.dm +++ /dev/null @@ -1,16 +0,0 @@ -/area - var/fishing_failure_prob = 95 - // Hardcoding the contents of /obj/random/junk to avoid hacks for getting results from /obj/random. - var/list/fishing_results = list( - /obj/item/remains/mouse = 1, - /obj/item/remains/robot = 1, - /obj/item/paper/crumpled = 1, - /obj/item/inflatable/torn = 1, - /obj/item/shard = 1, - /obj/item/hand/missing_card = 1 - ) - -/area/proc/get_fishing_result(turf/origin, obj/item/food/bait) - if(!length(fishing_results) || prob(fishing_failure_prob)) - return null - return pickweight(fishing_results) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index d92c81169bd..7d31f5eae33 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -98,15 +98,7 @@ var/global/list/areas = list() area_blurb_category = type ..() -/area/proc/get_additional_fishing_results() - return - /area/Initialize() - var/list/additional_fishing_results = get_additional_fishing_results() - if(LAZYLEN(additional_fishing_results)) - LAZYINITLIST(fishing_results) - for(var/fish in additional_fishing_results) - fishing_results[fish] = additional_fishing_results[fish] . = ..() global.areas += src if(!requires_power || !apc) diff --git a/code/game/objects/items/__item.dm b/code/game/objects/items/__item.dm index dfc848c8b32..fd5e22b082c 100644 --- a/code/game/objects/items/__item.dm +++ b/code/game/objects/items/__item.dm @@ -1203,19 +1203,6 @@ modules/mob/living/human/life.dm if you die, you will be zoomed out. /obj/item/proc/has_textile_fibers() return FALSE -// Returns a value used as a multiplier in the fishing delay calc. Higher represents a stronger reduction in fishing time. -#define BAIT_VALUE_CONSTANT 0.1 -/obj/item/proc/get_bait_value() - . = 0 - for(var/mat in matter) - var/decl/material/bait_mat = GET_DECL(mat) - if(bait_mat.fishing_bait_value) - . += MATERIAL_UNITS_TO_REAGENTS_UNITS(matter[mat]) * bait_mat.fishing_bait_value * BAIT_VALUE_CONSTANT - for(var/decl/material/reagent as anything in REAGENT_VOLUMES(reagents)) - if(reagent.fishing_bait_value) - . += REAGENT_VOLUME(reagents, reagent) * reagent.fishing_bait_value * BAIT_VALUE_CONSTANT -#undef BAIT_VALUE_CONSTANT - /obj/item/proc/get_storage_cost() //If you want to prevent stuff above a certain w_class from being stored, use max_w_class return BASE_STORAGE_COST(w_class) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 28171b7c239..8af4b0d3b11 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -845,10 +845,6 @@ /turf/get_color() return paint_color || get_material()?.color || color -/turf/proc/get_fishing_result(obj/item/food/bait) - var/area/A = get_area(src) - return A.get_fishing_result(src, bait) - /turf/get_affecting_weather() return weather diff --git a/code/modules/crafting/stack_recipes/recipes_planks.dm b/code/modules/crafting/stack_recipes/recipes_planks.dm index f00339f99e1..a7114078dda 100644 --- a/code/modules/crafting/stack_recipes/recipes_planks.dm +++ b/code/modules/crafting/stack_recipes/recipes_planks.dm @@ -22,9 +22,6 @@ difficulty = MAT_VALUE_VERY_HARD_DIY available_to_map_tech_level = MAP_TECH_LEVEL_SPACE -/decl/stack_recipe/planks/fishing_rod - result_type = /obj/item/fishing_rod - /decl/stack_recipe/planks/stick result_type = /obj/item/stick difficulty = MAT_VALUE_EASY_DIY diff --git a/code/modules/fabrication/designs/general/designs_general.dm b/code/modules/fabrication/designs/general/designs_general.dm index aef3b2e9a27..741fc0c2ded 100644 --- a/code/modules/fabrication/designs/general/designs_general.dm +++ b/code/modules/fabrication/designs/general/designs_general.dm @@ -153,12 +153,6 @@ path = /obj/item/stack/tape_roll/duct_tape pass_multiplier_to_product_new = FALSE // they are printed as single items with 32 uses -/datum/fabricator_recipe/fishing_line - path = /obj/item/fishing_line - -/datum/fabricator_recipe/fishing_line_high_quality - path = /obj/item/fishing_line/high_quality - /datum/fabricator_recipe/chipboard // base type is for oak path = /obj/item/stack/material/sheet/mapped/chipboard_oak category = "Textiles" diff --git a/code/modules/fishing/bait.dm b/code/modules/hydroponics/worm.dm similarity index 100% rename from code/modules/fishing/bait.dm rename to code/modules/hydroponics/worm.dm diff --git a/code/modules/maps/template_types/random_exoplanet/planet_types/grass.dm b/code/modules/maps/template_types/random_exoplanet/planet_types/grass.dm index d6ecd767659..1c4e55d0dd8 100644 --- a/code/modules/maps/template_types/random_exoplanet/planet_types/grass.dm +++ b/code/modules/maps/template_types/random_exoplanet/planet_types/grass.dm @@ -158,26 +158,3 @@ forced_ambience = list( 'sound/ambience/jungle.ogg' ) - fishing_failure_prob = 10 - // TODO: waterweed? - // Hardcoding the contents of /obj/random/natural_debris to avoid hacks to get results out of /obj/random. - fishing_results = list( - /mob/living/simple_animal/aquatic/fish = 10, - /mob/living/simple_animal/aquatic/fish/grump = 10, - /obj/item/mollusc = 5, - /obj/item/mollusc/barnacle/fished = 5, - /mob/living/simple_animal/aquatic/fish/large = 5, - /mob/living/simple_animal/aquatic/fish/large/bass = 5, - /mob/living/simple_animal/aquatic/fish/large/salmon = 5, - /mob/living/simple_animal/aquatic/fish/large/trout = 5, - /mob/living/simple_animal/aquatic/fish/large/pike = 3, - /mob/living/simple_animal/aquatic/fish/large/javelin = 3, - /obj/item/mollusc/clam/fished/pearl = 3, - /obj/item/trash/mollusc_shell/clam = 2, - /obj/item/trash/mollusc_shell/barnacle = 2, - /obj/item/remains/mouse = 2, - /obj/item/remains/lizard = 2, - /obj/item/stick = 1, - /obj/item/trash/mollusc_shell = 1, - /mob/living/simple_animal/aquatic/fish/large/koi = 1 - ) diff --git a/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm b/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm index 6964c4dbd2f..ffbdb92912b 100644 --- a/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm +++ b/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm @@ -13,14 +13,6 @@ area_flags = AREA_FLAG_IS_BACKGROUND | AREA_FLAG_EXTERNAL | AREA_FLAG_HIDE_FROM_HOLOMAP is_outside = OUTSIDE_YES -// Let's make a token effort at making the fish somewhat alien I guess. -/area/exoplanet/get_fishing_result(turf/origin, obj/item/food/bait) - . = ..() - if(ismob(.)) - var/mob/M = . - M.SetName("xeno-[M.name]") - M.set_color(get_random_colour(simple = TRUE)) - ///Spoopy undergrounds /area/exoplanet/underground name = "\improper Planetary mantle" diff --git a/code/modules/materials/_materials.dm b/code/modules/materials/_materials.dm index 02ab0d63cd4..e2ed9314861 100644 --- a/code/modules/materials/_materials.dm +++ b/code/modules/materials/_materials.dm @@ -286,9 +286,7 @@ var/global/list/materials_by_gas_symbol = list() /// If set to a material type, stacks of this material will be able to be tanned on a drying rack after being wetted to convert them to tans_to. var/tans_to - /// A multiplier for this material when used in fishing bait. - var/fishing_bait_value = 0 - /// A relative value used only by fishing line at time of commit. + /// A relative value used only by bowstrings and the fishing modpack at time of writing. var/tensile_strength = 0 /// What form does this take if dug out of the ground, if any? diff --git a/code/modules/materials/definitions/solids/materials_solid_butchery.dm b/code/modules/materials/definitions/solids/materials_solid_butchery.dm index fb78922a5f6..846edc4f50d 100644 --- a/code/modules/materials/definitions/solids/materials_solid_butchery.dm +++ b/code/modules/materials/definitions/solids/materials_solid_butchery.dm @@ -19,7 +19,6 @@ sound_manipulate = 'sound/foley/meat1.ogg' sound_dropped = 'sound/foley/meat2.ogg' hitsound = 'sound/effects/squelch1.ogg' - fishing_bait_value = 1 reagent_overlay = "soup_chunks" nutriment_factor = 10 allergen_flags = ALLERGEN_MEAT @@ -83,7 +82,6 @@ sound_manipulate = 'sound/foley/meat1.ogg' sound_dropped = 'sound/foley/meat2.ogg' hitsound = "punch" - fishing_bait_value = 0.75 tans_to = /decl/material/solid/organic/leather compost_value = 0.8 allergen_flags = ALLERGEN_MEAT @@ -116,7 +114,6 @@ default_solid_form = /obj/item/stack/material/skin/pelt sound_manipulate = 'sound/foley/paperpickup2.ogg' sound_dropped = 'sound/foley/paperpickup1.ogg' - fishing_bait_value = 0 paint_verb = "dyed" /decl/material/solid/organic/skin/fur/gray @@ -178,7 +175,6 @@ default_solid_form = /obj/item/stack/material/skin/feathers sound_manipulate = 'sound/foley/paperpickup2.ogg' sound_dropped = 'sound/foley/paperpickup1.ogg' - fishing_bait_value = 0 /decl/material/solid/organic/skin/feathers/purple color = COLOR_PALE_PURPLE_GRAY diff --git a/code/modules/materials/definitions/solids/materials_solid_organic.dm b/code/modules/materials/definitions/solids/materials_solid_organic.dm index be9b91441d1..4f0461dcba8 100644 --- a/code/modules/materials/definitions/solids/materials_solid_organic.dm +++ b/code/modules/materials/definitions/solids/materials_solid_organic.dm @@ -217,7 +217,6 @@ dug_drop_type = /obj/item/stack/material/slab sound_manipulate = 'sound/foley/paperpickup2.ogg' sound_dropped = 'sound/foley/paperpickup1.ogg' - fishing_bait_value = 0.75 allergen_flags = ALLERGEN_VEGETABLE exoplanet_rarity_plant = MAT_RARITY_MUNDANE diff --git a/code/modules/projectiles/guns/launcher/bows/bow_string.dm b/code/modules/projectiles/guns/launcher/bows/bow_string.dm index 72212cde43b..173b3567055 100644 --- a/code/modules/projectiles/guns/launcher/bows/bow_string.dm +++ b/code/modules/projectiles/guns/launcher/bows/bow_string.dm @@ -1,6 +1,6 @@ /obj/item/bowstring name = "bowstring" - icon = 'icons/obj/fishing_line.dmi' // works well enough for the time being + icon = 'icons/obj/bowstring.dmi' icon_state = ICON_STATE_WORLD desc = "A flexible length of material used to string bows." material = /decl/material/solid/organic/meat/gut diff --git a/code/modules/reagents/chems/chems_nutriment.dm b/code/modules/reagents/chems/chems_nutriment.dm index 929c4307d09..e8f8acf5bbd 100644 --- a/code/modules/reagents/chems/chems_nutriment.dm +++ b/code/modules/reagents/chems/chems_nutriment.dm @@ -9,7 +9,6 @@ fruit_descriptor = "nutritious" uid = "chem_nutriment" exoplanet_rarity_gas = MAT_RARITY_NOWHERE // Please, no more animal protein or glowsap or corn oil atmosphere. - fishing_bait_value = 0.65 compost_value = 1 nutriment_factor = 10 affect_blood_on_ingest = 0 diff --git a/code/modules/reagents/chems/chems_oil.dm b/code/modules/reagents/chems/chems_oil.dm index e540efb9844..4d38583ebe6 100644 --- a/code/modules/reagents/chems/chems_oil.dm +++ b/code/modules/reagents/chems/chems_oil.dm @@ -10,7 +10,6 @@ uid = "chem_oil_lamp" color = "#664330" value = 1.5 - fishing_bait_value = 0 taste_mult = 4 metabolism = REM * 4 exoplanet_rarity_gas = MAT_RARITY_NOWHERE diff --git a/icons/obj/fishing_line.dmi b/icons/obj/bowstring.dmi similarity index 100% rename from icons/obj/fishing_line.dmi rename to icons/obj/bowstring.dmi diff --git a/maps/modpack_testing/modpack_testing.dm b/maps/modpack_testing/modpack_testing.dm index 47e16e7a41b..31f2fc7176f 100644 --- a/maps/modpack_testing/modpack_testing.dm +++ b/maps/modpack_testing/modpack_testing.dm @@ -19,6 +19,7 @@ #include "../../mods/content/corporate/_corporate.dme" #include "../../mods/content/dungeon_loot/_dungeon_loot.dme" #include "../../mods/content/fantasy/_fantasy.dme" + #include "../../mods/content/fishing/_fishing.dme" #include "../../mods/content/generic_shuttles/_generic_shuttles.dme" #include "../../mods/content/government/_government.dme" #include "../../mods/content/inertia/_inertia.dme" diff --git a/maps/shaded_hills/shaded_hills.dm b/maps/shaded_hills/shaded_hills.dm index 0d7f2b5f770..e7487897a10 100644 --- a/maps/shaded_hills/shaded_hills.dm +++ b/maps/shaded_hills/shaded_hills.dm @@ -7,6 +7,7 @@ #include "../../mods/species/drakes/_drakes.dme" // include before _fantasy.dme so overrides work #include "../../mods/content/item_sharpening/_item_sharpening.dme" #include "../../mods/content/fantasy/_fantasy.dme" + #include "../../mods/content/fishing/_fishing.dme" #include "../../mods/content/blacksmithy/_blacksmithy.dme" #include "areas/_areas.dm" diff --git a/mods/content/fishing/_fishing.dm b/mods/content/fishing/_fishing.dm new file mode 100644 index 00000000000..f2f9bc00d27 --- /dev/null +++ b/mods/content/fishing/_fishing.dm @@ -0,0 +1,2 @@ +/decl/modpack/fishing + name = "Fishing Modpack" \ No newline at end of file diff --git a/mods/content/fishing/_fishing.dme b/mods/content/fishing/_fishing.dme new file mode 100644 index 00000000000..c68d52fd9e9 --- /dev/null +++ b/mods/content/fishing/_fishing.dme @@ -0,0 +1,13 @@ +#ifndef CONTENT_PACK_FISHING +#define CONTENT_PACK_FISHING +// BEGIN_INCLUDE +#include "_fishing.dm" +#include "area_fishing.dm" +#include "fishing_bait.dm" +#include "fishing_designs.dm" +#include "fishing_line.dm" +#include "fishing_recipes.dm" +#include "fishing_rod.dm" +#include "turf_fishing.dm" +// END_INCLUDE +#endif diff --git a/mods/content/fishing/area_fishing.dm b/mods/content/fishing/area_fishing.dm new file mode 100644 index 00000000000..5e046d58fbc --- /dev/null +++ b/mods/content/fishing/area_fishing.dm @@ -0,0 +1,63 @@ +/area + var/fishing_failure_prob = 95 + // Hardcoding the contents of /obj/random/junk to avoid hacks for getting results from /obj/random. + var/list/fishing_results = list( + /obj/item/remains/mouse = 1, + /obj/item/remains/robot = 1, + /obj/item/paper/crumpled = 1, + /obj/item/inflatable/torn = 1, + /obj/item/shard = 1, + /obj/item/hand/missing_card = 1 + ) + +/area/Initialize() + var/list/additional_fishing_results = get_additional_fishing_results() + if(LAZYLEN(additional_fishing_results)) + LAZYINITLIST(fishing_results) + for(var/fish in additional_fishing_results) + fishing_results[fish] = additional_fishing_results[fish] + . = ..() + +/area/proc/get_additional_fishing_results() + return + +/area/proc/get_fishing_result(turf/origin, obj/item/food/bait) + if(!length(fishing_results) || prob(fishing_failure_prob)) + return null + return pickweight(fishing_results) + +// overrides down here + +// Let's make a token effort at making the fish somewhat alien I guess. +/area/exoplanet/get_fishing_result(turf/origin, obj/item/food/bait) + . = ..() + if(ismob(.)) + var/mob/M = . + M.SetName("xeno-[M.name]") + M.set_color(get_random_colour(simple = TRUE)) + +//Fishing results for the grass exoplanet surface +/area/exoplanet/grass + fishing_failure_prob = 10 + // TODO: waterweed? + // Hardcoding the contents of /obj/random/natural_debris to avoid hacks to get results out of /obj/random. + fishing_results = list( + /mob/living/simple_animal/aquatic/fish = 10, + /mob/living/simple_animal/aquatic/fish/grump = 10, + /obj/item/mollusc = 5, + /obj/item/mollusc/barnacle/fished = 5, + /mob/living/simple_animal/aquatic/fish/large = 5, + /mob/living/simple_animal/aquatic/fish/large/bass = 5, + /mob/living/simple_animal/aquatic/fish/large/salmon = 5, + /mob/living/simple_animal/aquatic/fish/large/trout = 5, + /mob/living/simple_animal/aquatic/fish/large/pike = 3, + /mob/living/simple_animal/aquatic/fish/large/javelin = 3, + /obj/item/mollusc/clam/fished/pearl = 3, + /obj/item/trash/mollusc_shell/clam = 2, + /obj/item/trash/mollusc_shell/barnacle = 2, + /obj/item/remains/mouse = 2, + /obj/item/remains/lizard = 2, + /obj/item/stick = 1, + /obj/item/trash/mollusc_shell = 1, + /mob/living/simple_animal/aquatic/fish/large/koi = 1 + ) \ No newline at end of file diff --git a/mods/content/fishing/fishing_bait.dm b/mods/content/fishing/fishing_bait.dm new file mode 100644 index 00000000000..a0f3db926f6 --- /dev/null +++ b/mods/content/fishing/fishing_bait.dm @@ -0,0 +1,37 @@ +// Returns a value used as a multiplier in the fishing delay calc. Higher represents a stronger reduction in fishing time. +#define BAIT_VALUE_CONSTANT 0.1 +/obj/item/proc/get_bait_value() + . = 0 + for(var/mat in matter) + var/decl/material/bait_mat = GET_DECL(mat) + if(bait_mat.fishing_bait_value) + . += MATERIAL_UNITS_TO_REAGENTS_UNITS(matter[mat]) * bait_mat.fishing_bait_value * BAIT_VALUE_CONSTANT + for(var/decl/material/reagent as anything in REAGENT_VOLUMES(reagents)) + if(reagent.fishing_bait_value) + . += REAGENT_VOLUME(reagents, reagent) * reagent.fishing_bait_value * BAIT_VALUE_CONSTANT +#undef BAIT_VALUE_CONSTANT + +/decl/material + /// A multiplier for this material when used in fishing bait. + var/fishing_bait_value = 0 + +/decl/material/solid/organic/meat + fishing_bait_value = 1 + +/decl/material/solid/organic/plantmatter + fishing_bait_value = 0.75 + +/decl/material/liquid/nutriment + fishing_bait_value = 0.65 + +/decl/material/liquid/oil + fishing_bait_value = 0 + +/decl/material/solid/organic/skin + fishing_bait_value = 0.75 + +/decl/material/solid/organic/skin/feathers + fishing_bait_value = 0 + +/decl/material/solid/organic/skin/fur + fishing_bait_value = 0 \ No newline at end of file diff --git a/mods/content/fishing/fishing_designs.dm b/mods/content/fishing/fishing_designs.dm new file mode 100644 index 00000000000..9d1a864cbcc --- /dev/null +++ b/mods/content/fishing/fishing_designs.dm @@ -0,0 +1,5 @@ +/datum/fabricator_recipe/fishing_line + path = /obj/item/fishing_line + +/datum/fabricator_recipe/fishing_line_high_quality + path = /obj/item/fishing_line/high_quality \ No newline at end of file diff --git a/code/modules/fishing/fishing_line.dm b/mods/content/fishing/fishing_line.dm similarity index 93% rename from code/modules/fishing/fishing_line.dm rename to mods/content/fishing/fishing_line.dm index 28afa6aa1a2..7455c8af6b3 100644 --- a/code/modules/fishing/fishing_line.dm +++ b/mods/content/fishing/fishing_line.dm @@ -1,6 +1,6 @@ /obj/item/fishing_line name = "fishing line" - icon = 'icons/obj/fishing_line.dmi' + icon = 'mods/content/fishing/icons/fishing_line.dmi' icon_state = ICON_STATE_WORLD material_alteration = MAT_FLAG_ALTERATION_NAME | MAT_FLAG_ALTERATION_COLOR | MAT_FLAG_ALTERATION_DESC max_health = 100 diff --git a/mods/content/fishing/fishing_recipes.dm b/mods/content/fishing/fishing_recipes.dm new file mode 100644 index 00000000000..405773b3426 --- /dev/null +++ b/mods/content/fishing/fishing_recipes.dm @@ -0,0 +1,2 @@ +/decl/stack_recipe/planks/fishing_rod + result_type = /obj/item/fishing_rod \ No newline at end of file diff --git a/code/modules/fishing/fishing_rod.dm b/mods/content/fishing/fishing_rod.dm similarity index 98% rename from code/modules/fishing/fishing_rod.dm rename to mods/content/fishing/fishing_rod.dm index ae8342c0f67..eabc2f57cb0 100644 --- a/code/modules/fishing/fishing_rod.dm +++ b/mods/content/fishing/fishing_rod.dm @@ -9,7 +9,7 @@ color = /decl/material/solid/organic/wood/oak::color matter = null material_alteration = MAT_FLAG_ALTERATION_COLOR | MAT_FLAG_ALTERATION_NAME | MAT_FLAG_ALTERATION_DESC - icon = 'icons/obj/fishing_rod.dmi' + icon = 'mods/content/fishing/icons/fishing_rod.dmi' icon_state = ICON_STATE_WORLD w_class = ITEM_SIZE_LARGE @@ -346,7 +346,7 @@ matter = list( /decl/material/solid/metal/steel = MATTER_AMOUNT_REINFORCEMENT ) - icon = 'icons/obj/fishing_rod_advanced.dmi' + icon = 'mods/content/fishing/icons/fishing_rod_advanced.dmi' material_alteration = MAT_FLAG_ALTERATION_COLOR fishing_rod_quality = 0.2 line = /obj/item/fishing_line/high_quality diff --git a/mods/content/fishing/icons/fishing_line.dmi b/mods/content/fishing/icons/fishing_line.dmi new file mode 100644 index 0000000000000000000000000000000000000000..4824616fa1a6a968e6a4085f1563e3467992f3b1 GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv{s5m4*8>L*q$ZVhbyU2)z4*}Q$iB}&5mf8 literal 0 HcmV?d00001 diff --git a/icons/obj/fishing_rod.dmi b/mods/content/fishing/icons/fishing_rod.dmi similarity index 100% rename from icons/obj/fishing_rod.dmi rename to mods/content/fishing/icons/fishing_rod.dmi diff --git a/icons/obj/fishing_rod_advanced.dmi b/mods/content/fishing/icons/fishing_rod_advanced.dmi similarity index 100% rename from icons/obj/fishing_rod_advanced.dmi rename to mods/content/fishing/icons/fishing_rod_advanced.dmi diff --git a/mods/content/fishing/turf_fishing.dm b/mods/content/fishing/turf_fishing.dm new file mode 100644 index 00000000000..cc796cdcb3c --- /dev/null +++ b/mods/content/fishing/turf_fishing.dm @@ -0,0 +1,3 @@ +/turf/proc/get_fishing_result(obj/item/food/bait) + var/area/A = get_area(src) + return A.get_fishing_result(src, bait) \ No newline at end of file diff --git a/nebula.dme b/nebula.dme index 68161da9a70..ebf1f8f1a81 100644 --- a/nebula.dme +++ b/nebula.dme @@ -783,7 +783,6 @@ #include "code\game\antagonist\antagonist_update.dm" #include "code\game\area\area_abstract.dm" #include "code\game\area\area_access.dm" -#include "code\game\area\area_fishing.dm" #include "code\game\area\area_power.dm" #include "code\game\area\area_space.dm" #include "code\game\area\areas.dm" @@ -2431,9 +2430,6 @@ #include "code\modules\fabrication\designs\textile\protective.dm" #include "code\modules\fabrication\designs\textile\space.dm" #include "code\modules\fabrication\designs\textile\storage.dm" -#include "code\modules\fishing\bait.dm" -#include "code\modules\fishing\fishing_line.dm" -#include "code\modules\fishing\fishing_rod.dm" #include "code\modules\fission\core.dm" #include "code\modules\fission\core_control.dm" #include "code\modules\fission\fission_circuits.dm" @@ -2616,6 +2612,7 @@ #include "code\modules\hydroponics\seed_mobs.dm" #include "code\modules\hydroponics\seed_packets.dm" #include "code\modules\hydroponics\seed_storage.dm" +#include "code\modules\hydroponics\worm.dm" #include "code\modules\hydroponics\plant_types\seeds_herbs.dm" #include "code\modules\hydroponics\plant_types\seeds_misc.dm" #include "code\modules\hydroponics\spreading\spreading.dm" From 79e2c2ca0e9c445ce0b2c84e81144ac85ba2cf4b Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Wed, 8 Jul 2026 23:46:41 -0400 Subject: [PATCH 51/79] Mark a bunch of areas as abstract --- code/game/area/area_abstract.dm | 7 +++++++ .../random_exoplanet/random_planet_areas.dm | 1 + code/modules/turbolift/turbolift_areas.dm | 1 + code/unit_tests/~unit_test_types.dm | 3 +++ maps/away/derelict/derelict_areas.dm | 3 +++ maps/away/errant_pisces/errant_pisces_areas.dm | 1 + maps/away/liberia/liberia_areas.dm | 1 + maps/away/lost_supply_base/lost_supply_base_areas.dm | 6 +----- maps/away/magshield/magshield_areas.dm | 10 ++++------ maps/away/mining/mining_areas.dm | 6 +++++- maps/away/smugglers/smugglers_areas.dm | 9 +++++---- maps/away/unishi/unishi_areas.dm | 5 +++-- maps/away/yacht/yacht_areas.dm | 11 +++++++---- maps/example/example_areas.dm | 2 ++ maps/exodus/exodus_areas.dm | 1 + maps/tradeship/tradeship_areas.dm | 2 -- maps/~mapsystem/maps_unit_testing.dm | 5 ----- .../corporate/away_sites/lar_maria/lar_maria_areas.dm | 1 + mods/content/fantasy/submaps/_submaps.dm | 1 + .../government/away_sites/icarus/icarus_areas.dm | 1 + 20 files changed, 48 insertions(+), 29 deletions(-) diff --git a/code/game/area/area_abstract.dm b/code/game/area/area_abstract.dm index 853b2907a95..55e8c249a71 100644 --- a/code/game/area/area_abstract.dm +++ b/code/game/area/area_abstract.dm @@ -1,9 +1,11 @@ /area/hallway + abstract_type = /area/hallway name = "hallway" holomap_color = HOLOMAP_AREACOLOR_HALLWAYS area_start_lit = TRUE /area/maintenance + abstract_type = /area/maintenance area_flags = AREA_FLAG_RAD_SHIELDED sound_env = TUNNEL_ENCLOSED turf_initializer = /decl/turf_initializer/maintenance @@ -12,6 +14,7 @@ holomap_color = HOLOMAP_AREACOLOR_HALLWAYS /area/shuttle + abstract_type = /area/shuttle requires_power = 0 sound_env = SMALL_ENCLOSED base_turf = /turf/space @@ -19,6 +22,10 @@ holomap_color = HOLOMAP_AREACOLOR_CREW /area/ship + abstract_type = /area/ship name = "\improper Generic Ship" ambience = list('sound/ambience/ambigen3.ogg','sound/ambience/ambigen4.ogg','sound/ambience/ambigen5.ogg','sound/ambience/ambigen6.ogg','sound/ambience/ambigen7.ogg','sound/ambience/ambigen8.ogg','sound/ambience/ambigen9.ogg','sound/ambience/ambigen10.ogg','sound/ambience/ambigen11.ogg','sound/ambience/ambigen12.ogg') holomap_color = HOLOMAP_AREACOLOR_CREW + +/area/map_template + abstract_type = /area/map_template \ No newline at end of file diff --git a/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm b/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm index 6964c4dbd2f..2a225bae12c 100644 --- a/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm +++ b/code/modules/maps/template_types/random_exoplanet/random_planet_areas.dm @@ -1,5 +1,6 @@ ///Windy surface /area/exoplanet + // not abstract, this can get instantiated name = "\improper Planetary surface" ambience = list( 'sound/effects/wind/wind_2_1.ogg', diff --git a/code/modules/turbolift/turbolift_areas.dm b/code/modules/turbolift/turbolift_areas.dm index 893f07ab67c..8874a14f998 100644 --- a/code/modules/turbolift/turbolift_areas.dm +++ b/code/modules/turbolift/turbolift_areas.dm @@ -1,5 +1,6 @@ // Used for creating the exchange areas. /area/turbolift + abstract_type = /area/turbolift name = "\improper Turbolift" base_turf = /turf/open requires_power = FALSE diff --git a/code/unit_tests/~unit_test_types.dm b/code/unit_tests/~unit_test_types.dm index 71ba1939b5a..72fa0be9eb7 100644 --- a/code/unit_tests/~unit_test_types.dm +++ b/code/unit_tests/~unit_test_types.dm @@ -45,6 +45,9 @@ /obj/unit_test/transparent opacity = FALSE +/area/test_area + abstract_type = /area/test_area + /area/test_area/general icon_state = "blue" diff --git a/maps/away/derelict/derelict_areas.dm b/maps/away/derelict/derelict_areas.dm index 3ac3f4cceb7..a07f86784dd 100644 --- a/maps/away/derelict/derelict_areas.dm +++ b/maps/away/derelict/derelict_areas.dm @@ -1,3 +1,6 @@ +/area/derelict + abstract_type = /area/derelict + /area/derelict/ship name = "\improper Abandoned Ship" icon_state = "yellow" diff --git a/maps/away/errant_pisces/errant_pisces_areas.dm b/maps/away/errant_pisces/errant_pisces_areas.dm index dedee8fc025..dc6fef3b065 100644 --- a/maps/away/errant_pisces/errant_pisces_areas.dm +++ b/maps/away/errant_pisces/errant_pisces_areas.dm @@ -1,4 +1,5 @@ /area/errant_pisces + abstract_type = /area/errant_pisces icon = 'maps/away/errant_pisces/icons/areas.dmi' /area/errant_pisces/bow_port diff --git a/maps/away/liberia/liberia_areas.dm b/maps/away/liberia/liberia_areas.dm index de18310846c..4e14d0cb731 100644 --- a/maps/away/liberia/liberia_areas.dm +++ b/maps/away/liberia/liberia_areas.dm @@ -1,4 +1,5 @@ /area/liberia + abstract_type = /area/liberia req_access = list(access_merchant) /area/liberia/dockinghall diff --git a/maps/away/lost_supply_base/lost_supply_base_areas.dm b/maps/away/lost_supply_base/lost_supply_base_areas.dm index c33069e5e7a..2f58f748ba0 100644 --- a/maps/away/lost_supply_base/lost_supply_base_areas.dm +++ b/maps/away/lost_supply_base/lost_supply_base_areas.dm @@ -6,19 +6,15 @@ /area/lost_supply_base/solar name = "\improper Abandoned supply station solars control room" icon_state = "lost_supply_base_solar" - icon = 'maps/away/lost_supply_base/lost_supply_base_sprites.dmi' /area/lost_supply_base/office name = "\improper Abandoned supply station office" icon_state = "lost_supply_base_office" - icon = 'maps/away/lost_supply_base/lost_supply_base_sprites.dmi' /area/lost_supply_base/supply name = "\improper Abandoned supply station supplies room" icon_state = "lost_supply_base_supply" - icon = 'maps/away/lost_supply_base/lost_supply_base_sprites.dmi' /area/lost_supply_base/common name = "\improper Abandoned supply station common area" - icon_state = "lost_supply_base_common" - icon = 'maps/away/lost_supply_base/lost_supply_base_sprites.dmi' \ No newline at end of file + icon_state = "lost_supply_base_common" \ No newline at end of file diff --git a/maps/away/magshield/magshield_areas.dm b/maps/away/magshield/magshield_areas.dm index 85a429168f8..0ff5d9ace63 100644 --- a/maps/away/magshield/magshield_areas.dm +++ b/maps/away/magshield/magshield_areas.dm @@ -1,29 +1,27 @@ +/area/magshield + abstract_type = /area/magshield + icon = 'magshield_sprites.dmi' + /area/magshield/south name = "Orbital Station South Wing" icon_state = "south" - icon = 'magshield_sprites.dmi' /area/magshield/north name = "Orbital Station North Wing" icon_state = "north" - icon = 'magshield_sprites.dmi' /area/magshield/east name = "Orbital Station East Wing" icon_state = "east" - icon = 'magshield_sprites.dmi' /area/magshield/west name = "Orbital Station West Wing" icon_state = "west" - icon = 'magshield_sprites.dmi' /area/magshield/engine name = "Orbital Station Engine" icon_state = "engine" - icon = 'magshield_sprites.dmi' /area/magshield/smes_storage name = "Orbital Station SMES Battery Room" icon_state = "smes_storage" - icon = 'magshield_sprites.dmi' diff --git a/maps/away/mining/mining_areas.dm b/maps/away/mining/mining_areas.dm index 3dd6da72529..f786fa4c844 100644 --- a/maps/away/mining/mining_areas.dm +++ b/maps/away/mining/mining_areas.dm @@ -1,5 +1,6 @@ // GENERIC MINING AREAS /area/mine + abstract_type = /area/mine icon_state = "mining" ambience = list('sound/ambience/ambimine.ogg', 'sound/ambience/song_game.ogg') sound_env = ASTEROID @@ -15,9 +16,12 @@ icon_state = "unexplored" // OUTPOSTS +/area/outpost + abstract_type = /area/outpost + icon_state = "dark" + /area/outpost/abandoned name = "Abandoned Outpost" - icon_state = "dark" /area/djstation name = "\improper Listening Post" diff --git a/maps/away/smugglers/smugglers_areas.dm b/maps/away/smugglers/smugglers_areas.dm index 4736cb8a0f0..be6b6277b23 100644 --- a/maps/away/smugglers/smugglers_areas.dm +++ b/maps/away/smugglers/smugglers_areas.dm @@ -1,14 +1,15 @@ +/area/smugglers + abstract_type = /area/smugglers + icon = 'smugglers_sprites.dmi' + /area/smugglers/base name = "\improper Asteroid Base" icon_state = "smgl_base" - icon = 'smugglers_sprites.dmi' /area/smugglers/office name = "\improper Asteroid Base Office" icon_state = "smgl_office" - icon = 'smugglers_sprites.dmi' /area/smugglers/dorms name = "\improper Asteroid Base Rest Area" - icon_state = "smgl_dorms" - icon = 'smugglers_sprites.dmi' \ No newline at end of file + icon_state = "smgl_dorms" \ No newline at end of file diff --git a/maps/away/unishi/unishi_areas.dm b/maps/away/unishi/unishi_areas.dm index b09d7b9fb5e..f8e1c52698d 100644 --- a/maps/away/unishi/unishi_areas.dm +++ b/maps/away/unishi/unishi_areas.dm @@ -1,5 +1,6 @@ -/area/unishi/ - icon = 'unishi.dmi' +/area/unishi + abstract_type = /area/unishi + icon = 'unishi.dmi' /area/unishi/bridge name = "\improper Bridge" diff --git a/maps/away/yacht/yacht_areas.dm b/maps/away/yacht/yacht_areas.dm index 92795b8720b..f85268abb67 100644 --- a/maps/away/yacht/yacht_areas.dm +++ b/maps/away/yacht/yacht_areas.dm @@ -1,12 +1,15 @@ +/area/yacht + abstract_type = /area/yacht + icon = 'yacht_icons.dmi' + /area/yacht/bridge name = "\improper Yacht Bridge" icon_state = "bridge" - icon = 'yacht_icons.dmi' + /area/yacht/living name = "\improper Yacht Living" icon_state = "living" - icon = 'yacht_icons.dmi' + /area/yacht/engine name = "\improper Yacht Engine" - icon_state = "engine" - icon = 'yacht_icons.dmi' \ No newline at end of file + icon_state = "engine" \ No newline at end of file diff --git a/maps/example/example_areas.dm b/maps/example/example_areas.dm index 097163dd93f..39eb70c9cbd 100644 --- a/maps/example/example_areas.dm +++ b/maps/example/example_areas.dm @@ -1,4 +1,5 @@ /area/example + abstract_type = /area/example holomap_color = HOLOMAP_AREACOLOR_CREW /area/example/first @@ -14,6 +15,7 @@ icon_state = "storage" /area/turbolift/example + abstract_type = /area/turbolift/example name = "\improper Testing Site Elevator" icon_state = "shuttle" requires_power = FALSE diff --git a/maps/exodus/exodus_areas.dm b/maps/exodus/exodus_areas.dm index 336403c6218..516fb098544 100644 --- a/maps/exodus/exodus_areas.dm +++ b/maps/exodus/exodus_areas.dm @@ -11,6 +11,7 @@ //Do not remove dots after comments /area/exodus + abstract_type = /area/exodus secure = TRUE holomap_color = HOLOMAP_AREACOLOR_CREW diff --git a/maps/tradeship/tradeship_areas.dm b/maps/tradeship/tradeship_areas.dm index 1d46436626c..8451df7b1c1 100644 --- a/maps/tradeship/tradeship_areas.dm +++ b/maps/tradeship/tradeship_areas.dm @@ -10,8 +10,6 @@ /area/ship/trade name = "\improper Tradeship" ambience = list('sound/ambience/ambigen3.ogg','sound/ambience/ambigen4.ogg','sound/ambience/ambigen5.ogg','sound/ambience/ambigen6.ogg','sound/ambience/ambigen7.ogg','sound/ambience/ambigen8.ogg','sound/ambience/ambigen9.ogg','sound/ambience/ambigen10.ogg','sound/ambience/ambigen11.ogg','sound/ambience/ambigen12.ogg') - -/area/ship/trade holomap_color = HOLOMAP_AREACOLOR_CREW /area/ship/trade/crew diff --git a/maps/~mapsystem/maps_unit_testing.dm b/maps/~mapsystem/maps_unit_testing.dm index 2ac13dfa9bb..e90ef209923 100644 --- a/maps/~mapsystem/maps_unit_testing.dm +++ b/maps/~mapsystem/maps_unit_testing.dm @@ -23,18 +23,13 @@ // These areas are used specifically by code and need to be broken out somehow var/list/area_usage_test_exempted_areas = list( - /area/ship, - /area/hallway, - /area/maintenance, /area/overmap, - /area/shuttle, /area/template_noop ) var/list/area_usage_test_exempted_root_areas = list( /area/map_template, /area/exoplanet, - /area/turbolift ) var/list/area_purity_test_exempt_areas = list() diff --git a/mods/content/corporate/away_sites/lar_maria/lar_maria_areas.dm b/mods/content/corporate/away_sites/lar_maria/lar_maria_areas.dm index 65115dbaf27..7aa337a145e 100644 --- a/mods/content/corporate/away_sites/lar_maria/lar_maria_areas.dm +++ b/mods/content/corporate/away_sites/lar_maria/lar_maria_areas.dm @@ -1,4 +1,5 @@ /area/lar_maria + abstract_type = /area/lar_maria icon = 'mods/content/corporate/away_sites/lar_maria/lar_maria_sprites.dmi' /////////////////////////////Upper level areas diff --git a/mods/content/fantasy/submaps/_submaps.dm b/mods/content/fantasy/submaps/_submaps.dm index da60d375fd6..b92c691ab14 100644 --- a/mods/content/fantasy/submaps/_submaps.dm +++ b/mods/content/fantasy/submaps/_submaps.dm @@ -47,6 +47,7 @@ area_flags = AREA_FLAG_EXTERNAL | AREA_FLAG_IS_BACKGROUND /area/fantasy/outside/point_of_interest + abstract_type = /area/fantasy/outside/point_of_interest name = "Point Of Interest" description = null area_blurb_category = /area/fantasy/outside/point_of_interest diff --git a/mods/content/government/away_sites/icarus/icarus_areas.dm b/mods/content/government/away_sites/icarus/icarus_areas.dm index 6ebe40524fb..000d9a7c529 100644 --- a/mods/content/government/away_sites/icarus/icarus_areas.dm +++ b/mods/content/government/away_sites/icarus/icarus_areas.dm @@ -1,4 +1,5 @@ /area/icarus + abstract_type = /area/icarus icon = 'mods/content/government/away_sites/icarus/icarus_sprites.dmi' /area/icarus/vessel From afa70c0cef329c9df1a73fa838f15bd4ae1c5f29 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Wed, 8 Jul 2026 23:45:06 -0400 Subject: [PATCH 52/79] Skip abstract areas in area usage test --- code/unit_tests/area_tests.dm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/unit_tests/area_tests.dm b/code/unit_tests/area_tests.dm index fafbf4d00d6..fabf408602f 100644 --- a/code/unit_tests/area_tests.dm +++ b/code/unit_tests/area_tests.dm @@ -82,7 +82,9 @@ /datum/unit_test/areas_shall_be_used/start_test() var/unused_areas = 0 - for(var/area_type in subtypesof(/area)) + for(var/area/area_type as anything in subtypesof(/area)) + if(TYPE_IS_ABSTRACT(area_type)) + continue if(area_type in global.using_map.area_usage_test_exempted_areas) continue if(is_path_in_list(area_type, global.using_map.area_usage_test_exempted_root_areas)) From ae2d45c7f7b84147866ac7d88c6ccf0237a78cf9 Mon Sep 17 00:00:00 2001 From: Tetra Zeta Date: Wed, 8 Jul 2026 23:24:09 -0600 Subject: [PATCH 53/79] apply suggestion --- code/game/turfs/flooring/flooring_sand.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/turfs/flooring/flooring_sand.dm b/code/game/turfs/flooring/flooring_sand.dm index 5e057b64eee..fabb83aaf04 100644 --- a/code/game/turfs/flooring/flooring_sand.dm +++ b/code/game/turfs/flooring/flooring_sand.dm @@ -5,7 +5,7 @@ icon = 'icons/turf/flooring/sand.dmi' icon_base = "sand" icon_edge_layer = FLOOR_EDGE_SAND - color = null + color = null // autoset from material has_base_range = 4 turf_flags = TURF_FLAG_BACKGROUND | TURF_IS_HOLOMAP_PATH | TURF_FLAG_ABSORB_LIQUID force_material = /decl/material/solid/sand From 1368dba8347501defa0e611c76024f665aecf347 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Thu, 9 Jul 2026 01:06:58 -0400 Subject: [PATCH 54/79] Move the holodeck into a modpack --- code/datums/trading/traders/misc.dm | 3 +- .../atmoalter/portable_atmospherics.dm | 2 + code/game/objects/__objs.dm | 2 - code/game/objects/items/books/skill/_skill.dm | 2 + code/game/objects/items/paintkit.dm | 2 + .../items/weapons/grenades/prank_grenades.dm | 17 -- .../items/weapons/grenades/spawnergrenade.dm | 2 +- .../game/objects/items/weapons/tanks/tanks.dm | 2 + code/game/objects/items/weapons/tech_disks.dm | 2 +- .../game/objects/structures/beds/rollerbed.dm | 2 + code/game/objects/structures/racks.dm | 6 - code/game/objects/structures/tables.dm | 20 -- code/game/turfs/flooring/_flooring.dm | 5 +- .../game/turfs/flooring/flooring_holowater.dm | 2 +- code/game/turfs/flooring/flooring_sand.dm | 4 +- code/game/turfs/flooring/flooring_snow.dm | 4 +- .../components/unary/unary_base.dm | 2 + .../crafting/stack_recipes/_recipe_getter.dm | 2 +- code/modules/decoration/decoration_item.dm | 2 + code/modules/economy/_worth.dm | 4 + code/modules/economy/worth_cash.dm | 4 +- code/modules/economy/worth_clothing.dm | 2 +- code/modules/economy/worth_items.dm | 8 +- code/modules/economy/worth_mob.dm | 4 + code/modules/economy/worth_obj.dm | 2 - .../imprinter/designs_misc_circuits.dm | 3 - code/modules/gemstones/_gemstone.dm | 2 + code/modules/hydroponics/seed_packets.dm | 2 + code/modules/materials/_materials.dm | 8 +- .../solids/materials_solid_metal.dm | 4 +- .../solids/materials_solid_organic.dm | 2 +- .../solids/materials_solid_wood.dm | 2 +- .../reagent_containers/food/meat/cubes.dm | 2 + .../research/design_database_analyzer.dm | 6 +- maps/modpack_testing/modpack_testing.dm | 1 + maps/~mapsystem/maps.dm | 9 - mods/content/holodeck/_holodeck.dm | 3 + mods/content/holodeck/_holodeck.dme | 18 ++ mods/content/holodeck/holo_items.dm | 8 + mods/content/holodeck/holo_mobs.dm | 61 ++++++ .../content/holodeck/holo_objects.dm | 200 +++--------------- mods/content/holodeck/holo_racks.dm | 12 ++ mods/content/holodeck/holo_tables.dm | 22 ++ mods/content/holodeck/holo_turfs.dm | 114 ++++++++++ .../holodeck/holodeck_control_circuit.dm | 0 .../holodeck/holodeck_control_console.dm | 1 + mods/content/holodeck/holodeck_designs.dm | 2 + .../content/holodeck/holodeck_programs.dm | 0 mods/content/holodeck/maps_holodeck.dm | 9 + mods/content/holodeck/trader_overrides.dm | 3 + mods/content/xenobiology/slime/items.dm | 2 +- nebula.dme | 4 - 52 files changed, 345 insertions(+), 262 deletions(-) create mode 100644 mods/content/holodeck/_holodeck.dm create mode 100644 mods/content/holodeck/_holodeck.dme create mode 100644 mods/content/holodeck/holo_items.dm create mode 100644 mods/content/holodeck/holo_mobs.dm rename code/modules/holodeck/HolodeckObjects.dm => mods/content/holodeck/holo_objects.dm (58%) create mode 100644 mods/content/holodeck/holo_racks.dm create mode 100644 mods/content/holodeck/holo_tables.dm create mode 100644 mods/content/holodeck/holo_turfs.dm rename code/game/objects/items/circuitboards/computer/holodeckcontrol.dm => mods/content/holodeck/holodeck_control_circuit.dm (100%) rename code/modules/holodeck/HolodeckControl.dm => mods/content/holodeck/holodeck_control_console.dm (99%) create mode 100644 mods/content/holodeck/holodeck_designs.dm rename code/modules/holodeck/HolodeckPrograms.dm => mods/content/holodeck/holodeck_programs.dm (100%) create mode 100644 mods/content/holodeck/maps_holodeck.dm create mode 100644 mods/content/holodeck/trader_overrides.dm diff --git a/code/datums/trading/traders/misc.dm b/code/datums/trading/traders/misc.dm index 6356be83f0d..13c1828ffef 100644 --- a/code/datums/trading/traders/misc.dm +++ b/code/datums/trading/traders/misc.dm @@ -119,8 +119,7 @@ /obj/item/chems/spray/waterflower = TRADER_THIS_TYPE, /obj/item/gun/launcher/pneumatic/small = TRADER_THIS_TYPE, /obj/item/gun/projectile/revolver/capgun = TRADER_THIS_TYPE, - /obj/item/clothing/mask/fakemoustache = TRADER_THIS_TYPE, - /obj/item/grenade/spawnergrenade/fake_carp = TRADER_THIS_TYPE + /obj/item/clothing/mask/fakemoustache = TRADER_THIS_TYPE ) /datum/trader/ship/replica_shop diff --git a/code/game/machinery/atmoalter/portable_atmospherics.dm b/code/game/machinery/atmoalter/portable_atmospherics.dm index b788ebfe1e8..1b635ce452c 100644 --- a/code/game/machinery/atmoalter/portable_atmospherics.dm +++ b/code/game/machinery/atmoalter/portable_atmospherics.dm @@ -11,6 +11,8 @@ var/start_pressure = ONE_ATMOSPHERE /obj/machinery/portable_atmospherics/get_single_monetary_worth() + if(worthless) + return 0 . = ..() for(var/gas_type, gas_amount in air_contents?.gas) var/decl/material/gas_data = GET_DECL(gas_type) diff --git a/code/game/objects/__objs.dm b/code/game/objects/__objs.dm index feebd929384..7ed2505ff8c 100644 --- a/code/game/objects/__objs.dm +++ b/code/game/objects/__objs.dm @@ -21,8 +21,6 @@ var/in_use = FALSE // If we have a user using us, this will be set on. We will check if the user has stopped using us, and thus stop updating and LAGGING EVERYTHING! var/armor_penetration = 0 var/anchor_fall = FALSE - /// if the obj is a holographic object spawned by the holodeck - var/holographic = FALSE ///JSON list of directions to x,y offsets to be applied to the object depending on its direction EX: @'{"NORTH":{"x":12,"y":5}, "EAST":{"x":10,"y":50}}' var/directional_offset diff --git a/code/game/objects/items/books/skill/_skill.dm b/code/game/objects/items/books/skill/_skill.dm index 39ed2b3cdfe..7f940ad6367 100644 --- a/code/game/objects/items/books/skill/_skill.dm +++ b/code/game/objects/items/books/skill/_skill.dm @@ -113,6 +113,8 @@ Skill books that increase your skills while you activate and hold them limit = 1 // you can only read one book at a time nerd, therefore you can only get one buff at a time /obj/item/book/skill/get_single_monetary_worth() + if(worthless) + return 0 . = max(..(), 200) + (100 * skill_req) /obj/item/book/skill/proc/check_can_read(mob/user) diff --git a/code/game/objects/items/paintkit.dm b/code/game/objects/items/paintkit.dm index 5d8602efb26..27857f5ec34 100644 --- a/code/game/objects/items/paintkit.dm +++ b/code/game/objects/items/paintkit.dm @@ -11,6 +11,8 @@ var/custom = FALSE /obj/item/kit/get_single_monetary_worth() + if(worthless) + return 0 . = max(round(..()), (custom ? 100 : 750) * uses) // Luxury good, value is entirely artificial. /obj/item/kit/get_examine_strings(mob/user, distance, infix, suffix) diff --git a/code/game/objects/items/weapons/grenades/prank_grenades.dm b/code/game/objects/items/weapons/grenades/prank_grenades.dm index 0a884f850ec..1a04949c921 100644 --- a/code/game/objects/items/weapons/grenades/prank_grenades.dm +++ b/code/game/objects/items/weapons/grenades/prank_grenades.dm @@ -4,20 +4,3 @@ /obj/item/grenade/fake/detonate() active = 0 playsound(src.loc, get_sfx("explosion"), 50, 1, 30) - -/obj/item/natural_weapon/bite/fake - _base_attack_force = 0 - -/mob/living/simple_animal/hostile/carp/holodeck/fake - faction = null - natural_weapon = /obj/item/natural_weapon/bite/fake - environment_smash = 0 - ai = /datum/mob_controller/aggressive/carp/fake - -/datum/mob_controller/aggressive/carp/fake - try_destroy_surroundings = FALSE - -/obj/item/grenade/spawnergrenade/fake_carp - origin_tech = @'{"materials":2,"magnets":2,"wormholes":5}' - spawner_type = /mob/living/simple_animal/hostile/carp/holodeck/fake - deliveryamt = 4 diff --git a/code/game/objects/items/weapons/grenades/spawnergrenade.dm b/code/game/objects/items/weapons/grenades/spawnergrenade.dm index a636763ac07..4d15f289bd6 100644 --- a/code/game/objects/items/weapons/grenades/spawnergrenade.dm +++ b/code/game/objects/items/weapons/grenades/spawnergrenade.dm @@ -6,7 +6,7 @@ var/spawner_type = null // must be an object path var/deliveryamt = 1 // amount of type to deliver -/obj/item/grenade/spawnergrenade/fake_carp/detonate() +/obj/item/grenade/spawnergrenade/detonate() if(spawner_type && deliveryamt) var/turf/T = get_turf(src) playsound(T, 'sound/effects/phasein.ogg', 100, 1) diff --git a/code/game/objects/items/weapons/tanks/tanks.dm b/code/game/objects/items/weapons/tanks/tanks.dm index b191b55e56d..cef5d34401a 100644 --- a/code/game/objects/items/weapons/tanks/tanks.dm +++ b/code/game/objects/items/weapons/tanks/tanks.dm @@ -79,6 +79,8 @@ var/global/list/global/tank_gauge_cache = list() . = ..() /obj/item/tank/get_single_monetary_worth() + if(worthless) + return 0 . = ..() for(var/gas_type, gas_amount in air_contents?.gas) var/decl/material/gas_data = GET_DECL(gas_type) diff --git a/code/game/objects/items/weapons/tech_disks.dm b/code/game/objects/items/weapons/tech_disks.dm index c33fceca35b..0e87df82301 100644 --- a/code/game/objects/items/weapons/tech_disks.dm +++ b/code/game/objects/items/weapons/tech_disks.dm @@ -142,4 +142,4 @@ . += "A tiny indicator on \the [src] shows it holds [data] good explorer point\s." /obj/item/disk/survey/get_base_value() - . = holographic ? 0 : (sqrt(data) * 5) + return sqrt(data) * 5 diff --git a/code/game/objects/structures/beds/rollerbed.dm b/code/game/objects/structures/beds/rollerbed.dm index d7e7f102ec6..739c85b1f23 100644 --- a/code/game/objects/structures/beds/rollerbed.dm +++ b/code/game/objects/structures/beds/rollerbed.dm @@ -136,6 +136,8 @@ var/structure_form_type = /obj/structure/bed/roller //The deployed form path. /obj/item/roller/get_single_monetary_worth() + if(worthless) + return 0 . = structure_form_type ? atom_info_repository.get_combined_worth_for(structure_form_type) : ..() /obj/item/roller/attack_self(mob/user) diff --git a/code/game/objects/structures/racks.dm b/code/game/objects/structures/racks.dm index 4830025c353..264000c2020 100644 --- a/code/game/objects/structures/racks.dm +++ b/code/game/objects/structures/racks.dm @@ -44,12 +44,6 @@ auto_align(used_item, click_params) return TRUE -/obj/structure/rack/holorack/dismantle_structure(mob/user) - material = null - reinf_material = null - parts_type = null - . = ..() - /obj/structure/rack/dark color = COLOR_GRAY40 diff --git a/code/game/objects/structures/tables.dm b/code/game/objects/structures/tables.dm index fb9dda2bf28..6ce7d95c8dc 100644 --- a/code/game/objects/structures/tables.dm +++ b/code/game/objects/structures/tables.dm @@ -706,26 +706,6 @@ color = "#8f29a3" reinf_material = /decl/material/solid/glass/borosilicate -/obj/structure/table/holotable - icon_state = "holo_preview" - holographic = TRUE - color = COLOR_OFF_WHITE - material = /decl/material/solid/metal/aluminium/holographic - reinf_material = /decl/material/solid/metal/aluminium/holographic - -/obj/structure/table/holo_plastictable - icon_state = "holo_preview" - holographic = TRUE - color = COLOR_OFF_WHITE - material = /decl/material/solid/organic/plastic/holographic - reinf_material = /decl/material/solid/organic/plastic/holographic - -/obj/structure/table/holo_woodentable - holographic = TRUE - icon_state = "holo_preview" - material = /decl/material/solid/organic/wood/holographic - reinf_material = /decl/material/solid/organic/wood/holographic - //wood wood wood /obj/structure/table/wood icon_state = "solid_preview" diff --git a/code/game/turfs/flooring/_flooring.dm b/code/game/turfs/flooring/_flooring.dm index f30adf01de7..71f162a7fb5 100644 --- a/code/game/turfs/flooring/_flooring.dm +++ b/code/game/turfs/flooring/_flooring.dm @@ -89,7 +89,8 @@ var/global/list/flooring_cache = list() var/render_trenches = TRUE var/floor_layer = TURF_LAYER - var/holographic = FALSE + /// If TRUE, this turf cannot be damaged, painted, pried off, etc. + var/visual_only = FALSE var/dirt_color = /decl/material/solid/soil::color var/list/burned_states @@ -105,7 +106,7 @@ var/global/list/flooring_cache = list() if(!istype(force_material)) force_material = null - if(holographic) + if(visual_only) turf_flags = null damage_temperature = INFINITY build_type = null diff --git a/code/game/turfs/flooring/flooring_holowater.dm b/code/game/turfs/flooring/flooring_holowater.dm index 6bfe235d67e..32f36110eef 100644 --- a/code/game/turfs/flooring/flooring_holowater.dm +++ b/code/game/turfs/flooring/flooring_holowater.dm @@ -6,6 +6,6 @@ icon_base = "fakewater" has_base_range = null footstep_type = /decl/footsteps/water - holographic = TRUE + visual_only = TRUE constructed = TRUE uid = "floor_water_fake" diff --git a/code/game/turfs/flooring/flooring_sand.dm b/code/game/turfs/flooring/flooring_sand.dm index 5b4f13501ea..bf59037f235 100644 --- a/code/game/turfs/flooring/flooring_sand.dm +++ b/code/game/turfs/flooring/flooring_sand.dm @@ -43,7 +43,7 @@ /decl/flooring/sand/fake name = "holosand" desc = "Uncomfortably coarse and gritty for a hologram." - holographic = TRUE + visual_only = TRUE uid = "floor_sand_fake" /decl/flooring/fake_space @@ -52,7 +52,7 @@ icon = 'icons/turf/flooring/fake_space.dmi' icon_base = "space" has_base_range = 25 - holographic = TRUE + visual_only = TRUE gender = NEUTER uid = "floor_space_fake" diff --git a/code/game/turfs/flooring/flooring_snow.dm b/code/game/turfs/flooring/flooring_snow.dm index 72362928c06..d4922e3d30b 100644 --- a/code/game/turfs/flooring/flooring_snow.dm +++ b/code/game/turfs/flooring/flooring_snow.dm @@ -60,7 +60,7 @@ uid = "floor_permafrost" /decl/flooring/permafrost/get_vehicle_transit_delay(obj/vehicle/vehicle) - if(holographic) + if(visual_only) return vehicle::base_speed if(vehicle.vehicle_transit_type == vehicle::VEHICLE_SNOWMOBILE) return 0.8 @@ -69,6 +69,6 @@ /decl/flooring/snow/fake name = "holosnow" desc = "Not quite the same as snow on an entertainment terminal, but close." - holographic = TRUE + visual_only = TRUE uid = "floor_snow_fake" diff --git a/code/modules/atmospherics/components/unary/unary_base.dm b/code/modules/atmospherics/components/unary/unary_base.dm index 21734409daa..0e452755498 100644 --- a/code/modules/atmospherics/components/unary/unary_base.dm +++ b/code/modules/atmospherics/components/unary/unary_base.dm @@ -11,6 +11,8 @@ var/controlled = TRUE // if true, report to air alarm, if false, probably in direct contact with something else by radio (e.g. airlocks) /obj/machinery/atmospherics/unary/get_single_monetary_worth() + if(worthless) + return 0 . = ..() for(var/gas_type, gas_amount in air_contents?.gas) var/decl/material/gas_data = GET_DECL(gas_type) diff --git a/code/modules/crafting/stack_recipes/_recipe_getter.dm b/code/modules/crafting/stack_recipes/_recipe_getter.dm index cf79001263d..7f6896fcb90 100644 --- a/code/modules/crafting/stack_recipes/_recipe_getter.dm +++ b/code/modules/crafting/stack_recipes/_recipe_getter.dm @@ -19,7 +19,7 @@ /proc/get_stack_recipes(decl/material/mat, decl/material/reinf_mat, stack_type, tool_type, flat = FALSE) // No recipes for holograms or fluids. - if(istype(mat) && (mat.holographic || mat.phase_at_temperature() != MAT_PHASE_SOLID)) + if(istype(mat) && (mat.visual_only || mat.phase_at_temperature() != MAT_PHASE_SOLID)) return list() #ifndef UNIT_TEST // key creation is SLOW and in unit testing almost every call to this will be a cache fail diff --git a/code/modules/decoration/decoration_item.dm b/code/modules/decoration/decoration_item.dm index ae628cf68bf..034ebbb7871 100644 --- a/code/modules/decoration/decoration_item.dm +++ b/code/modules/decoration/decoration_item.dm @@ -74,6 +74,8 @@ return TRUE /obj/item/get_single_monetary_worth() + if(worthless) + return 0 . = ..() var/base_value = . for(var/decl/item_decoration/decoration as anything in decorations) diff --git a/code/modules/economy/_worth.dm b/code/modules/economy/_worth.dm index f2f476a776b..cceeb5e0ae7 100644 --- a/code/modules/economy/_worth.dm +++ b/code/modules/economy/_worth.dm @@ -1,4 +1,6 @@ /atom + /// If TRUE, this is worthless. Its contents will still be properly valued by get_contents_monetary_worth(), however. + var/worthless = FALSE var/monetary_worth_multiplier = 1 /atom/proc/get_base_value() @@ -8,6 +10,8 @@ . = monetary_worth_multiplier /atom/proc/get_single_monetary_worth() + if(worthless) + return 0 . = get_base_value() * get_value_multiplier() if(reagents) for(var/decl/material/reagent as anything in REAGENT_VOLUMES(reagents)) diff --git a/code/modules/economy/worth_cash.dm b/code/modules/economy/worth_cash.dm index 49d04f24ebd..84db32d7d5e 100644 --- a/code/modules/economy/worth_cash.dm +++ b/code/modules/economy/worth_cash.dm @@ -42,7 +42,7 @@ update_from_worth() /obj/item/cash/get_base_value() - . = holographic ? 0 : absolute_worth + return absolute_worth /obj/item/cash/proc/set_currency(var/new_currency) currency = new_currency @@ -245,7 +245,7 @@ . += SPAN_NOTICE("[capitalize(cur.name)] remaining: [floor(loaded_worth / cur.absolute_value)].") /obj/item/charge_stick/get_base_value() - . = holographic ? 0 : loaded_worth + return loaded_worth /obj/item/charge_stick/attackby(var/obj/item/used_item, var/mob/user) var/datum/extension/lockable/lock = get_extension(src, /datum/extension/lockable) diff --git a/code/modules/economy/worth_clothing.dm b/code/modules/economy/worth_clothing.dm index bf1a039887b..7a0a45548d3 100644 --- a/code/modules/economy/worth_clothing.dm +++ b/code/modules/economy/worth_clothing.dm @@ -1,6 +1,6 @@ /obj/item/clothing/get_base_value() . = max(..(), 10) - if(!holographic && flash_protection > 0) + if(flash_protection > 0) . += flash_protection * 25 /obj/item/clothing/head/collectable/get_value_multiplier() diff --git a/code/modules/economy/worth_items.dm b/code/modules/economy/worth_items.dm index 15998a7521f..bea9943f41e 100644 --- a/code/modules/economy/worth_items.dm +++ b/code/modules/economy/worth_items.dm @@ -3,10 +3,6 @@ #define BASE_ARMOUR_WORTH 50 /obj/item/get_base_value() - - if(holographic) - return 0 - . = ..() if(origin_tech) @@ -75,7 +71,7 @@ #undef MUNDANE_ARMOUR_VALUE #undef BASE_ARMOUR_WORTH -/obj/item/organ/get_single_monetary_worth() +/obj/item/organ/get_value_multiplier() . = ..() if(species) - . = round(. * species.rarity_value) + . *= species.rarity_value diff --git a/code/modules/economy/worth_mob.dm b/code/modules/economy/worth_mob.dm index 7102d7c6809..d4bb79d02ec 100644 --- a/code/modules/economy/worth_mob.dm +++ b/code/modules/economy/worth_mob.dm @@ -5,6 +5,8 @@ . = max(round(.), mob_size) /mob/living/get_single_monetary_worth() + if(worthless) + return 0 . = ..() for(var/atom/movable/organ in get_organs()) . += organ.get_combined_monetary_worth() @@ -14,5 +16,7 @@ . = round(.) /mob/living/get_value_multiplier() + if(worthless) + return 0 var/decl/species/my_species = get_species() . = my_species ? my_species.rarity_value : 1 diff --git a/code/modules/economy/worth_obj.dm b/code/modules/economy/worth_obj.dm index 04b75a13fed..3e89c43e68c 100644 --- a/code/modules/economy/worth_obj.dm +++ b/code/modules/economy/worth_obj.dm @@ -18,8 +18,6 @@ . = length(matter) ? ..() : (material?.value || 1) /obj/get_base_value() - if(holographic) - return 0 if(length(matter)) . = 0 for(var/mat in matter) diff --git a/code/modules/fabrication/designs/imprinter/designs_misc_circuits.dm b/code/modules/fabrication/designs/imprinter/designs_misc_circuits.dm index 474b8c3e409..902b95c986f 100644 --- a/code/modules/fabrication/designs/imprinter/designs_misc_circuits.dm +++ b/code/modules/fabrication/designs/imprinter/designs_misc_circuits.dm @@ -83,9 +83,6 @@ /datum/fabricator_recipe/imprinter/circuit/accounts path = /obj/item/stock_parts/circuitboard/account_database -/datum/fabricator_recipe/imprinter/circuit/holo - path = /obj/item/stock_parts/circuitboard/holodeck_control - /datum/fabricator_recipe/imprinter/circuit/aiupload path = /obj/item/stock_parts/circuitboard/aiupload diff --git a/code/modules/gemstones/_gemstone.dm b/code/modules/gemstones/_gemstone.dm index 02d698d4a13..7bee9c244a8 100644 --- a/code/modules/gemstones/_gemstone.dm +++ b/code/modules/gemstones/_gemstone.dm @@ -35,6 +35,8 @@ var/global/list/_available_gemstone_cuts SetName("[cut.adjective] [material.solid_name]") /obj/item/gemstone/get_single_monetary_worth() + if(worthless) + return 0 . = ..() * cut.worth_multiplier /obj/item/gemstone/attackby(obj/item/used_item, mob/user) diff --git a/code/modules/hydroponics/seed_packets.dm b/code/modules/hydroponics/seed_packets.dm index c86b81ef2e9..e0a70230689 100644 --- a/code/modules/hydroponics/seed_packets.dm +++ b/code/modules/hydroponics/seed_packets.dm @@ -25,6 +25,8 @@ add_to_reagents(/decl/material/liquid/oil/plant, 3) /obj/item/seeds/get_single_monetary_worth() + if(worthless) + return 0 . = seed ? seed.get_monetary_value() : ..() // Used for extracts/seed sampling purposes. diff --git a/code/modules/materials/_materials.dm b/code/modules/materials/_materials.dm index 02ab0d63cd4..3f4eaa2fbff 100644 --- a/code/modules/materials/_materials.dm +++ b/code/modules/materials/_materials.dm @@ -278,7 +278,7 @@ var/global/list/materials_by_gas_symbol = list() var/sound_manipulate //Default sound something like a material stack made of this material does when picked up var/sound_dropped //Default sound something like a material stack made of this material does when hitting the ground or placed down - var/holographic // Set to true if this material is fake/visual only. + var/visual_only // Set to true if this material is fake/visual only. Can be used for holograms, placeholders, etc. /// Does high temperature baking change this material into something else? var/bakes_into_material @@ -338,7 +338,7 @@ var/global/list/materials_by_gas_symbol = list() hidden_from_codex = TRUE exoplanet_rarity_plant = MAT_RARITY_NOWHERE exoplanet_rarity_gas = MAT_RARITY_NOWHERE - holographic = TRUE + visual_only = TRUE // Make sure we have a use name and shard icon even if they aren't explicitly set. /decl/material/Initialize() @@ -356,7 +356,7 @@ var/global/list/materials_by_gas_symbol = list() adjective_name ||= use_name // Null/clear a bunch of physical vars as this material is fake. - if(holographic) + if(visual_only) temperature_burn_milestone_material = null can_boil_to_gas = FALSE shard_name = SHARD_NONE @@ -408,7 +408,7 @@ var/global/list/materials_by_gas_symbol = list() global.materials_by_gas_symbol[gas_symbol] = type generate_armor_values() - if(!holographic) + if(!visual_only) var/list/cocktails = decls_repository.get_decls_of_subtype(/decl/cocktail) for(var/ctype in cocktails) var/decl/cocktail/cocktail = cocktails[ctype] diff --git a/code/modules/materials/definitions/solids/materials_solid_metal.dm b/code/modules/materials/definitions/solids/materials_solid_metal.dm index d5abbbb0350..19c069e9bc5 100644 --- a/code/modules/materials/definitions/solids/materials_solid_metal.dm +++ b/code/modules/materials/definitions/solids/materials_solid_metal.dm @@ -222,7 +222,7 @@ /decl/material/solid/metal/steel/holographic name = "holographic steel" uid = "solid_holographic_steel" - holographic = TRUE + visual_only = TRUE /decl/material/solid/metal/stainlesssteel name = "stainless steel" @@ -269,7 +269,7 @@ /decl/material/solid/metal/aluminium/holographic name = "holoaluminium" uid = "solid_holographic_aluminium" - holographic = TRUE + visual_only = TRUE /decl/material/solid/metal/plasteel name = "plasteel" diff --git a/code/modules/materials/definitions/solids/materials_solid_organic.dm b/code/modules/materials/definitions/solids/materials_solid_organic.dm index be9b91441d1..1a44d59c288 100644 --- a/code/modules/materials/definitions/solids/materials_solid_organic.dm +++ b/code/modules/materials/definitions/solids/materials_solid_organic.dm @@ -76,7 +76,7 @@ /decl/material/solid/organic/plastic/holographic name = "holographic plastic" uid = "solid_holographic_plastic" - holographic = TRUE + visual_only = TRUE /decl/material/solid/organic/cardboard name = "cardboard" diff --git a/code/modules/materials/definitions/solids/materials_solid_wood.dm b/code/modules/materials/definitions/solids/materials_solid_wood.dm index 0a3b021339b..9e44c5f6d15 100644 --- a/code/modules/materials/definitions/solids/materials_solid_wood.dm +++ b/code/modules/materials/definitions/solids/materials_solid_wood.dm @@ -82,7 +82,7 @@ uid = "solid_holographic_wood" color = WOOD_COLOR_CHOCOLATE //the very concept of wood should be brown adjective_name = "holowood" - holographic = TRUE + visual_only = TRUE /decl/material/solid/organic/wood/mahogany name = "mahogany" diff --git a/code/modules/reagents/reagent_containers/food/meat/cubes.dm b/code/modules/reagents/reagent_containers/food/meat/cubes.dm index 13f68070877..3dbbd1ab517 100644 --- a/code/modules/reagents/reagent_containers/food/meat/cubes.dm +++ b/code/modules/reagents/reagent_containers/food/meat/cubes.dm @@ -36,6 +36,8 @@ add_to_reagents(/decl/material/solid/organic/meat, 10) /obj/item/food/animal_cube/get_single_monetary_worth() + if(worthless) + return 0 . = (spawn_type ? round(atom_info_repository.get_combined_worth_for((islist(spawn_type) ? spawn_type[1] : spawn_type)) * 1.25) : 5) if(wrapper_type) . += atom_info_repository.get_combined_worth_for(wrapper_type) diff --git a/code/modules/research/design_database_analyzer.dm b/code/modules/research/design_database_analyzer.dm index 6d57b82a063..0c1d18a2ad6 100644 --- a/code/modules/research/design_database_analyzer.dm +++ b/code/modules/research/design_database_analyzer.dm @@ -80,6 +80,10 @@ D.ui_interact(user) return TRUE +/// Returns TRUE if used_item can be deconstructed, assuming it meets other criteria (tech level, etc.) +/obj/machinery/destructive_analyzer/proc/can_deconstruct(var/obj/item/used_item) + return TRUE + /obj/machinery/destructive_analyzer/attackby(var/obj/item/used_item, var/mob/user) if(IS_MULTITOOL(used_item) && !user.check_intent(I_FLAG_HARM)) @@ -106,7 +110,7 @@ return TRUE var/list/techlvls = cached_json_decode(tech) - if(!length(techlvls) || used_item.holographic) + if(!length(techlvls) || !can_deconstruct(used_item)) to_chat(user, SPAN_WARNING("You cannot deconstruct this item.")) return TRUE diff --git a/maps/modpack_testing/modpack_testing.dm b/maps/modpack_testing/modpack_testing.dm index 47e16e7a41b..0c458cd9893 100644 --- a/maps/modpack_testing/modpack_testing.dm +++ b/maps/modpack_testing/modpack_testing.dm @@ -21,6 +21,7 @@ #include "../../mods/content/fantasy/_fantasy.dme" #include "../../mods/content/generic_shuttles/_generic_shuttles.dme" #include "../../mods/content/government/_government.dme" + #include "../../mods/content/holodeck/_holodeck.dme" #include "../../mods/content/inertia/_inertia.dme" #include "../../mods/content/integrated_electronics/_integrated_electronics.dme" #include "../../mods/content/item_sharpening/_item_sharpening.dme" diff --git a/maps/~mapsystem/maps.dm b/maps/~mapsystem/maps.dm index 5ddba61742d..82100776d7e 100644 --- a/maps/~mapsystem/maps.dm +++ b/maps/~mapsystem/maps.dm @@ -74,15 +74,6 @@ var/global/const/MAP_HAS_RANK = 2 //Rank system, also toggleable var/emergency_shuttle_recall_message var/emergency_shuttle_arriving_at_dock_message - var/list/holodeck_programs = list() // map of string ids to /datum/holodeck_program instances - var/list/holodeck_supported_programs = list() // map of maps - first level maps from list-of-programs string id (e.g. "BarPrograms") to another map - // this is in order to support multiple holodeck program listings for different holodecks - // second level maps from program friendly display names ("Picnic Area") to program string ids ("picnicarea") - // as defined in holodeck_programs - var/list/holodeck_restricted_programs = list() // as above... but EVIL! - var/list/holodeck_default_program = list() // map of program list string ids to default program string id - var/list/holodeck_off_program = list() // as above... but for being off i guess - var/allowed_latejoin_spawns = list( /decl/spawnpoint/arrivals ) diff --git a/mods/content/holodeck/_holodeck.dm b/mods/content/holodeck/_holodeck.dm new file mode 100644 index 00000000000..491e56bf744 --- /dev/null +++ b/mods/content/holodeck/_holodeck.dm @@ -0,0 +1,3 @@ +/decl/modpack/holodeck + name = "Holodecks and Hardlight Holograms" + desc = "Adds holodecks and support for hardlight hologram objects." \ No newline at end of file diff --git a/mods/content/holodeck/_holodeck.dme b/mods/content/holodeck/_holodeck.dme new file mode 100644 index 00000000000..80d4c5be2cd --- /dev/null +++ b/mods/content/holodeck/_holodeck.dme @@ -0,0 +1,18 @@ +#ifndef CONTENT_PACK_HOLODECK +#define CONTENT_PACK_HOLODECK +// BEGIN_INCLUDE +#include "_holodeck.dm" +#include "holo_items.dm" +#include "holo_mobs.dm" +#include "holo_objects.dm" +#include "holo_racks.dm" +#include "holo_tables.dm" +#include "holo_turfs.dm" +#include "holodeck_control_circuit.dm" +#include "holodeck_control_console.dm" +#include "holodeck_designs.dm" +#include "holodeck_programs.dm" +#include "maps_holodeck.dm" +#include "trader_overrides.dm" +// END_INCLUDE +#endif \ No newline at end of file diff --git a/mods/content/holodeck/holo_items.dm b/mods/content/holodeck/holo_items.dm new file mode 100644 index 00000000000..b62da36bd4f --- /dev/null +++ b/mods/content/holodeck/holo_items.dm @@ -0,0 +1,8 @@ +/obj/machinery/destructive_analyzer/can_deconstruct(var/obj/item/used_item) + if(used_item.holographic) + return FALSE + +/obj/item/grenade/spawnergrenade/fake_carp + origin_tech = @'{"materials":2,"magnets":2,"wormholes":5}' + spawner_type = /mob/living/simple_animal/hostile/carp/holodeck/fake + deliveryamt = 4 \ No newline at end of file diff --git a/mods/content/holodeck/holo_mobs.dm b/mods/content/holodeck/holo_mobs.dm new file mode 100644 index 00000000000..a9e914b20f6 --- /dev/null +++ b/mods/content/holodeck/holo_mobs.dm @@ -0,0 +1,61 @@ +//Holocarp + +/mob/living/simple_animal/hostile/carp/holodeck + icon = 'icons/mob/simple_animal/holocarp.dmi' + alpha = 127 + butchery_data = null + worthless = TRUE + +/mob/living/simple_animal/hostile/carp/holodeck/carp_randomify() + return + +/mob/living/simple_animal/hostile/carp/holodeck/on_update_icon() + SHOULD_CALL_PARENT(FALSE) + return + +/mob/living/simple_animal/hostile/carp/holodeck/Initialize() + . = ..() + set_light(2) //hologram lighting + +/mob/living/simple_animal/hostile/carp/holodeck/proc/set_safety(var/safe) + if (safe) + faction = MOB_FACTION_NEUTRAL + natural_weapon.set_base_attack_force(0) + environment_smash = 0 + ai?.try_destroy_surroundings = FALSE + else + faction = "carp" + natural_weapon.set_base_attack_force(natural_weapon.get_initial_base_attack_force()) + +/mob/living/simple_animal/hostile/carp/holodeck/gib(do_gibs = TRUE) + SHOULD_CALL_PARENT(FALSE) + if(stat != DEAD) + death(gibbed = TRUE) + if(stat == DEAD) + qdel(src) + return TRUE + return FALSE + +/mob/living/simple_animal/hostile/carp/holodeck/get_death_message(gibbed) + return "fades away..." + +/mob/living/simple_animal/hostile/carp/holodeck/get_self_death_message(gibbed) + return "You have been destroyed." + +/mob/living/simple_animal/hostile/carp/holodeck/death(gibbed) + . = ..() + if(. && !gibbed) + gib() + +// Non-dangerous holocarp +/mob/living/simple_animal/hostile/carp/holodeck/fake + faction = null + natural_weapon = /obj/item/natural_weapon/bite/fake + environment_smash = 0 + ai = /datum/mob_controller/aggressive/carp/fake + +/obj/item/natural_weapon/bite/fake + _base_attack_force = 0 + +/datum/mob_controller/aggressive/carp/fake + try_destroy_surroundings = FALSE \ No newline at end of file diff --git a/code/modules/holodeck/HolodeckObjects.dm b/mods/content/holodeck/holo_objects.dm similarity index 58% rename from code/modules/holodeck/HolodeckObjects.dm rename to mods/content/holodeck/holo_objects.dm index 407b1b196da..d1125a62467 100644 --- a/code/modules/holodeck/HolodeckObjects.dm +++ b/mods/content/holodeck/holo_objects.dm @@ -3,119 +3,9 @@ // Holographic tables are in code/modules/tables/presets.dm // Holographic racks are in code/modules/tables/rack.dm -/turf/floor/holofloor - thermal_conductivity = 0 - -/turf/floor/holofloor/get_lumcount(var/minlum = 0, var/maxlum = 1) - return 0.8 - -/turf/floor/holofloor/attackby(obj/item/used_item, mob/user) - return TRUE - // HOLOFLOOR DOES NOT GIVE A FUCK - -/turf/floor/holofloor/carpet - name = "brown carpet" - icon = 'icons/turf/flooring/carpet.dmi' - icon_state = "brown" - _flooring = /decl/flooring/carpet - -/turf/floor/holofloor/concrete - name = "brown carpet" - icon = 'icons/turf/flooring/carpet.dmi' - icon_state = "brown" - _flooring = /decl/flooring/carpet - -/turf/floor/holofloor/concrete - name = "floor" - icon = 'icons/turf/flooring/misc.dmi' - icon_state = "concrete" - _flooring = null - -/turf/floor/holofloor/tiled - name = "floor" - icon = 'icons/turf/flooring/tiles.dmi' - icon_state = "steel" - _flooring = /decl/flooring/tiling - -/turf/floor/holofloor/tiled/dark - name = "dark floor" - icon_state = "dark" - _flooring = /decl/flooring/tiling/dark - -/turf/floor/holofloor/tiled/stone - name = "stone floor" - icon_state = "stone" - _flooring = /decl/flooring/tiling/stone - -/turf/floor/holofloor/lino - name = "lino" - icon = 'icons/turf/flooring/linoleum.dmi' - icon_state = "lino" - _flooring = /decl/flooring/linoleum - -/turf/floor/holofloor/wood - name = "wooden floor" - icon = 'icons/turf/flooring/wood.dmi' - icon_state = "wood0" - color = WOOD_COLOR_CHOCOLATE - _flooring = /decl/flooring/wood - -/turf/floor/holofloor/grass - name = "lush grass" - icon = 'icons/turf/flooring/fakegrass.dmi' - icon_state = "grass0" - _flooring = /decl/flooring/grass/fake - -/turf/floor/holofloor/snow - name = "snow" - icon = 'icons/turf/flooring/snow.dmi' - icon_state = "snow0" - _flooring = /decl/flooring/snow/fake - -/turf/floor/holofloor/space - name = "\proper space" - icon = 'icons/turf/flooring/fake_space.dmi' - icon_state = "space0" - _flooring = /decl/flooring/fake_space - -/turf/floor/holofloor/reinforced - name = "reinforced holofloor" - icon = 'icons/turf/flooring/tiles.dmi' - _flooring = /decl/flooring/reinforced - icon_state = "reinforced" - -/turf/floor/holofloor/beach - desc = "Uncomfortably gritty for a hologram." - icon = 'icons/misc/beach.dmi' - _flooring = /decl/flooring/sand/fake - abstract_type = /turf/floor/holofloor/beach - -/turf/floor/holofloor/beach/sand - name = "sand" - icon_state = "desert0" - -/turf/floor/holofloor/beach/coastline - name = "coastline" - icon = 'icons/misc/beach2.dmi' - icon_state = "sandwater" - _flooring = /decl/flooring/sand/fake - -/turf/floor/holofloor/beach/water - name = "water" - icon_state = "seashallow" - _flooring = /decl/flooring/fake_water - -/turf/floor/holofloor/desert - name = "desert sand" - desc = "Uncomfortably gritty for a hologram." - icon = 'icons/turf/flooring/barren.dmi' - icon_state = "barren" - _flooring = /decl/flooring/sand/fake - -/turf/floor/holofloor/desert/Initialize(var/ml) - . = ..() - if(prob(10)) - LAZYADD(decals, image('icons/turf/flooring/decals.dmi', "asteroid[rand(0,9)]")) +/obj + /// if the obj is a holographic object spawned by the holodeck + var/holographic = FALSE /obj/structure/holostool name = "stool" @@ -123,10 +13,18 @@ icon = 'icons/obj/furniture.dmi' icon_state = "stool_padded_preview" anchored = TRUE + worthless = TRUE + holographic = TRUE /obj/item/clothing/gloves/boxing/hologlove name = "boxing gloves" desc = "Because you really needed another excuse to punch your crewmates." + worthless = TRUE + holographic = TRUE + +/obj/structure/window/reinforced/holowindow + worthless = TRUE + holographic = TRUE /obj/structure/window/reinforced/holowindow/full dir = NORTHEAST @@ -148,27 +46,31 @@ // This subtype is deleted when a ready button in the same area is pressed. /obj/structure/window/reinforced/holowindow/disappearing +/obj/machinery/door/window/holowindoor + holographic = TRUE + worthless = TRUE + /obj/machinery/door/window/holowindoor/attackby(obj/item/used_item, mob/user) - if (src.operating == 1) + if (operating) return TRUE - if(src.density && istype(used_item, /obj/item) && !istype(used_item, /obj/item/card)) - playsound(src.loc, 'sound/effects/Glasshit.ogg', 75, 1) + if(density && istype(used_item, /obj/item) && !istype(used_item, /obj/item/card)) + playsound(loc, 'sound/effects/Glasshit.ogg', 75, 1) visible_message("\The [src] was hit by \the [used_item].") if(used_item.atom_damage_type == BRUTE || used_item.atom_damage_type == BURN) take_damage(used_item.expend_attack_force(user)) return TRUE - src.add_fingerprint(user) - if (src.allowed(user)) - if (src.density) + add_fingerprint(user) + if (allowed(user)) + if (density) open() else close() return TRUE - else if (src.density) + else if (density) flick("[base_state]deny", src) return TRUE return FALSE @@ -184,14 +86,18 @@ /obj/structure/bed/holobed tool_interaction_flags = 0 holographic = TRUE + worthless = TRUE material = /decl/material/solid/metal/aluminium/holographic /obj/structure/chair/holochair tool_interaction_flags = 0 holographic = TRUE + worthless = TRUE material = /decl/material/solid/metal/aluminium/holographic /obj/item/holo + holographic = TRUE + worthless = TRUE atom_damage_type = PAIN no_attack_log = 1 max_health = ITEM_HEALTH_NO_DAMAGE @@ -258,6 +164,8 @@ anchored = TRUE density = TRUE throwpass = 1 + holographic = TRUE + worthless = TRUE /obj/structure/holohoop/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) if (istype(mover,/obj/item) && mover.throwing) @@ -284,6 +192,8 @@ layer = TABLE_LAYER throwpass = 1 dir = EAST + holographic = TRUE + worthless = TRUE /obj/structure/holonet/end icon_state = "volleynet_end" @@ -315,6 +225,8 @@ idle_power_usage = 2 active_power_usage = 6 power_channel = ENVIRON + holographic = TRUE + worthless = TRUE /obj/machinery/readybutton/attack_ai(mob/living/silicon/ai/user) to_chat(user, "The AI is not to interact with these devices!") @@ -364,51 +276,3 @@ for(var/mob/M in currentarea) to_chat(M, "FIGHT!") - -//Holocarp - -/mob/living/simple_animal/hostile/carp/holodeck - icon = 'icons/mob/simple_animal/holocarp.dmi' - alpha = 127 - butchery_data = null - -/mob/living/simple_animal/hostile/carp/holodeck/carp_randomify() - return - -/mob/living/simple_animal/hostile/carp/holodeck/on_update_icon() - SHOULD_CALL_PARENT(FALSE) - return - -/mob/living/simple_animal/hostile/carp/holodeck/Initialize() - . = ..() - set_light(2) //hologram lighting - -/mob/living/simple_animal/hostile/carp/holodeck/proc/set_safety(var/safe) - if (safe) - faction = MOB_FACTION_NEUTRAL - natural_weapon.set_base_attack_force(0) - environment_smash = 0 - ai?.try_destroy_surroundings = FALSE - else - faction = "carp" - natural_weapon.set_base_attack_force(natural_weapon.get_initial_base_attack_force()) - -/mob/living/simple_animal/hostile/carp/holodeck/gib(do_gibs = TRUE) - SHOULD_CALL_PARENT(FALSE) - if(stat != DEAD) - death(gibbed = TRUE) - if(stat == DEAD) - qdel(src) - return TRUE - return FALSE - -/mob/living/simple_animal/hostile/carp/holodeck/get_death_message(gibbed) - return "fades away..." - -/mob/living/simple_animal/hostile/carp/holodeck/get_self_death_message(gibbed) - return "You have been destroyed." - -/mob/living/simple_animal/hostile/carp/holodeck/death(gibbed) - . = ..() - if(. && !gibbed) - gib() diff --git a/mods/content/holodeck/holo_racks.dm b/mods/content/holodeck/holo_racks.dm new file mode 100644 index 00000000000..9745f215b88 --- /dev/null +++ b/mods/content/holodeck/holo_racks.dm @@ -0,0 +1,12 @@ +/obj/structure/rack/holorack + holographic = TRUE + worthless = TRUE + color = COLOR_OFF_WHITE + material = /decl/material/solid/metal/aluminium/holographic + reinf_material = /decl/material/solid/metal/aluminium/holographic + +/obj/structure/rack/holorack/dismantle_structure(mob/user) + material = null + reinf_material = null + parts_type = null + . = ..() \ No newline at end of file diff --git a/mods/content/holodeck/holo_tables.dm b/mods/content/holodeck/holo_tables.dm new file mode 100644 index 00000000000..fe1e52be43c --- /dev/null +++ b/mods/content/holodeck/holo_tables.dm @@ -0,0 +1,22 @@ +/obj/structure/table/holotable + icon_state = "holo_preview" + holographic = TRUE + worthless = TRUE + color = COLOR_OFF_WHITE + material = /decl/material/solid/metal/aluminium/holographic + reinf_material = /decl/material/solid/metal/aluminium/holographic + +/obj/structure/table/holo_plastictable + icon_state = "holo_preview" + holographic = TRUE + worthless = TRUE + color = COLOR_OFF_WHITE + material = /decl/material/solid/organic/plastic/holographic + reinf_material = /decl/material/solid/organic/plastic/holographic + +/obj/structure/table/holo_woodentable + holographic = TRUE + worthless = TRUE + icon_state = "holo_preview" + material = /decl/material/solid/organic/wood/holographic + reinf_material = /decl/material/solid/organic/wood/holographic \ No newline at end of file diff --git a/mods/content/holodeck/holo_turfs.dm b/mods/content/holodeck/holo_turfs.dm new file mode 100644 index 00000000000..44b28b5fc96 --- /dev/null +++ b/mods/content/holodeck/holo_turfs.dm @@ -0,0 +1,114 @@ +/turf/floor/holofloor + abstract_type = /turf/floor/holofloor + thermal_conductivity = 0 + +/turf/floor/holofloor/get_lumcount(var/minlum = 0, var/maxlum = 1) + return 0.8 + +/turf/floor/holofloor/attackby(obj/item/used_item, mob/user) + return TRUE + // HOLOFLOOR DOES NOT GIVE A FUCK + +/turf/floor/holofloor/carpet + name = "brown carpet" + icon = 'icons/turf/flooring/carpet.dmi' + icon_state = "brown" + _flooring = /decl/flooring/carpet + +/turf/floor/holofloor/concrete + name = "brown carpet" + icon = 'icons/turf/flooring/carpet.dmi' + icon_state = "brown" + _flooring = /decl/flooring/carpet + +/turf/floor/holofloor/concrete + name = "floor" + icon = 'icons/turf/flooring/misc.dmi' + icon_state = "concrete" + _flooring = null + +/turf/floor/holofloor/tiled + name = "floor" + icon = 'icons/turf/flooring/tiles.dmi' + icon_state = "steel" + _flooring = /decl/flooring/tiling + +/turf/floor/holofloor/tiled/dark + name = "dark floor" + icon_state = "dark" + _flooring = /decl/flooring/tiling/dark + +/turf/floor/holofloor/tiled/stone + name = "stone floor" + icon_state = "stone" + _flooring = /decl/flooring/tiling/stone + +/turf/floor/holofloor/lino + name = "lino" + icon = 'icons/turf/flooring/linoleum.dmi' + icon_state = "lino" + _flooring = /decl/flooring/linoleum + +/turf/floor/holofloor/wood + name = "wooden floor" + icon = 'icons/turf/flooring/wood.dmi' + icon_state = "wood0" + color = WOOD_COLOR_CHOCOLATE + _flooring = /decl/flooring/wood + +/turf/floor/holofloor/grass + name = "lush grass" + icon = 'icons/turf/flooring/fakegrass.dmi' + icon_state = "grass0" + _flooring = /decl/flooring/grass/fake + +/turf/floor/holofloor/snow + name = "snow" + icon = 'icons/turf/flooring/snow.dmi' + icon_state = "snow0" + _flooring = /decl/flooring/snow/fake + +/turf/floor/holofloor/space + name = "\proper space" + icon = 'icons/turf/flooring/fake_space.dmi' + icon_state = "space0" + _flooring = /decl/flooring/fake_space + +/turf/floor/holofloor/reinforced + name = "reinforced holofloor" + icon = 'icons/turf/flooring/tiles.dmi' + _flooring = /decl/flooring/reinforced + icon_state = "reinforced" + +/turf/floor/holofloor/beach + desc = "Uncomfortably gritty for a hologram." + icon = 'icons/misc/beach.dmi' + _flooring = /decl/flooring/sand/fake + abstract_type = /turf/floor/holofloor/beach + +/turf/floor/holofloor/beach/sand + name = "sand" + icon_state = "desert0" + +/turf/floor/holofloor/beach/coastline + name = "coastline" + icon = 'icons/misc/beach2.dmi' + icon_state = "sandwater" + _flooring = /decl/flooring/sand/fake + +/turf/floor/holofloor/beach/water + name = "water" + icon_state = "seashallow" + _flooring = /decl/flooring/fake_water + +/turf/floor/holofloor/desert + name = "desert sand" + desc = "Uncomfortably gritty for a hologram." + icon = 'icons/turf/flooring/barren.dmi' + icon_state = "barren" + _flooring = /decl/flooring/sand/fake + +/turf/floor/holofloor/desert/Initialize(var/ml) + . = ..() + if(prob(10)) + LAZYADD(decals, image('icons/turf/flooring/decals.dmi', "asteroid[rand(0,9)]")) \ No newline at end of file diff --git a/code/game/objects/items/circuitboards/computer/holodeckcontrol.dm b/mods/content/holodeck/holodeck_control_circuit.dm similarity index 100% rename from code/game/objects/items/circuitboards/computer/holodeckcontrol.dm rename to mods/content/holodeck/holodeck_control_circuit.dm diff --git a/code/modules/holodeck/HolodeckControl.dm b/mods/content/holodeck/holodeck_control_console.dm similarity index 99% rename from code/modules/holodeck/HolodeckControl.dm rename to mods/content/holodeck/holodeck_control_console.dm index f5db252b01d..80b2597f9c0 100644 --- a/code/modules/holodeck/HolodeckControl.dm +++ b/mods/content/holodeck/holodeck_control_console.dm @@ -276,6 +276,7 @@ for(var/obj/holo_obj in holographic_objs) holo_obj.alpha *= 0.8 //give holodeck objs a slight transparency holo_obj.holographic = TRUE + holo_obj.worthless = TRUE if(HP.ambience) linkedholodeck.forced_ambience = HP.ambience diff --git a/mods/content/holodeck/holodeck_designs.dm b/mods/content/holodeck/holodeck_designs.dm new file mode 100644 index 00000000000..2160366208d --- /dev/null +++ b/mods/content/holodeck/holodeck_designs.dm @@ -0,0 +1,2 @@ +/datum/fabricator_recipe/imprinter/circuit/holo + path = /obj/item/stock_parts/circuitboard/holodeck_control \ No newline at end of file diff --git a/code/modules/holodeck/HolodeckPrograms.dm b/mods/content/holodeck/holodeck_programs.dm similarity index 100% rename from code/modules/holodeck/HolodeckPrograms.dm rename to mods/content/holodeck/holodeck_programs.dm diff --git a/mods/content/holodeck/maps_holodeck.dm b/mods/content/holodeck/maps_holodeck.dm new file mode 100644 index 00000000000..427fd2af9ba --- /dev/null +++ b/mods/content/holodeck/maps_holodeck.dm @@ -0,0 +1,9 @@ +/datum/map + var/list/holodeck_programs = list() // map of string ids to /datum/holodeck_program instances + var/list/holodeck_supported_programs = list() // map of maps - first level maps from list-of-programs string id (e.g. "BarPrograms") to another map + // this is in order to support multiple holodeck program listings for different holodecks + // second level maps from program friendly display names ("Picnic Area") to program string ids ("picnicarea") + // as defined in holodeck_programs + var/list/holodeck_restricted_programs = list() // as above... but EVIL! + var/list/holodeck_default_program = list() // map of program list string ids to default program string id + var/list/holodeck_off_program = list() // as above... but for being off i guess \ No newline at end of file diff --git a/mods/content/holodeck/trader_overrides.dm b/mods/content/holodeck/trader_overrides.dm new file mode 100644 index 00000000000..f3f957c0819 --- /dev/null +++ b/mods/content/holodeck/trader_overrides.dm @@ -0,0 +1,3 @@ +/datum/trader/ship/prank_shop/New() + LAZYSET(possible_trading_items, /obj/item/grenade/spawnergrenade/fake_carp, TRADER_THIS_TYPE) + ..() \ No newline at end of file diff --git a/mods/content/xenobiology/slime/items.dm b/mods/content/xenobiology/slime/items.dm index 26304f3e971..28c96213663 100644 --- a/mods/content/xenobiology/slime/items.dm +++ b/mods/content/xenobiology/slime/items.dm @@ -15,7 +15,7 @@ var/Uses = 1 // uses before it goes inert var/enhanced = 0 //has it been enhanced before? -/obj/item/slime_extract/get_base_value() +/obj/item/slime_extract/get_value_multiplier() . = ..() * Uses /obj/item/slime_extract/attackby(obj/item/used_item, mob/user) diff --git a/nebula.dme b/nebula.dme index 68161da9a70..e5d014314fb 100644 --- a/nebula.dme +++ b/nebula.dme @@ -1160,7 +1160,6 @@ #include "code\game\objects\items\circuitboards\wall.dm" #include "code\game\objects\items\circuitboards\computer\air_management.dm" #include "code\game\objects\items\circuitboards\computer\computer.dm" -#include "code\game\objects\items\circuitboards\computer\holodeckcontrol.dm" #include "code\game\objects\items\circuitboards\computer\modular.dm" #include "code\game\objects\items\circuitboards\computer\shuttle.dm" #include "code\game\objects\items\circuitboards\computer\station_alert.dm" @@ -2597,9 +2596,6 @@ #include "code\modules\holidays\holiday_hook.dm" #include "code\modules\holidays\holiday_name.dm" #include "code\modules\holidays\holiday_special.dm" -#include "code\modules\holodeck\HolodeckControl.dm" -#include "code\modules\holodeck\HolodeckObjects.dm" -#include "code\modules\holodeck\HolodeckPrograms.dm" #include "code\modules\holomap\holomap.dm" #include "code\modules\hotloading\_admin.dm" #include "code\modules\hotloading\note.dm" From a9baa0deb5ad67f9100a98eae8568e5d959daaa7 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Thu, 9 Jul 2026 12:49:08 -0400 Subject: [PATCH 55/79] Remove inappropriate holofloors from maps --- maps/away/derelict/derelict-station.dmm | 2 +- maps/away/mining/mining-signal.dmm | 181 +++++++++--------------- maps/ministation/ministation-2.dmm | 2 +- 3 files changed, 68 insertions(+), 117 deletions(-) diff --git a/maps/away/derelict/derelict-station.dmm b/maps/away/derelict/derelict-station.dmm index 46a25943bbf..94cb83ccc6a 100644 --- a/maps/away/derelict/derelict-station.dmm +++ b/maps/away/derelict/derelict-station.dmm @@ -998,7 +998,7 @@ /turf/floor/plating/airless, /area/constructionsite/hallway/fore) "dw" = ( -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/constructionsite/hallway/fore) "dx" = ( /obj/random/junk, diff --git a/maps/away/mining/mining-signal.dmm b/maps/away/mining/mining-signal.dmm index 7badf05b323..95d27ad6286 100644 --- a/maps/away/mining/mining-signal.dmm +++ b/maps/away/mining/mining-signal.dmm @@ -12,28 +12,28 @@ /obj/structure/rack, /obj/random/tech_supply, /obj/random/bomb_supply, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "ah" = ( /obj/structure/rack, /obj/random/tech_supply, /obj/random/loot, /obj/random/loot, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "ai" = ( /obj/structure/table/steel_reinforced, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "aj" = ( /obj/machinery/porta_turret/stationary, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "ak" = ( /obj/structure/rack, /obj/item/cell/hyper, /obj/item/cell/hyper, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "am" = ( /obj/structure/rack, @@ -45,7 +45,7 @@ dir = 4 }, /obj/machinery/door/window/brigdoor/southleft, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "an" = ( /obj/machinery/mech_recharger, @@ -53,10 +53,7 @@ /obj/structure/window/reinforced{ dir = 8 }, -/turf/floor/holofloor/tiled/dark, -/area/outpost/abandoned) -"ao" = ( -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "ap" = ( /obj/structure/table, @@ -125,23 +122,23 @@ dir = 8; icon_state = "bulb1" }, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "aE" = ( /obj/random/trash, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "aF" = ( /obj/random/technology_scanner, /obj/machinery/porta_turret/stationary, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "aG" = ( /obj/machinery/light/small/emergency{ dir = 4; icon_state = "bulb1" }, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "aH" = ( /turf/floor/plating, @@ -257,7 +254,7 @@ /obj/machinery/light/small{ dir = 1 }, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/outpost/abandoned) "be" = ( /obj/effect/decal/cleanable/blood, @@ -345,13 +342,6 @@ }, /turf/floor/tiled/white, /area/outpost/abandoned) -"bu" = ( -/obj/effect/decal/cleanable/dirt/visible, -/obj/effect/floor_decal/corner/purple{ - dir = 5 - }, -/turf/floor/tiled/white, -/area/outpost/abandoned) "bv" = ( /obj/effect/decal/cleanable/dirt/visible, /obj/machinery/light/small{ @@ -429,13 +419,6 @@ /obj/effect/decal/cleanable/dirt/visible, /turf/floor, /area/outpost/abandoned) -"bE" = ( -/obj/effect/floor_decal/corner/purple{ - dir = 5 - }, -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/white, -/area/outpost/abandoned) "bF" = ( /obj/machinery/light/small{ dir = 1 @@ -469,10 +452,6 @@ /obj/effect/gibspawner/human, /turf/floor/tiled/white, /area/outpost/abandoned) -"bL" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/white, -/area/outpost/abandoned) "bM" = ( /obj/effect/decal/cleanable/blood, /turf/floor/tiled/white, @@ -626,10 +605,6 @@ }, /turf/floor/tiled/white, /area/outpost/abandoned) -"ck" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor, -/area/outpost/abandoned) "cl" = ( /obj/structure/hygiene/shower{ dir = 8 @@ -799,10 +774,6 @@ /obj/effect/decal/cleanable/dirt/visible, /turf/floor/tiled/airless, /area/outpost/abandoned) -"cW" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/airless, -/area/outpost/abandoned) "cX" = ( /obj/effect/decal/cleanable/blood, /obj/effect/floor_decal/corner/paleblue{ @@ -900,10 +871,6 @@ /obj/effect/decal/cleanable/dirt/visible, /turf/floor/carpet, /area/outpost/abandoned) -"dk" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/carpet/broken, -/area/outpost/abandoned) "dl" = ( /obj/effect/floor_decal/spline/fancy/wood{ dir = 6 @@ -1231,10 +1198,6 @@ /obj/effect/decal/cleanable/dirt/visible, /turf/floor/tiled/white/airless, /area/outpost/abandoned) -"es" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/white/airless, -/area/outpost/abandoned) "et" = ( /obj/abstract/landmark/mapped_fluid/fuel, /obj/structure/table, @@ -1477,10 +1440,6 @@ /obj/effect/floor_decal/industrial/warning/cee, /turf/floor/plating, /area/outpost/abandoned) -"fh" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/dark, -/area/outpost/abandoned) "fi" = ( /obj/item/pen, /obj/effect/decal/cleanable/dirt/visible, @@ -1860,10 +1819,6 @@ /obj/effect/decal/cleanable/dirt/visible, /turf/floor/barren, /area/mine/explored) -"gt" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/barren, -/area/mine/explored) "gu" = ( /obj/effect/decal/cleanable/dirt/visible, /turf/floor/plating, @@ -2420,10 +2375,6 @@ }, /turf/floor/tiled/white, /area/outpost/abandoned) -"HD" = ( -/obj/effect/decal/cleanable/dirt/visible, -/turf/floor/tiled/white/airless, -/area/outpost/abandoned) "IX" = ( /obj/machinery/door/firedoor, /obj/machinery/door/blast/regular/open, @@ -11822,7 +11773,7 @@ cu cU af dE -cW +cw dD dW fm @@ -12025,9 +11976,9 @@ cV cG dF dV -es -HD -HD +dW +dW +dW fF cG aa @@ -12223,7 +12174,7 @@ aa af cc cw -cW +cw ds dD dW @@ -13845,7 +13796,7 @@ dI ea dH eV -cW +cw fK af aa @@ -14044,10 +13995,10 @@ aa aa af dJ -cW +cw ez eW -cW +cw fL af aa @@ -14246,10 +14197,10 @@ cy af af dK -cW +cw dH eX -cW +cw fM af af @@ -14449,7 +14400,7 @@ cZ cG dL eb -cW +cw eY ft dH @@ -14852,7 +14803,7 @@ sb db af dN -cW +cw cw eZ fv @@ -15054,10 +15005,10 @@ tb bZ cy dH -cW +cw cw fa -cW +cw fO fY gf @@ -15247,7 +15198,7 @@ az aX az bm -bu +bt bK bQ bX @@ -15453,15 +15404,15 @@ bv bJ bQ bX -ck +by vb dd dt dP dH -cW +cw eZ -cW +cw dH af af @@ -15667,7 +15618,7 @@ cw fQ dH dH -cW +cw gy cw gK @@ -15867,8 +15818,8 @@ eC fc fx cw -cW -cW +cw +cw gm dH gE @@ -16068,7 +16019,7 @@ eg eD fd eD -cW +cw ga ga gn @@ -16076,8 +16027,8 @@ gn gF dH gP -cW -cW +cw +cw gL hl ht @@ -16279,7 +16230,7 @@ af af cy gS -cW +cw gL dH hu @@ -16453,9 +16404,9 @@ aa aa af ag -ao +dT aD -ao +dT ba bh af @@ -16655,9 +16606,9 @@ aa aa af ah -ao -ao -ao +dT +dT +dT bb bi af @@ -16857,13 +16808,13 @@ aa aa af ai -ao +dT aj af af af af -bu +bt bJ bQ af @@ -17059,11 +17010,11 @@ aa aa af aj -ao -ao +dT +dT af -ao -ao +dT +dT af bB bJ @@ -17261,11 +17212,11 @@ aa aa af ak -ao +dT aE aN -ao -ao +dT +dT bn bx bH @@ -17463,14 +17414,14 @@ aa aa af aj -ao -ao +dT +dT af bc -ao +dT af bC -bL +bJ bT bZ co @@ -17485,7 +17436,7 @@ ab ab gb ej -gt +ej fe ab gM @@ -17665,7 +17616,7 @@ aa aa af ai -ao +dT aF af af @@ -17867,8 +17818,8 @@ aa aa af am -ao -ao +dT +dT aO pb bj @@ -18069,7 +18020,7 @@ aa aa af an -ao +dT aG aP be @@ -18081,7 +18032,7 @@ bP bZ cr cL -dk +di dA dm ab @@ -19904,7 +19855,7 @@ cQ dS em eK -fh +eL dT fS af @@ -20106,8 +20057,8 @@ cQ cQ af eL -fh -fh +eL +eL eL af aa @@ -20703,7 +20654,7 @@ aH af bx bN -bL +bJ bU af af @@ -20903,7 +20854,7 @@ aH aH aI bq -bE +bC bJ bJ bP diff --git a/maps/ministation/ministation-2.dmm b/maps/ministation/ministation-2.dmm index 36d8182f59f..2d036207cdf 100644 --- a/maps/ministation/ministation-2.dmm +++ b/maps/ministation/ministation-2.dmm @@ -3147,7 +3147,7 @@ /obj/machinery/door/firedoor{ dir = 8 }, -/turf/floor/holofloor/lino, +/turf/floor/lino, /area/ministation/telecomms) "op" = ( /obj/machinery/light/small{ From d0cf608efaa049533d41ad5ca54ee7b71db87819 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Sat, 11 Jul 2026 09:03:19 +1000 Subject: [PATCH 56/79] Automatic changelog generation for PR #5395 [ci skip] --- html/changelogs/AutoChangeLog-pr-5395.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-5395.yml diff --git a/html/changelogs/AutoChangeLog-pr-5395.yml b/html/changelogs/AutoChangeLog-pr-5395.yml new file mode 100644 index 00000000000..9a098d54e4a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-5395.yml @@ -0,0 +1,4 @@ +author: 'tetra zeta ' +changes: + - {imageadd: sand resprited and converted to greyscale} +delete-after: true From 9850d4cedf20d44c057518ab59ce9dbc16f9e47f Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Sat, 11 Jul 2026 01:48:39 +0000 Subject: [PATCH 57/79] Automatic changelog generation [ci skip] --- html/changelog.html | 6 ++++++ html/changelogs/.all_changelog.yml | 3 +++ html/changelogs/AutoChangeLog-pr-5395.yml | 4 ---- 3 files changed, 9 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-5395.yml diff --git a/html/changelog.html b/html/changelog.html index 185b3d9def1..e54c76e6c58 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -52,6 +52,12 @@ -->
    +

    11 July 2026

    +

    tetra zeta updated:

    +
      +
    • sand resprited and converted to greyscale
    • +
    +

    30 May 2026

    Penelope Haze updated:

      diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml index 2fb6a977c18..4d94674e39a 100644 --- a/html/changelogs/.all_changelog.yml +++ b/html/changelogs/.all_changelog.yml @@ -15079,3 +15079,6 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py. - tweak: Deactivating message passing on the message server now prevents requests console messages from being received. They will still be logged for admins and on the in-game message server console. +2026-07-11: + 'tetra zeta ': + - imageadd: sand resprited and converted to greyscale diff --git a/html/changelogs/AutoChangeLog-pr-5395.yml b/html/changelogs/AutoChangeLog-pr-5395.yml deleted file mode 100644 index 9a098d54e4a..00000000000 --- a/html/changelogs/AutoChangeLog-pr-5395.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: 'tetra zeta ' -changes: - - {imageadd: sand resprited and converted to greyscale} -delete-after: true From 0d162df64b3487e288dd0c607406fb01a60b8292 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Tue, 14 Jul 2026 18:44:36 -0400 Subject: [PATCH 58/79] Fix incorrect holostool icon --- mods/content/holodeck/holo_objects.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/content/holodeck/holo_objects.dm b/mods/content/holodeck/holo_objects.dm index d1125a62467..08f617da4fb 100644 --- a/mods/content/holodeck/holo_objects.dm +++ b/mods/content/holodeck/holo_objects.dm @@ -10,7 +10,7 @@ /obj/structure/holostool name = "stool" desc = "Apply butt." - icon = 'icons/obj/furniture.dmi' + icon = 'icons/obj/stool.dmi' icon_state = "stool_padded_preview" anchored = TRUE worthless = TRUE From 295db9d592e6e2912c0a4c60edf90e8e074efdff Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Tue, 14 Jul 2026 18:44:58 -0400 Subject: [PATCH 59/79] Fix holostools being used outside the holodeck --- maps/away/bearcat/bearcat-1.dmm | 18 +++++++++--------- maps/tradeship/tradeship-1.dmm | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/maps/away/bearcat/bearcat-1.dmm b/maps/away/bearcat/bearcat-1.dmm index 534accc465e..312a07c8266 100644 --- a/maps/away/bearcat/bearcat-1.dmm +++ b/maps/away/bearcat/bearcat-1.dmm @@ -378,7 +378,7 @@ /area/ship/scrap/gambling) "aW" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light/small{ dir = 1; icon_state = "bulb1" @@ -497,7 +497,7 @@ /obj/effect/floor_decal/corner/beige{ dir = 5 }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light/small{ dir = 1; icon_state = "bulb1" @@ -527,7 +527,7 @@ /turf/floor/usedup, /area/ship/scrap/gambling) "bj" = ( -/obj/structure/holostool, +/obj/item/stool/padded, /obj/item/hand/missing_card, /turf/floor/usedup, /area/ship/scrap/gambling) @@ -553,7 +553,7 @@ /obj/structure/cable{ icon_state = "1-4" }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/item/hand/missing_card, /turf/floor/usedup, /area/ship/scrap/gambling) @@ -701,7 +701,7 @@ dir = 1; level = 2 }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/item/hand/missing_card, /turf/floor/usedup, /area/ship/scrap/gambling) @@ -801,7 +801,7 @@ dir = 1; level = 2 }, -/obj/structure/holostool, +/obj/item/stool/padded, /turf/floor/tiled/dark/airless, /area/ship/scrap/crew/dorms1) "bO" = ( @@ -1019,7 +1019,7 @@ /area/ship/scrap/crew/dorms2) "cj" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light/small{ dir = 1; icon_state = "bulb1" @@ -1473,7 +1473,7 @@ /obj/effect/floor_decal/corner/beige{ dir = 5 }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light/small{ dir = 1; icon_state = "bulb1" @@ -1687,7 +1687,7 @@ dir = 1; level = 2 }, -/obj/structure/holostool, +/obj/item/stool/padded, /turf/floor/tiled/dark/airless, /area/ship/scrap/crew/dorms3) "dG" = ( diff --git a/maps/tradeship/tradeship-1.dmm b/maps/tradeship/tradeship-1.dmm index 34a51a33bfa..73454f89be1 100644 --- a/maps/tradeship/tradeship-1.dmm +++ b/maps/tradeship/tradeship-1.dmm @@ -2580,7 +2580,7 @@ /obj/effect/floor_decal/corner/beige{ dir = 5 }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light{ dir = 1; icon_state = "bulb1" @@ -2720,7 +2720,7 @@ dir = 1; level = 2 }, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/firealarm{ dir = 1; pixel_y = -21 @@ -2847,7 +2847,7 @@ /area/ship/trade/science/fabricaton) "Ib" = ( /obj/machinery/atmospherics/unary/vent_scrubber/on, -/obj/structure/holostool, +/obj/item/stool/padded, /obj/machinery/light{ dir = 1; icon_state = "bulb1" From dc0da5c7583da724e5409612ed923e8d581f6847 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Tue, 14 Jul 2026 18:45:41 -0400 Subject: [PATCH 60/79] Fix holofloor in derelict station map --- maps/away/derelict/derelict-station.dmm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps/away/derelict/derelict-station.dmm b/maps/away/derelict/derelict-station.dmm index 94cb83ccc6a..7b09f5b340c 100644 --- a/maps/away/derelict/derelict-station.dmm +++ b/maps/away/derelict/derelict-station.dmm @@ -1048,7 +1048,7 @@ /obj/machinery/door/airlock/glass/command{ name = "Bridge" }, -/turf/floor/holofloor/tiled/dark, +/turf/floor/tiled/dark, /area/constructionsite/hallway/fore) "dI" = ( /obj/machinery/door/airlock/glass{ From 96f3b825910872e451c6b7849edd4aa8ca2fa24c Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Wed, 15 Jul 2026 11:01:39 -0400 Subject: [PATCH 61/79] Fix gitignore not specifying repo root --- .gitignore | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 185303ed487..83d58a21245 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,10 @@ *.lk *.backup *.before -codex/ -data/ -dmdoc/ -cfg/ +/codex/ +/data/ +/dmdoc/ +/cfg/ build_log.txt use_map stopserver @@ -17,9 +17,9 @@ reboot_called atupdate # ignore config, but not subdirs -!config/*/ -config/* -sql/test_db +/!config/*/ +/config/* +/sql/test_db # misc OS garbage Thumbs.db @@ -27,7 +27,7 @@ Thumbs.db:encryptable .DS_Store # vscode -.vscode/* +/.vscode/* *.code-workspace .history From c0fcf10a6c597164b8f0150ce543f12688c23aa7 Mon Sep 17 00:00:00 2001 From: Lohikar Date: Sat, 18 Jul 2026 16:27:11 -0500 Subject: [PATCH 62/79] lighting: Ignore zlev transition boundaries for z-stack construction --- code/modules/lighting/lighting_corner.dm | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/code/modules/lighting/lighting_corner.dm b/code/modules/lighting/lighting_corner.dm index 36926e35da4..474b5acd641 100644 --- a/code/modules/lighting/lighting_corner.dm +++ b/code/modules/lighting/lighting_corner.dm @@ -191,6 +191,7 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, SSlighting.corner_queue += src /datum/lighting_corner/proc/generate_z_connections(direction = LIGHTING_CORNER_GENERATE_BOTH) + ASSERT(z != null) /* ZM_ALLOW_LIGHTING means that a z-turf is lighting-connected to the turf below it. So: @@ -217,26 +218,30 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, This brick is responsible for finding the corner that's directly above us, and forcibly generating the corner if it doesn't exist yet. It's just the same block of code repeated four times (for each master), plus the case of there now being no above corner, but previously having had one. We also only initialize the one corner we need rather than all four since there's no benefit to initializing them all -- if a true light needs them, it'll make them itself. + + Nebula specific: due to lighting on edges of z-levels wrapping around, this logic needs to exclude masters that are on a different Z-level. + This logic assumes that all (up to) four masters of this corner are equivalent, but this is not true of corners found via z-level transition boundaries. + Turfs with transition corners should have at least one non-transition corner, so we just ignore them. */ - if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING)) + if (t1?.z == z && (T = t1.above || GET_ABOVE(t1)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t1i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t1i] - else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING)) + else if (t2?.z == z && (T = t2.above || GET_ABOVE(t2)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t2i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t2i] - else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING)) + else if (t3?.z == z && (T = t3.above || GET_ABOVE(t3)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t3i]) && GOING_UP) if (!T.corners) T.corners = new(4) T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_UP) above_corner = T.corners[t3i] - else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING)) + else if (t4?.z == z && (T = t4.above || GET_ABOVE(t4)) && (T.z_flags & ZM_ALLOW_LIGHTING)) if (!(above_corner = T.corners?[t4i]) && GOING_UP) if (!T.corners) T.corners = new(4) @@ -280,25 +285,25 @@ var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, SSlighting.corner_queue += corn // As above, so below. The ordering here is a bit different from the above block, check the comment at the top of this proc. - if ((t1?.z_flags & ZM_ALLOW_LIGHTING) && (T = t1.below || GET_BELOW(t1))) + if (t1?.z == z && (t1.z_flags & ZM_ALLOW_LIGHTING) && (T = t1.below || GET_BELOW(t1))) if (!(below_corner = T.corners?[t1i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t1i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t1i], t1i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t1i] - else if ((t2?.z_flags & ZM_ALLOW_LIGHTING) && (T = t2.below || GET_BELOW(t2))) + else if (t2?.z == z && (t2.z_flags & ZM_ALLOW_LIGHTING) && (T = t2.below || GET_BELOW(t2))) if (!(below_corner = T.corners?[t2i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t2i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t2i], t2i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t2i] - else if ((t3?.z_flags & ZM_ALLOW_LIGHTING) && (T = t3.below || GET_BELOW(t3))) + else if (t3?.z == z && (t3.z_flags & ZM_ALLOW_LIGHTING) && (T = t3.below || GET_BELOW(t3))) if (!(below_corner = T.corners?[t3i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) T.corners[t3i] = new/datum/lighting_corner(T, LIGHTING_CORNER_DIAGONAL[t3i], t3i, LIGHTING_CORNER_GENERATE_DOWN) below_corner = T.corners[t3i] - else if ((t4?.z_flags & ZM_ALLOW_LIGHTING) && (T = t4.below || GET_BELOW(t4))) + else if (t4?.z == z && (t4.z_flags & ZM_ALLOW_LIGHTING) && (T = t4.below || GET_BELOW(t4))) if (!(below_corner = T.corners?[t4i]) && GOING_DOWN) if (!T.corners) T.corners = new(4) From af6caecd33e4b5c641eedce90f76999bd2b0de8f Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sat, 18 Jul 2026 18:15:19 -0400 Subject: [PATCH 63/79] Update CI to 516.1685 --- .github/workflows/test.yml | 2 +- install-byond.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f402ce4ca9..be6edf6ec39 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ on: env: BYOND_MAJOR: "516" - BYOND_MINOR: "1663" + BYOND_MINOR: "1685" SPACEMAN_DMM_VERSION: suite-1.11 jobs: diff --git a/install-byond.sh b/install-byond.sh index 5d60db27712..ae7d1be6973 100755 --- a/install-byond.sh +++ b/install-byond.sh @@ -9,7 +9,7 @@ else cd "$HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR}" echo "Installing DreamMaker to $PWD" #curl "http://www.byond.com/download/build/${BYOND_MAJOR}/${BYOND_MAJOR}.${BYOND_MINOR}_byond_linux.zip" -H "User-Agent: NebulaSS13/1.0 Continuous Integration" -o byond.zip - curl "https://spacestation13.github.io/byond-builds/${BYOND_MAJOR}/${BYOND_MAJOR}.${BYOND_MINOR}_byond_linux.zip" -H "User-Agent: NebulaSS13/1.0 Continuous Integration" -o byond.zip + curl "https://byond-builds.dm-lang.org/${BYOND_MAJOR}/${BYOND_MAJOR}.${BYOND_MINOR}_byond_linux.zip" -H "User-Agent: NebulaSS13/1.0 Continuous Integration" -o byond.zip unzip -o byond.zip cd byond make here From cddcf8f88ae55cf632887619384d0d907877b457 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sat, 18 Jul 2026 22:38:56 -0400 Subject: [PATCH 64/79] Add missing libcurl dependency --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be6edf6ec39..dd359273ecc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -106,6 +106,11 @@ jobs: with: path: ~/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }} key: ${{ runner.os }}-byond-${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }} + - name: Install Dependencies + run: | + sudo dpkg --add-architecture i386 + sudo apt update || true + sudo apt install -o APT::Immediate-Configure=false curl:i386 - name: Run Tests env: TEST: MAP From 064044947c69a7231c881fb7566adfd17125e100 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Fri, 24 Jul 2026 01:50:56 +0000 Subject: [PATCH 65/79] Automatic changelog generation [ci skip] --- html/changelog.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index e54c76e6c58..197d8b4966e 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -63,13 +63,6 @@

      Penelope Haze updated:

      • Deactivating message passing on the message server now prevents requests console messages from being received. They will still be logged for admins and on the in-game message server console.
      - -

      22 May 2026

      -

      MistakeNot4892 updated:

      -
        -
      • Mech plasmacutter is full auto and mech AR full auto should now work.
      • -
      • Autofire should be more responsive in general.
      • -
    From d736dff04680b601e11a17dd08d7e4776c2dd1d1 Mon Sep 17 00:00:00 2001 From: NebulaSS13Bot Date: Sat, 1 Aug 2026 01:57:02 +0000 Subject: [PATCH 66/79] Automatic changelog generation [ci skip] --- html/changelog.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index 197d8b4966e..2a482a38ae9 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -57,12 +57,6 @@

    tetra zeta updated:

    • sand resprited and converted to greyscale
    - -

    30 May 2026

    -

    Penelope Haze updated:

    -
      -
    • Deactivating message passing on the message server now prevents requests console messages from being received. They will still be logged for admins and on the in-game message server console.
    • -
From 5ecc068a249fb7b2a17a06c27c13401ce6371204 Mon Sep 17 00:00:00 2001 From: Zandario Date: Fri, 31 Jul 2026 23:39:53 -0400 Subject: [PATCH 67/79] Fix typo in ore image rotation matrix --- code/modules/materials/stack_types/material_stack_ore.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/materials/stack_types/material_stack_ore.dm b/code/modules/materials/stack_types/material_stack_ore.dm index 391166a8bb1..3bff7ccab9f 100644 --- a/code/modules/materials/stack_types/material_stack_ore.dm +++ b/code/modules/materials/stack_types/material_stack_ore.dm @@ -56,7 +56,7 @@ //Randomize the orientation and position of each ores in the image var/matrix/M = matrix() M.Translate(rand(-6, 6), rand(-6, 6)) - M.Turn(pick(-72, -58, -45, -27.-5, 0, 0, 0, 0, 0, 27.5, 45, 58, 72)) + M.Turn(pick(-72, -58, -45, -27.5, 0, 0, 0, 0, 0, 27.5, 45, 58, 72)) var/image/oreoverlay = image('icons/obj/materials/ore.dmi', IS) oreoverlay.transform = M scrapboard.overlays += oreoverlay From ea54528cef8df9ca096ceb16be0a61e86292ccf8 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sun, 5 Jul 2026 19:24:44 -0400 Subject: [PATCH 68/79] Move turbolifts into a modpack --- code/controllers/subsystems/mapping.dm | 8 +++--- code/controllers/subsystems/misc_late.dm | 9 +++---- code/modules/turbolift/turbolift_turfs.dm | 2 -- maps/example/example_shuttles.dm | 2 +- maps/exodus/exodus_elevator.dm | 2 +- maps/ministation/ministation_shuttles.dm | 2 +- maps/tradeship/tradeship.dm | 2 ++ maps/tradeship/tradeship_shuttles.dm | 23 ------------------ maps/tradeship/tradeship_turbolift.dm | 22 +++++++++++++++++ mods/_modpack.dm | 8 ++++++ mods/content/turbolift/_turbolift.dm | 3 +++ mods/content/turbolift/_turbolift.dme | 14 +++++++++++ .../content/turbolift/icons/door}/door.dmi | Bin .../turbolift/icons/door}/fill_glass.dmi | Bin .../turbolift/icons/door}/fill_steel.dmi | Bin .../turbolift/icons/door}/lights_bolts.dmi | Bin .../turbolift/icons/door}/lights_deny.dmi | Bin .../turbolift/icons/door}/lights_green.dmi | Bin .../content/turbolift/icons}/turbolift.dmi | Bin .../icons}/turbolift_preview_3x3.dmi | Bin .../icons}/turbolift_preview_5x5.dmi | Bin .../icons}/turbolift_preview_nowalls_3x3.dmi | Bin .../icons}/turbolift_preview_nowalls_4x4.dmi | Bin .../content}/turbolift/turbolift.dm | 0 .../content}/turbolift/turbolift_areas.dm | 0 .../content}/turbolift/turbolift_console.dm | 2 +- .../content}/turbolift/turbolift_door.dm | 12 ++++----- .../content}/turbolift/turbolift_floor.dm | 0 mods/content/turbolift/turbolift_init.dm | 16 ++++++++++++ .../content}/turbolift/turbolift_map.dm | 11 ++++++--- mods/content/turbolift/turbolift_turfs.dm | 2 ++ nebula.dme | 7 ------ 32 files changed, 90 insertions(+), 57 deletions(-) delete mode 100644 code/modules/turbolift/turbolift_turfs.dm create mode 100644 maps/tradeship/tradeship_turbolift.dm create mode 100644 mods/content/turbolift/_turbolift.dm create mode 100644 mods/content/turbolift/_turbolift.dme rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/door.dmi (100%) rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/fill_glass.dmi (100%) rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/fill_steel.dmi (100%) rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/lights_bolts.dmi (100%) rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/lights_deny.dmi (100%) rename {icons/obj/doors/elevator => mods/content/turbolift/icons/door}/lights_green.dmi (100%) rename {icons/obj => mods/content/turbolift/icons}/turbolift.dmi (100%) rename {icons/obj => mods/content/turbolift/icons}/turbolift_preview_3x3.dmi (100%) rename {icons/obj => mods/content/turbolift/icons}/turbolift_preview_5x5.dmi (100%) rename {icons/obj => mods/content/turbolift/icons}/turbolift_preview_nowalls_3x3.dmi (100%) rename {icons/obj => mods/content/turbolift/icons}/turbolift_preview_nowalls_4x4.dmi (100%) rename {code/modules => mods/content}/turbolift/turbolift.dm (100%) rename {code/modules => mods/content}/turbolift/turbolift_areas.dm (100%) rename {code/modules => mods/content}/turbolift/turbolift_console.dm (98%) rename {code/modules => mods/content}/turbolift/turbolift_door.dm (77%) rename {code/modules => mods/content}/turbolift/turbolift_floor.dm (100%) create mode 100644 mods/content/turbolift/turbolift_init.dm rename {code/modules => mods/content}/turbolift/turbolift_map.dm (94%) create mode 100644 mods/content/turbolift/turbolift_turfs.dm diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm index 78253db4d1d..90cdbebd8c4 100644 --- a/code/controllers/subsystems/mapping.dm +++ b/code/controllers/subsystems/mapping.dm @@ -48,8 +48,6 @@ SUBSYSTEM_DEF(mapping) var/base_floor_area /// A list of connected z-levels to avoid repeatedly rebuilding connections var/list/connected_z_cache = list() - /// A list of turbolift holders to initialize. - var/list/turbolifts_to_initialize = list() ///Associative list of planetoid/exoplanet data currently registered. The key is the planetoid id, the value is the planetoid_data datum. var/list/planetoid_data_by_id ///List of all z-levels in the world where the index corresponds to a z-level, and the key at that index is the planetoid_data datum for the associated planet @@ -169,9 +167,9 @@ SUBSYSTEM_DEF(mapping) global.level_persistence_ref_map.Cut() - // Generate turbolifts last, since away sites may have elevators to generate too. - for(var/obj/abstract/turbolift_spawner/turbolift as anything in turbolifts_to_initialize) - turbolift.build_turbolift() + for(var/modpack_name in SSmodpacks.loaded_modpacks) + var/decl/modpack/loaded_modpack = SSmodpacks.loaded_modpacks[modpack_name] + loaded_modpack.on_mapping_pre_finalize() // With levels set up and serde complete (and levels flagged) we can do any remaining level generation. global.using_map.finalize_map_generation() diff --git a/code/controllers/subsystems/misc_late.dm b/code/controllers/subsystems/misc_late.dm index 7f9ceb8ce6e..45f0efac238 100644 --- a/code/controllers/subsystems/misc_late.dm +++ b/code/controllers/subsystems/misc_late.dm @@ -3,18 +3,15 @@ SUBSYSTEM_DEF(misc_late) name = "Late Initialization" init_order = SS_INIT_MISC_LATE flags = SS_NO_FIRE - var/list/turbolifts_to_open = list() /datum/controller/subsystem/misc_late/Initialize() var/decl/asset_cache/asset_cache = GET_DECL(/decl/asset_cache) asset_cache.load() - // This is gross but I'm not sure where else to handle it. Sorry. - for(var/datum/turbolift/lift in turbolifts_to_open) - if(!QDELETED(lift)) - lift.open_doors() - turbolifts_to_open.Cut() + for(var/modpack_name in SSmodpacks.loaded_modpacks) + var/decl/modpack/loaded_modpack = SSmodpacks.loaded_modpacks[modpack_name] + loaded_modpack.on_misc_late_init() // Pre-populate the emote list. decls_repository.get_decls_of_type(/decl/emote) diff --git a/code/modules/turbolift/turbolift_turfs.dm b/code/modules/turbolift/turbolift_turfs.dm deleted file mode 100644 index 045790c2529..00000000000 --- a/code/modules/turbolift/turbolift_turfs.dm +++ /dev/null @@ -1,2 +0,0 @@ -/turf/wall/elevator/Initialize(var/ml) - . = ..(ml, /decl/material/solid/metal/alienalloy/elevatorium) diff --git a/maps/example/example_shuttles.dm b/maps/example/example_shuttles.dm index bcc91db25c6..cccd4c24903 100644 --- a/maps/example/example_shuttles.dm +++ b/maps/example/example_shuttles.dm @@ -31,7 +31,7 @@ /obj/abstract/turbolift_spawner/example name = "Testing Site elevator placeholder" - icon = 'icons/obj/turbolift_preview_nowalls_3x3.dmi' + icon = 'mods/content/turbolift/icons/turbolift_preview_nowalls_3x3.dmi' depth = 3 lift_size_x = 2 lift_size_y = 2 diff --git a/maps/exodus/exodus_elevator.dm b/maps/exodus/exodus_elevator.dm index a455e5ad5bd..82176c89dc2 100644 --- a/maps/exodus/exodus_elevator.dm +++ b/maps/exodus/exodus_elevator.dm @@ -23,7 +23,7 @@ /obj/abstract/turbolift_spawner/exodus/engineering name = "Exodus turbolift map placeholder - Engineering" - icon = 'icons/obj/turbolift_preview_3x3.dmi' + icon = 'mods/content/turbolift/icons/turbolift_preview_3x3.dmi' dir = EAST lift_size_x = 4 lift_size_y = 4 diff --git a/maps/ministation/ministation_shuttles.dm b/maps/ministation/ministation_shuttles.dm index af90fe8838e..a79f5ae6243 100644 --- a/maps/ministation/ministation_shuttles.dm +++ b/maps/ministation/ministation_shuttles.dm @@ -73,7 +73,7 @@ // Essentially a bare platform that moves up and down. /obj/abstract/turbolift_spawner/ministation name = "Tradestation cargo elevator placeholder" -// icon = 'icons/obj/turbolift_preview_nowalls_3x3.dmi' +// icon = 'mods/content/turbolift/icons/turbolift_preview_nowalls_3x3.dmi' depth = 3 lift_size_x = 2 lift_size_y = 2 diff --git a/maps/tradeship/tradeship.dm b/maps/tradeship/tradeship.dm index 2101d73bb84..36be422ad7a 100644 --- a/maps/tradeship/tradeship.dm +++ b/maps/tradeship/tradeship.dm @@ -34,6 +34,7 @@ #include "../../mods/content/sealant_gun/_sealant_gun.dme" #include "../../mods/content/standard_jobs/_standard_jobs.dme" #include "../../mods/content/supermatter/_supermatter.dme" + #include "../../mods/content/turbolift/_turbolift.dme" #include "../../mods/content/ventcrawl/_ventcrawl.dme" #include "../../mods/content/xenobiology/_xenobiology.dme" #include "../../mods/content/exploration/_exploration.dme" @@ -85,6 +86,7 @@ #include "tradeship_overrides.dm" #include "tradeship_shuttles.dm" #include "tradeship_spawnpoints.dm" + #include "tradeship_turbolift.dm" #include "tradeship_unit_testing.dm" #include "tradeship-0.dmm" #include "tradeship-1.dmm" diff --git a/maps/tradeship/tradeship_shuttles.dm b/maps/tradeship/tradeship_shuttles.dm index c309ad625a2..66b5728ba50 100644 --- a/maps/tradeship/tradeship_shuttles.dm +++ b/maps/tradeship/tradeship_shuttles.dm @@ -43,26 +43,3 @@ /obj/effect/shuttle_landmark/below_deck_starboardastern name = "Near CSV Tradeship Starboard Astern" landmark_tag = "nav_tradeship_below_starboardastern" - -// Essentially a bare platform that moves up and down. -/obj/abstract/turbolift_spawner/tradeship - name = "Tradeship cargo elevator placeholder" - icon = 'icons/obj/turbolift_preview_nowalls_4x4.dmi' - depth = 4 - lift_size_x = 3 - lift_size_y = 3 - door_type = null - wall_type = null - firedoor_type = null - light_type = null - floor_type = /turf/floor/tiled/steel_grid - button_type = /obj/structure/lift/button/standalone - panel_type = /obj/structure/lift/panel/standalone - areas_to_use = list( - /area/turbolift/tradeship_enclave, - /area/turbolift/tradeship_cargo, - /area/turbolift/tradeship_upper, - /area/turbolift/tradeship_roof - ) - floor_departure_sound = 'sound/effects/lift_heavy_start.ogg' - floor_arrival_sound = 'sound/effects/lift_heavy_stop.ogg' diff --git a/maps/tradeship/tradeship_turbolift.dm b/maps/tradeship/tradeship_turbolift.dm new file mode 100644 index 00000000000..2b8ecabee46 --- /dev/null +++ b/maps/tradeship/tradeship_turbolift.dm @@ -0,0 +1,22 @@ +// Essentially a bare platform that moves up and down. +/obj/abstract/turbolift_spawner/tradeship + name = "Tradeship cargo elevator placeholder" + icon = 'mods/content/turbolift/icons/turbolift_preview_nowalls_4x4.dmi' + depth = 4 + lift_size_x = 3 + lift_size_y = 3 + door_type = null + wall_type = null + firedoor_type = null + light_type = null + floor_type = /turf/floor/tiled/steel_grid + button_type = /obj/structure/lift/button/standalone + panel_type = /obj/structure/lift/panel/standalone + areas_to_use = list( + /area/turbolift/tradeship_enclave, + /area/turbolift/tradeship_cargo, + /area/turbolift/tradeship_upper, + /area/turbolift/tradeship_roof + ) + floor_departure_sound = 'sound/effects/lift_heavy_start.ogg' + floor_arrival_sound = 'sound/effects/lift_heavy_stop.ogg' diff --git a/mods/_modpack.dm b/mods/_modpack.dm index 0fb1d5d7908..b9bad395444 100644 --- a/mods/_modpack.dm +++ b/mods/_modpack.dm @@ -75,6 +75,14 @@ /decl/modpack/proc/on_roundstart() return +/// This runs before `global.using_map.finalize_map_generation()` in SSmapping initialize. +/decl/modpack/proc/on_mapping_pre_finalize() + return + +/// This runs in SSmisc_late Initialize. +/decl/modpack/proc/on_misc_late_init() + return + /decl/modpack/proc/get_membership_perks() return diff --git a/mods/content/turbolift/_turbolift.dm b/mods/content/turbolift/_turbolift.dm new file mode 100644 index 00000000000..794dac19865 --- /dev/null +++ b/mods/content/turbolift/_turbolift.dm @@ -0,0 +1,3 @@ +/decl/modpack/turbolift + name = "Turbolifts" + desc = "Adds elevators and supporting code." \ No newline at end of file diff --git a/mods/content/turbolift/_turbolift.dme b/mods/content/turbolift/_turbolift.dme new file mode 100644 index 00000000000..27c46ca72c8 --- /dev/null +++ b/mods/content/turbolift/_turbolift.dme @@ -0,0 +1,14 @@ +#ifndef MODPACK_TURBOLIFT +#define MODPACK_TURBOLIFT +// BEGIN_INCLUDE +#include "_turbolift.dm" +#include "turbolift.dm" +#include "turbolift_areas.dm" +#include "turbolift_console.dm" +#include "turbolift_door.dm" +#include "turbolift_floor.dm" +#include "turbolift_init.dm" +#include "turbolift_map.dm" +#include "turbolift_turfs.dm" +// END_INCLUDE +#endif \ No newline at end of file diff --git a/icons/obj/doors/elevator/door.dmi b/mods/content/turbolift/icons/door/door.dmi similarity index 100% rename from icons/obj/doors/elevator/door.dmi rename to mods/content/turbolift/icons/door/door.dmi diff --git a/icons/obj/doors/elevator/fill_glass.dmi b/mods/content/turbolift/icons/door/fill_glass.dmi similarity index 100% rename from icons/obj/doors/elevator/fill_glass.dmi rename to mods/content/turbolift/icons/door/fill_glass.dmi diff --git a/icons/obj/doors/elevator/fill_steel.dmi b/mods/content/turbolift/icons/door/fill_steel.dmi similarity index 100% rename from icons/obj/doors/elevator/fill_steel.dmi rename to mods/content/turbolift/icons/door/fill_steel.dmi diff --git a/icons/obj/doors/elevator/lights_bolts.dmi b/mods/content/turbolift/icons/door/lights_bolts.dmi similarity index 100% rename from icons/obj/doors/elevator/lights_bolts.dmi rename to mods/content/turbolift/icons/door/lights_bolts.dmi diff --git a/icons/obj/doors/elevator/lights_deny.dmi b/mods/content/turbolift/icons/door/lights_deny.dmi similarity index 100% rename from icons/obj/doors/elevator/lights_deny.dmi rename to mods/content/turbolift/icons/door/lights_deny.dmi diff --git a/icons/obj/doors/elevator/lights_green.dmi b/mods/content/turbolift/icons/door/lights_green.dmi similarity index 100% rename from icons/obj/doors/elevator/lights_green.dmi rename to mods/content/turbolift/icons/door/lights_green.dmi diff --git a/icons/obj/turbolift.dmi b/mods/content/turbolift/icons/turbolift.dmi similarity index 100% rename from icons/obj/turbolift.dmi rename to mods/content/turbolift/icons/turbolift.dmi diff --git a/icons/obj/turbolift_preview_3x3.dmi b/mods/content/turbolift/icons/turbolift_preview_3x3.dmi similarity index 100% rename from icons/obj/turbolift_preview_3x3.dmi rename to mods/content/turbolift/icons/turbolift_preview_3x3.dmi diff --git a/icons/obj/turbolift_preview_5x5.dmi b/mods/content/turbolift/icons/turbolift_preview_5x5.dmi similarity index 100% rename from icons/obj/turbolift_preview_5x5.dmi rename to mods/content/turbolift/icons/turbolift_preview_5x5.dmi diff --git a/icons/obj/turbolift_preview_nowalls_3x3.dmi b/mods/content/turbolift/icons/turbolift_preview_nowalls_3x3.dmi similarity index 100% rename from icons/obj/turbolift_preview_nowalls_3x3.dmi rename to mods/content/turbolift/icons/turbolift_preview_nowalls_3x3.dmi diff --git a/icons/obj/turbolift_preview_nowalls_4x4.dmi b/mods/content/turbolift/icons/turbolift_preview_nowalls_4x4.dmi similarity index 100% rename from icons/obj/turbolift_preview_nowalls_4x4.dmi rename to mods/content/turbolift/icons/turbolift_preview_nowalls_4x4.dmi diff --git a/code/modules/turbolift/turbolift.dm b/mods/content/turbolift/turbolift.dm similarity index 100% rename from code/modules/turbolift/turbolift.dm rename to mods/content/turbolift/turbolift.dm diff --git a/code/modules/turbolift/turbolift_areas.dm b/mods/content/turbolift/turbolift_areas.dm similarity index 100% rename from code/modules/turbolift/turbolift_areas.dm rename to mods/content/turbolift/turbolift_areas.dm diff --git a/code/modules/turbolift/turbolift_console.dm b/mods/content/turbolift/turbolift_console.dm similarity index 98% rename from code/modules/turbolift/turbolift_console.dm rename to mods/content/turbolift/turbolift_console.dm index 03c3be6b0e4..934603a1904 100644 --- a/code/modules/turbolift/turbolift_console.dm +++ b/mods/content/turbolift/turbolift_console.dm @@ -1,7 +1,7 @@ // Base type, do not use. /obj/structure/lift name = "turbolift control component" - icon = 'icons/obj/turbolift.dmi' + icon = 'mods/content/turbolift/icons/turbolift.dmi' anchored = TRUE density = FALSE layer = ABOVE_OBJ_LAYER diff --git a/code/modules/turbolift/turbolift_door.dm b/mods/content/turbolift/turbolift_door.dm similarity index 77% rename from code/modules/turbolift/turbolift_door.dm rename to mods/content/turbolift/turbolift_door.dm index f1254ef4f35..faed7d4ea29 100644 --- a/code/modules/turbolift/turbolift_door.dm +++ b/mods/content/turbolift/turbolift_door.dm @@ -5,12 +5,12 @@ autoclose = 0 glass = 1 airlock_type = "Lift" - icon = 'icons/obj/doors/elevator/door.dmi' - fill_file = 'icons/obj/doors/elevator/fill_steel.dmi' - glass_file = 'icons/obj/doors/elevator/fill_glass.dmi' - bolts_file = 'icons/obj/doors/elevator/lights_bolts.dmi' - deny_file = 'icons/obj/doors/elevator/lights_deny.dmi' - lights_file = 'icons/obj/doors/elevator/lights_green.dmi' + icon = 'mods/content/turbolift/icons/door/door.dmi' + fill_file = 'mods/content/turbolift/icons/door/fill_steel.dmi' + glass_file = 'mods/content/turbolift/icons/door/fill_glass.dmi' + bolts_file = 'mods/content/turbolift/icons/door/lights_bolts.dmi' + deny_file = 'mods/content/turbolift/icons/door/lights_deny.dmi' + lights_file = 'mods/content/turbolift/icons/door/lights_green.dmi' paintable = PAINT_WINDOW_PAINTABLE diff --git a/code/modules/turbolift/turbolift_floor.dm b/mods/content/turbolift/turbolift_floor.dm similarity index 100% rename from code/modules/turbolift/turbolift_floor.dm rename to mods/content/turbolift/turbolift_floor.dm diff --git a/mods/content/turbolift/turbolift_init.dm b/mods/content/turbolift/turbolift_init.dm new file mode 100644 index 00000000000..f5010303d30 --- /dev/null +++ b/mods/content/turbolift/turbolift_init.dm @@ -0,0 +1,16 @@ +/decl/modpack/turbolift + /// A list of turbolift holders to initialize. + var/list/obj/abstract/turbolift_spawner/turbolifts_to_initialize = list() + /// A list of turbolift datums whose currently-selected floor will open on misc-late init. + var/list/datum/turbolift/turbolifts_to_open = list() + +/decl/modpack/turbolift/on_mapping_pre_finalize() + // Generate turbolifts last, since away sites may have elevators to generate too. + for(var/obj/abstract/turbolift_spawner/turbolift as anything in turbolifts_to_initialize) + turbolift.build_turbolift() + +/decl/modpack/turbolift/on_misc_late_init() + for(var/datum/turbolift/lift in turbolifts_to_open) + if(!QDELETED(lift)) + lift.open_doors() + turbolifts_to_open.Cut() diff --git a/code/modules/turbolift/turbolift_map.dm b/mods/content/turbolift/turbolift_map.dm similarity index 94% rename from code/modules/turbolift/turbolift_map.dm rename to mods/content/turbolift/turbolift_map.dm index d69a39fd5bb..eeed3f1976a 100644 --- a/code/modules/turbolift/turbolift_map.dm +++ b/mods/content/turbolift/turbolift_map.dm @@ -1,7 +1,7 @@ // Map object. /obj/abstract/turbolift_spawner name = "turbolift map placeholder" - icon = 'icons/obj/turbolift_preview_3x3.dmi' + icon = 'mods/content/turbolift/icons/turbolift_preview_3x3.dmi' dir = SOUTH // Direction of the holder determines the placement of the lift control panel and doors. var/depth = 1 // Number of floors to generate, including the initial floor. var/lift_size_x = 2 // Number of turfs on each axis to generate in addition to the first @@ -31,10 +31,12 @@ INITIALIZE_IMMEDIATE(/obj/abstract/turbolift_spawner) if(SSmapping.initialized) build_turbolift() else - SSmapping.turbolifts_to_initialize += src + var/decl/modpack/turbolift/turbolift_modpack = IMPLIED_DECL + turbolift_modpack.turbolifts_to_initialize += src /obj/abstract/turbolift_spawner/Destroy() - SSmapping.turbolifts_to_initialize -= src + var/decl/modpack/turbolift/turbolift_modpack = IMPLIED_DECL + turbolift_modpack.turbolifts_to_initialize -= src return ..() /obj/abstract/turbolift_spawner/proc/build_turbolift() @@ -254,6 +256,7 @@ INITIALIZE_IMMEDIATE(/obj/abstract/turbolift_spawner) if(SSmisc_late.initialized) lift.open_doors() else - SSmisc_late.turbolifts_to_open += lift + var/decl/modpack/turbolift/turbolift_modpack = IMPLIED_DECL + turbolift_modpack.turbolifts_to_open += lift qdel(src) // We're done. diff --git a/mods/content/turbolift/turbolift_turfs.dm b/mods/content/turbolift/turbolift_turfs.dm new file mode 100644 index 00000000000..4095ad376e6 --- /dev/null +++ b/mods/content/turbolift/turbolift_turfs.dm @@ -0,0 +1,2 @@ +/turf/wall/elevator + material = /decl/material/solid/metal/alienalloy/elevatorium diff --git a/nebula.dme b/nebula.dme index f904c51d030..4966607f9e0 100644 --- a/nebula.dme +++ b/nebula.dme @@ -3933,13 +3933,6 @@ #include "code\modules\turbines\largeturbine.dm" #include "code\modules\turbines\smallturbine.dm" #include "code\modules\turbines\turbine_circuits.dm" -#include "code\modules\turbolift\turbolift.dm" -#include "code\modules\turbolift\turbolift_areas.dm" -#include "code\modules\turbolift\turbolift_console.dm" -#include "code\modules\turbolift\turbolift_door.dm" -#include "code\modules\turbolift\turbolift_floor.dm" -#include "code\modules\turbolift\turbolift_map.dm" -#include "code\modules\turbolift\turbolift_turfs.dm" #include "code\modules\vehicles\bike.dm" #include "code\modules\vehicles\cargo_train.dm" #include "code\modules\vehicles\cargo_trolley.dm" From 7eb8b505d8c280fd0d9fe37a1a6e5bb250f6c412 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sun, 5 Jul 2026 19:29:59 -0400 Subject: [PATCH 69/79] Remove turbolifts from example modpack --- maps/example/example-1.dmm | 78 ++++++-------------- maps/example/example-2.dmm | 36 +++++++--- maps/example/example-3.dmm | 103 +++++++++++++-------------- maps/example/example_areas.dm | 35 --------- maps/example/example_shuttles.dm | 20 ------ maps/example/example_unit_testing.dm | 1 - 6 files changed, 95 insertions(+), 178 deletions(-) diff --git a/maps/example/example-1.dmm b/maps/example/example-1.dmm index 500a16664c7..77809d5fc12 100644 --- a/maps/example/example-1.dmm +++ b/maps/example/example-1.dmm @@ -141,12 +141,6 @@ "gO" = ( /turf/wall/titanium, /area/shuttle/ferry) -"gT" = ( -/obj/effect/floor_decal/industrial/warning{ - dir = 6 - }, -/turf/floor/tiled/steel_grid, -/area/example/first) "he" = ( /obj/effect/floor_decal/industrial/warning{ dir = 4 @@ -225,9 +219,7 @@ dir = 1 }, /obj/structure/ladder, -/obj/effect/floor_decal/industrial/warning{ - dir = 4 - }, +/obj/effect/floor_decal/industrial/warning/full, /turf/floor/tiled/dark/monotile, /area/example/first) "mo" = ( @@ -427,10 +419,6 @@ /obj/machinery/light, /turf/floor/tiled/steel_grid, /area/example/first) -"tA" = ( -/obj/abstract/turbolift_spawner/example, -/turf/floor, -/area/turbolift/example/first) "uD" = ( /obj/machinery/light, /turf/floor, @@ -652,15 +640,6 @@ /obj/machinery/light, /turf/floor/tiled/steel_grid, /area/example/first) -"FZ" = ( -/obj/effect/floor_decal/industrial/warning/corner{ - dir = 1 - }, -/obj/machinery/light{ - dir = 4 - }, -/turf/floor, -/area/example/first) "Gg" = ( /obj/structure/railing/mapped{ dir = 1 @@ -762,15 +741,9 @@ }, /turf/floor/tiled/steel_grid, /area/example/first) -"LM" = ( -/obj/effect/floor_decal/industrial/warning{ - dir = 1 - }, -/turf/floor, -/area/example/first) "LP" = ( -/turf/floor/plating, -/area/turbolift/example/first) +/turf/floor/tiled/dark/monotile, +/area/example/first) "LW" = ( /obj/machinery/teleport/station, /turf/floor/tiled/dark/monotile, @@ -810,12 +783,6 @@ /obj/effect/floor_decal/industrial/outline/red, /turf/floor/tiled/dark/monotile, /area/example/first) -"Pp" = ( -/obj/effect/floor_decal/industrial/warning/corner{ - dir = 4 - }, -/turf/floor, -/area/example/first) "Pv" = ( /obj/structure/rack, /obj/item/gun/projectile/shotgun/pump, @@ -871,9 +838,6 @@ }, /turf/floor/tiled/steel_grid, /area/example/first) -"RW" = ( -/turf/floor, -/area/turbolift/example/first) "Ss" = ( /obj/structure/tank_rack/oxygen, /obj/effect/floor_decal/corner/orange/half{ @@ -932,10 +896,10 @@ /turf/floor, /area/example/first) "Wi" = ( -/obj/effect/floor_decal/industrial/warning/fulltile, -/obj/effect/floor_decal/industrial/warning{ - dir = 8 +/obj/effect/floor_decal/corner/orange{ + dir = 6 }, +/obj/effect/floor_decal/industrial/warning, /turf/floor/tiled/steel_grid, /area/example/first) "Wj" = ( @@ -2892,9 +2856,9 @@ XZ XZ XZ mc -xT -gT -Pp +CU +WW +Yp VY kw kw @@ -2946,9 +2910,9 @@ KT oz XZ LP -LP -tA -LM +CU +WW +Yp VY kw kw @@ -3000,9 +2964,9 @@ CU oz XZ LP -LP -RW -LM +CU +WW +Yp on he tt @@ -3054,9 +3018,9 @@ CU oz XZ LP -LP -RW -LM +CU +WW +Yp Yp WF WF @@ -3107,10 +3071,10 @@ Mc CU oz XZ +LP +hA Wi -Wi -Wi -FZ +WS Yp jP HA diff --git a/maps/example/example-2.dmm b/maps/example/example-2.dmm index 51b091fc0d0..6e3749b02a9 100644 --- a/maps/example/example-2.dmm +++ b/maps/example/example-2.dmm @@ -130,6 +130,9 @@ /obj/effect/floor_decal/industrial/warning{ dir = 4 }, +/obj/structure/railing/mapped{ + dir = 4 + }, /turf/floor/tiled/dark/monotile, /area/example/second) "kf" = ( @@ -495,6 +498,9 @@ /obj/effect/floor_decal/industrial/warning{ dir = 4 }, +/obj/structure/railing/mapped{ + dir = 4 + }, /turf/floor/tiled/steel_grid, /area/example/second) "Gn" = ( @@ -649,14 +655,24 @@ }, /turf/floor, /area/example/second) +"Vp" = ( +/obj/structure/ladder, +/obj/structure/railing/mapped{ + dir = 4 + }, +/turf/open, +/area/example/second) "VG" = ( /obj/machinery/fabricator/bioprinter, /obj/effect/floor_decal/corner/blue/mono, /turf/floor/tiled/white/monotile, /area/example/second) "VL" = ( +/obj/structure/sign/warning/fall{ + pixel_y = 32 + }, /turf/open, -/area/turbolift/example/second) +/area/example/second) "WW" = ( /obj/effect/floor_decal/corner/mauve/mono, /obj/machinery/recycler, @@ -2588,7 +2604,7 @@ sA XE XE XE -NY +Vp jW FE Gn @@ -2643,8 +2659,8 @@ qV CW XE VL -VL -VL +wP +wP Gn wP wP @@ -2696,9 +2712,9 @@ CW CW CW XE -VL -VL -VL +wP +wP +wP Gn wP wP @@ -2750,9 +2766,9 @@ CW CW CW XE -VL -VL -VL +wP +wP +wP Gn wP wP diff --git a/maps/example/example-3.dmm b/maps/example/example-3.dmm index 018cb124270..37b7b892fb9 100644 --- a/maps/example/example-3.dmm +++ b/maps/example/example-3.dmm @@ -10,9 +10,6 @@ /obj/effect/floor_decal/industrial/warning/dust, /turf/floor, /area/example/third) -"cj" = ( -/turf/open, -/area/turbolift/example/third) "cw" = ( /obj/machinery/atmospherics/portables_connector, /obj/machinery/portable_atmospherics/canister/empty, @@ -273,10 +270,6 @@ }, /turf/floor, /area/example/third) -"TM" = ( -/obj/structure/catwalk, -/turf/open, -/area/example/third) "TO" = ( /obj/machinery/atmospherics/pipe/simple/hidden, /obj/effect/floor_decal/industrial/warning/dust{ @@ -1818,9 +1811,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -1872,9 +1865,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -1926,9 +1919,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -1980,9 +1973,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -2034,9 +2027,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -2088,9 +2081,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -2142,9 +2135,9 @@ Ke Ke Ke Ke -TM -TM -TM +Ke +Ke +Ke Ke Ke Ke @@ -2195,11 +2188,11 @@ Ke Ke Ke Ke -TM -TM -TM -TM -TM +Ke +Ke +Ke +Ke +Ke Ke Ke Ke @@ -2249,11 +2242,11 @@ Ke Ke Ke Ke -TM -cj -cj -cj -TM +Ke +Ke +Ke +Ke +Ke Ke Ke Ke @@ -2303,11 +2296,11 @@ Ke Ke Ke Ke -TM -cj -cj -cj -TM +Ke +Ke +Ke +Ke +Ke Ke Ke Ke @@ -2357,11 +2350,11 @@ Ke Ke Ke Ke -TM -cj -cj -cj -TM +Ke +Ke +Ke +Ke +Ke Ke Ke Ke @@ -2411,11 +2404,11 @@ Ke Ke Ke Ke -TM Ke Ke Ke -TM +Ke +Ke Ke Ke Ke @@ -2465,11 +2458,11 @@ Ke Ke Ke Ke -TM -TM -TM -TM -TM +Ke +Ke +Ke +Ke +Ke Ke Ke Ke diff --git a/maps/example/example_areas.dm b/maps/example/example_areas.dm index 39eb70c9cbd..856a1b9daa2 100644 --- a/maps/example/example_areas.dm +++ b/maps/example/example_areas.dm @@ -14,41 +14,6 @@ name = "\improper Testing Site Third Floor" icon_state = "storage" -/area/turbolift/example - abstract_type = /area/turbolift/example - name = "\improper Testing Site Elevator" - icon_state = "shuttle" - requires_power = FALSE - dynamic_lighting = TRUE - sound_env = STANDARD_STATION - area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED - ambience = list( - 'sound/ambience/ambigen3.ogg', - 'sound/ambience/ambigen4.ogg', - 'sound/ambience/ambigen5.ogg', - 'sound/ambience/ambigen6.ogg', - 'sound/ambience/ambigen7.ogg', - 'sound/ambience/ambigen8.ogg', - 'sound/ambience/ambigen9.ogg', - 'sound/ambience/ambigen10.ogg', - 'sound/ambience/ambigen11.ogg', - 'sound/ambience/ambigen12.ogg' - ) - arrival_sound = null - lift_announce_str = null - - base_turf = /turf/open - -/area/turbolift/example/first - name = "Testing Site First Floor Lift" - base_turf = /turf/floor/plating - -/area/turbolift/example/second - name = "Testing Site Second Floor Lift" - -/area/turbolift/example/third - name = "Testing Site Third Floor Lift" - /area/shuttle/ferry name = "\improper Testing Site Ferry" icon_state = "shuttle" diff --git a/maps/example/example_shuttles.dm b/maps/example/example_shuttles.dm index cccd4c24903..e78882e4162 100644 --- a/maps/example/example_shuttles.dm +++ b/maps/example/example_shuttles.dm @@ -29,23 +29,3 @@ ) ceiling_type = /turf/floor/shuttle_ceiling -/obj/abstract/turbolift_spawner/example - name = "Testing Site elevator placeholder" - icon = 'mods/content/turbolift/icons/turbolift_preview_nowalls_3x3.dmi' - depth = 3 - lift_size_x = 2 - lift_size_y = 2 - door_type = null - wall_type = null - firedoor_type = null - light_type = null - floor_type = /turf/floor/tiled/techfloor - button_type = /obj/structure/lift/button/standalone - panel_type = /obj/structure/lift/panel/standalone - areas_to_use = list( - /area/turbolift/example/first, - /area/turbolift/example/second, - /area/turbolift/example/third - ) - floor_departure_sound = 'sound/effects/lift_heavy_start.ogg' - floor_arrival_sound = 'sound/effects/lift_heavy_stop.ogg' diff --git a/maps/example/example_unit_testing.dm b/maps/example/example_unit_testing.dm index df857c78a41..906851bd4fb 100644 --- a/maps/example/example_unit_testing.dm +++ b/maps/example/example_unit_testing.dm @@ -3,7 +3,6 @@ apc_test_exempt_areas = list( /area/space = NO_SCRUBBER|NO_VENT|NO_APC, /area/exoplanet = NO_SCRUBBER|NO_VENT|NO_APC, - /area/turbolift/example = NO_SCRUBBER|NO_VENT|NO_APC, /area/shuttle/ferry = NO_SCRUBBER|NO_VENT|NO_APC ) From 039ff1526f456934782ccfe2649e5d9584deb1d8 Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Sun, 5 Jul 2026 19:32:00 -0400 Subject: [PATCH 70/79] Add turbolift modpack to maps --- maps/exodus/exodus.dm | 1 + maps/ministation/ministation.dm | 1 + maps/modpack_testing/modpack_testing.dm | 1 + 3 files changed, 3 insertions(+) diff --git a/maps/exodus/exodus.dm b/maps/exodus/exodus.dm index 3553f269e87..5f5fa25e9fc 100644 --- a/maps/exodus/exodus.dm +++ b/maps/exodus/exodus.dm @@ -24,6 +24,7 @@ #include "../../mods/content/xenobiology/_xenobiology.dme" #include "../../mods/content/exploration/_exploration.dme" #include "../../mods/content/tabloids/_tabloids.dme" + #include "../../mods/content/turbolift/_turbolift.dme" #include "../../mods/gamemodes/cult/_cult.dme" #include "../../mods/gamemodes/heist/_heist.dme" diff --git a/maps/ministation/ministation.dm b/maps/ministation/ministation.dm index cf34155e64f..5d97d3401df 100644 --- a/maps/ministation/ministation.dm +++ b/maps/ministation/ministation.dm @@ -32,6 +32,7 @@ Twice... #include "../../mods/content/mouse_highlights/_mouse_highlight.dme" #include "../../mods/content/pheromones/_pheromones.dme" #include "../../mods/content/psionics/_psionics.dme" + #include "../../mods/content/turbolift/_turbolift.dme" #include "../../mods/content/sealant_gun/_sealant_gun.dme" #include "../../mods/content/standard_jobs/_standard_jobs.dme" #include "../../mods/content/supermatter/_supermatter.dme" diff --git a/maps/modpack_testing/modpack_testing.dm b/maps/modpack_testing/modpack_testing.dm index fdb35649f5c..6e506a09cf3 100644 --- a/maps/modpack_testing/modpack_testing.dm +++ b/maps/modpack_testing/modpack_testing.dm @@ -38,6 +38,7 @@ #include "../../mods/content/standard_jobs/_standard_jobs.dme" #include "../../mods/content/supermatter/_supermatter.dme" #include "../../mods/content/tabloids/_tabloids.dme" + #include "../../mods/content/turbolift/_turbolift.dme" #include "../../mods/content/undead/_undead.dme" #include "../../mods/content/ventcrawl/_ventcrawl.dme" #include "../../mods/content/xenobiology/_xenobiology.dme" From be2adc6732ba805008445a431d2dcdc5e84286ac Mon Sep 17 00:00:00 2001 From: Noelle Lavenza Date: Mon, 6 Jul 2026 11:56:28 -0400 Subject: [PATCH 71/79] Fix offset unit test with turbolift modpack unloaded --- code/unit_tests/offset_tests.dm | 1 - mods/content/turbolift/turbolift_console.dm | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/code/unit_tests/offset_tests.dm b/code/unit_tests/offset_tests.dm index b4605420adc..fb1537227ff 100644 --- a/code/unit_tests/offset_tests.dm +++ b/code/unit_tests/offset_tests.dm @@ -45,7 +45,6 @@ var/static/list/exception_types = list( /obj/machinery/light, /obj/machinery/camera, - /obj/structure/lift/button/standalone, /obj/structure/hygiene/sink ) diff --git a/mods/content/turbolift/turbolift_console.dm b/mods/content/turbolift/turbolift_console.dm index 934603a1904..4f09b77619f 100644 --- a/mods/content/turbolift/turbolift_console.dm +++ b/mods/content/turbolift/turbolift_console.dm @@ -72,6 +72,7 @@ update_icon() /obj/structure/lift/button/standalone + directional_offset = null icon_state = "plinth" /obj/structure/lift/button/on_update_icon() From d1c416ae723dc9c33c8fb9e5b89735e11efe7071 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Wed, 10 Sep 2025 00:54:42 -0400 Subject: [PATCH 72/79] Move singularity engine into its own module --- .../{power => }/singularity/collector.dm | 0 .../singularity/containment_field.dm | 0 .../{power => }/singularity/emitter.dm | 0 .../singularity/field_generator.dm | 0 .../{power => }/singularity/generator.dm | 0 .../particle_accelerator/particle.dm | 0 .../particle_accelerator.dm | 0 .../particle_accelerator/particle_chamber.dm | 0 .../particle_accelerator/particle_control.dm | 0 .../particle_accelerator/particle_emitter.dm | 0 .../particle_accelerator/particle_power.dm | 0 .../{power => }/singularity/singularity.dm | 0 .../singularity/singularity_events.dm | 0 .../singularity/singularity_stages.dm | 0 nebula.dme | 28 +++++++++---------- 15 files changed, 14 insertions(+), 14 deletions(-) rename code/modules/{power => }/singularity/collector.dm (100%) rename code/modules/{power => }/singularity/containment_field.dm (100%) rename code/modules/{power => }/singularity/emitter.dm (100%) rename code/modules/{power => }/singularity/field_generator.dm (100%) rename code/modules/{power => }/singularity/generator.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle_accelerator.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle_chamber.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle_control.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle_emitter.dm (100%) rename code/modules/{power => }/singularity/particle_accelerator/particle_power.dm (100%) rename code/modules/{power => }/singularity/singularity.dm (100%) rename code/modules/{power => }/singularity/singularity_events.dm (100%) rename code/modules/{power => }/singularity/singularity_stages.dm (100%) diff --git a/code/modules/power/singularity/collector.dm b/code/modules/singularity/collector.dm similarity index 100% rename from code/modules/power/singularity/collector.dm rename to code/modules/singularity/collector.dm diff --git a/code/modules/power/singularity/containment_field.dm b/code/modules/singularity/containment_field.dm similarity index 100% rename from code/modules/power/singularity/containment_field.dm rename to code/modules/singularity/containment_field.dm diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/singularity/emitter.dm similarity index 100% rename from code/modules/power/singularity/emitter.dm rename to code/modules/singularity/emitter.dm diff --git a/code/modules/power/singularity/field_generator.dm b/code/modules/singularity/field_generator.dm similarity index 100% rename from code/modules/power/singularity/field_generator.dm rename to code/modules/singularity/field_generator.dm diff --git a/code/modules/power/singularity/generator.dm b/code/modules/singularity/generator.dm similarity index 100% rename from code/modules/power/singularity/generator.dm rename to code/modules/singularity/generator.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle.dm b/code/modules/singularity/particle_accelerator/particle.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle.dm rename to code/modules/singularity/particle_accelerator/particle.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm b/code/modules/singularity/particle_accelerator/particle_accelerator.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle_accelerator.dm rename to code/modules/singularity/particle_accelerator/particle_accelerator.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle_chamber.dm b/code/modules/singularity/particle_accelerator/particle_chamber.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle_chamber.dm rename to code/modules/singularity/particle_accelerator/particle_chamber.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle_control.dm b/code/modules/singularity/particle_accelerator/particle_control.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle_control.dm rename to code/modules/singularity/particle_accelerator/particle_control.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle_emitter.dm b/code/modules/singularity/particle_accelerator/particle_emitter.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle_emitter.dm rename to code/modules/singularity/particle_accelerator/particle_emitter.dm diff --git a/code/modules/power/singularity/particle_accelerator/particle_power.dm b/code/modules/singularity/particle_accelerator/particle_power.dm similarity index 100% rename from code/modules/power/singularity/particle_accelerator/particle_power.dm rename to code/modules/singularity/particle_accelerator/particle_power.dm diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/singularity/singularity.dm similarity index 100% rename from code/modules/power/singularity/singularity.dm rename to code/modules/singularity/singularity.dm diff --git a/code/modules/power/singularity/singularity_events.dm b/code/modules/singularity/singularity_events.dm similarity index 100% rename from code/modules/power/singularity/singularity_events.dm rename to code/modules/singularity/singularity_events.dm diff --git a/code/modules/power/singularity/singularity_stages.dm b/code/modules/singularity/singularity_stages.dm similarity index 100% rename from code/modules/power/singularity/singularity_stages.dm rename to code/modules/singularity/singularity_stages.dm diff --git a/nebula.dme b/nebula.dme index f904c51d030..be70c91e3ec 100644 --- a/nebula.dme +++ b/nebula.dme @@ -3470,20 +3470,6 @@ #include "code\modules\power\cable\heavycable.dm" #include "code\modules\power\cell\_cell.dm" #include "code\modules\power\cell\cell_types.dm" -#include "code\modules\power\singularity\collector.dm" -#include "code\modules\power\singularity\containment_field.dm" -#include "code\modules\power\singularity\emitter.dm" -#include "code\modules\power\singularity\field_generator.dm" -#include "code\modules\power\singularity\generator.dm" -#include "code\modules\power\singularity\singularity.dm" -#include "code\modules\power\singularity\singularity_events.dm" -#include "code\modules\power\singularity\singularity_stages.dm" -#include "code\modules\power\singularity\particle_accelerator\particle.dm" -#include "code\modules\power\singularity\particle_accelerator\particle_accelerator.dm" -#include "code\modules\power\singularity\particle_accelerator\particle_chamber.dm" -#include "code\modules\power\singularity\particle_accelerator\particle_control.dm" -#include "code\modules\power\singularity\particle_accelerator\particle_emitter.dm" -#include "code\modules\power\singularity\particle_accelerator\particle_power.dm" #include "code\modules\power\solar\solar_control.dm" #include "code\modules\power\solar\solar_panel.dm" #include "code\modules\power\solar\tracker.dm" @@ -3805,6 +3791,20 @@ #include "code\modules\shuttles\shuttle_specops.dm" #include "code\modules\shuttles\shuttle_supply.dm" #include "code\modules\shuttles\shuttles_multi.dm" +#include "code\modules\singularity\collector.dm" +#include "code\modules\singularity\containment_field.dm" +#include "code\modules\singularity\emitter.dm" +#include "code\modules\singularity\field_generator.dm" +#include "code\modules\singularity\generator.dm" +#include "code\modules\singularity\singularity.dm" +#include "code\modules\singularity\singularity_events.dm" +#include "code\modules\singularity\singularity_stages.dm" +#include "code\modules\singularity\particle_accelerator\particle.dm" +#include "code\modules\singularity\particle_accelerator\particle_accelerator.dm" +#include "code\modules\singularity\particle_accelerator\particle_chamber.dm" +#include "code\modules\singularity\particle_accelerator\particle_control.dm" +#include "code\modules\singularity\particle_accelerator\particle_emitter.dm" +#include "code\modules\singularity\particle_accelerator\particle_power.dm" #include "code\modules\smes\_smes.dm" #include "code\modules\smes\smes_buildable.dm" #include "code\modules\smes\smes_circuit.dm" From 6f4701490a924e380736b40ba78d171546a6f023 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Fri, 7 Aug 2026 15:24:30 -0400 Subject: [PATCH 73/79] Move emitters from singulo module to game/machinery --- .../singularity => game/machinery}/emitter.dm | 49 ++++++++++--------- nebula.dme | 2 +- 2 files changed, 27 insertions(+), 24 deletions(-) rename code/{modules/singularity => game/machinery}/emitter.dm (93%) diff --git a/code/modules/singularity/emitter.dm b/code/game/machinery/emitter.dm similarity index 93% rename from code/modules/singularity/emitter.dm rename to code/game/machinery/emitter.dm index 192376cc994..d46f3e2c2ec 100644 --- a/code/modules/singularity/emitter.dm +++ b/code/game/machinery/emitter.dm @@ -12,16 +12,19 @@ var/efficiency = 0.3 // Energy efficiency. 30% at this time, so 100kW load means 30kW laser pulses. var/minimum_power = 10 KILOWATTS // The minimum power below which the emitter will turn off; different than the power needed to fire. - var/active = 0 + var/active = FALSE var/fire_delay = 100 var/max_burst_delay = 100 var/min_burst_delay = 20 var/burst_shots = 3 var/last_shot = 0 var/shot_number = 0 - var/state = 0 - var/locked = 0 - var/powered = 0 + var/const/STATE_UNSECURE = 0 + var/const/STATE_BOLTED = 1 + var/const/STATE_WELDED = 2 + var/state = STATE_UNSECURE + var/locked = FALSE + var/powered = FALSE core_skill = SKILL_ENGINES uncreated_component_parts = list( @@ -39,7 +42,7 @@ /obj/machinery/emitter/anchored anchored = TRUE - state = 2 + state = STATE_WELDED /obj/machinery/emitter/Destroy() log_and_message_admins("deleted \the [src]") @@ -62,15 +65,15 @@ if(!istype(user)) user = null // safety, as the proc is publicly available. - if(state == 2) + if(state == STATE_WELDED) if(!locked) - if(active==1) - active = 0 + if(active) + active = FALSE to_chat(user, "You turn off \the [src].") log_and_message_admins("turned off \the [src]", user) investigate_log("turned off by [key_name_admin(user)]","singulo") else - active = 1 + active = TRUE if(user) operator_skill = user.get_skill_value(core_skill) update_efficiency() @@ -99,11 +102,11 @@ /obj/machinery/emitter/Process() if(stat & (BROKEN)) return - if(state != 2) + if(state != STATE_WELDED) active = FALSE update_icon() return - if(((last_shot + fire_delay) <= world.time) && (active == 1)) + if(((last_shot + fire_delay) <= world.time) && active) if(active_power_usage - can_use_power_oneoff(active_power_usage) < minimum_power) powered = FALSE update_icon() @@ -140,21 +143,21 @@ to_chat(user, "Turn off [src] first.") return TRUE switch(state) - if(0) - state = 1 + if(STATE_UNSECURE) + state = STATE_BOLTED playsound(loc, 'sound/items/Ratchet.ogg', 75, 1) user.visible_message("[user.name] secures [src] to the floor.", \ "You secure the external reinforcing bolts to the floor.", \ "You hear a ratchet.") anchored = TRUE - if(1) - state = 0 + if(STATE_BOLTED) + state = STATE_UNSECURE playsound(loc, 'sound/items/Ratchet.ogg', 75, 1) user.visible_message("[user.name] unsecures [src] reinforcing bolts from the floor.", \ "You undo the external reinforcing bolts.", \ "You hear a ratchet.") anchored = FALSE - if(2) + if(STATE_WELDED) to_chat(user, "\The [src] needs to be unwelded from the floor.") return TRUE @@ -164,9 +167,9 @@ to_chat(user, "Turn off [src] first.") return TRUE switch(state) - if(0) + if(STATE_UNSECURE) to_chat(user, "\The [src] needs to be wrenched to the floor.") - if(1) + if(STATE_BOLTED) if (!welder.weld(0,user)) to_chat(user, "You need more welding fuel to complete this task.") return TRUE @@ -177,9 +180,9 @@ if (!do_after(user, 2 SECONDS, src)) return TRUE if(!src || !welder.isOn()) return TRUE - state = 2 + state = STATE_WELDED to_chat(user, "You weld [src] to the floor.") - if(2) + if(STATE_WELDED) if (welder.weld(0,user)) playsound(loc, 'sound/items/Welder2.ogg', 50, 1) user.visible_message("[user.name] starts to cut [src] free from the floor.", \ @@ -188,7 +191,7 @@ if (!do_after(user, 2 SECONDS, src)) return TRUE if(!src || !welder.isOn()) return TRUE - state = 1 + state = STATE_BOLTED to_chat(user, "You cut [src] free from the floor.") else to_chat(user, "You need more welding fuel to complete this task.") @@ -208,8 +211,8 @@ /obj/machinery/emitter/emag_act(var/remaining_charges, var/mob/user) if(!emagged) - locked = 0 - emagged = 1 + locked = FALSE + emagged = TRUE req_access.Cut() user.visible_message("[user.name] emags [src].","You short out the lock.") return 1 diff --git a/nebula.dme b/nebula.dme index be70c91e3ec..a4f48252924 100644 --- a/nebula.dme +++ b/nebula.dme @@ -834,6 +834,7 @@ #include "code\game\machinery\dehumidifier.dm" #include "code\game\machinery\deployable.dm" #include "code\game\machinery\doppler_array.dm" +#include "code\game\machinery\emitter.dm" #include "code\game\machinery\flasher.dm" #include "code\game\machinery\floodlight.dm" #include "code\game\machinery\floor_light.dm" @@ -3793,7 +3794,6 @@ #include "code\modules\shuttles\shuttles_multi.dm" #include "code\modules\singularity\collector.dm" #include "code\modules\singularity\containment_field.dm" -#include "code\modules\singularity\emitter.dm" #include "code\modules\singularity\field_generator.dm" #include "code\modules\singularity\generator.dm" #include "code\modules\singularity\singularity.dm" From 1114dc46df942dc08554611111369aa70ad3816d Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Fri, 7 Aug 2026 17:14:48 -0400 Subject: [PATCH 74/79] Make emitters use construction states --- .../machine_construction/_construction.dm | 1 + .../machine_construction/emitter.dm | 217 ++++++++++++++++++ code/game/machinery/emitter.dm | 114 +++------ code/modules/fusion/gyrotron/gyrotron.dm | 4 +- maps/away/derelict/derelict-station.dmm | 12 +- maps/exodus/exodus-2.dmm | 6 +- maps/ministation/ministation-0.dmm | 6 +- nebula.dme | 1 + .../5401_emitter_construct_state.txt | 8 + 9 files changed, 264 insertions(+), 105 deletions(-) create mode 100644 code/game/machinery/_machines_base/machine_construction/emitter.dm create mode 100644 tools/map_migrations/5401_emitter_construct_state.txt diff --git a/code/game/machinery/_machines_base/machine_construction/_construction.dm b/code/game/machinery/_machines_base/machine_construction/_construction.dm index 3555ee7173f..72679df311c 100644 --- a/code/game/machinery/_machines_base/machine_construction/_construction.dm +++ b/code/game/machinery/_machines_base/machine_construction/_construction.dm @@ -14,6 +14,7 @@ // Called on state transition; can intercept, but must call parent. /obj/machinery/proc/state_transition(var/decl/machine_construction/new_state, var/mob/user) + SHOULD_CALL_PARENT(TRUE) construct_state = new_state // Return a change state define or a fail message to block transition. diff --git a/code/game/machinery/_machines_base/machine_construction/emitter.dm b/code/game/machinery/_machines_base/machine_construction/emitter.dm new file mode 100644 index 00000000000..f1b87c2ad6a --- /dev/null +++ b/code/game/machinery/_machines_base/machine_construction/emitter.dm @@ -0,0 +1,217 @@ +// Emitters are not screwed apart like most machines; they are bolted down with a wrench and then welded in place. +// Some subtypes like gyrotrons also use panel_state to gain the usual maintenance hatch on top of that. +// Yes I hate this, yes it should be done differently, no I do not have it in me to do it any other way. +// Maybe we should just bite the bullet and make gyrotrons not emitters?? +/decl/machine_construction/emitter + visible_components = FALSE + /// The state entered when the emitter is fastened down further, if any. + var/down_state + /// The state entered when the emitter is loosened, if any. + var/up_state + /// Whether the emitter is anchored to the floor in this state. + var/anchored = FALSE + // gyrotron stuff below + /// The state entered when the maintenance hatch is toggled via screwdriver. Null if the emitter has no hatch. + var/panel_state + /// Whether the maintenance hatch is open in this state. + var/panel_open = FALSE + +/decl/machine_construction/emitter/state_is_valid(obj/machinery/machine) + return (machine.anchored == anchored) && (machine.panel_open == panel_open) + +/decl/machine_construction/emitter/validate_state(obj/machinery/machine) + . = ..() + if(!.) + if(machine.panel_open != panel_open) + try_change_state(machine, panel_state) + else + try_change_state(machine, machine.anchored ? down_state : up_state) + +// the panel starts open after construction, again taken from /decl/machine_construction/default/panel_closed +/decl/machine_construction/emitter/post_construct(obj/machinery/machine) + if(!panel_state || panel_open) + return + try_change_state(machine, panel_state) + machine.panel_open = TRUE + machine.queue_icon_update() + +/// Handles a wrench applied to the emitter in this state. Return TRUE if the interaction was handled. +/decl/machine_construction/emitter/proc/wrench_interaction(obj/item/used_item, mob/user, obj/machinery/emitter/machine) + return FALSE + +/// Handles a welding tool applied to the emitter in this state. Return TRUE if the interaction was handled. +/decl/machine_construction/emitter/proc/welder_interaction(obj/item/weldingtool/welder, mob/user, obj/machinery/emitter/machine) + return FALSE + +/decl/machine_construction/emitter/attackby(obj/item/used_item, mob/user, obj/machinery/emitter/machine) + if((. = ..())) + return + if(machine.active) // can't open/close/unweld/etc while operating + to_chat(user, SPAN_WARNING("Turn \the [machine] off first.")) + return TRUE + if(IS_WRENCH(used_item)) + return wrench_interaction(used_item, user, machine) + else if(IS_WELDER(used_item)) + return welder_interaction(used_item, user, machine) + // everything after this is for gyrotrons/etc + if(!panel_state) + return FALSE + // handle this here because otherwise we'd have some nasty code duplication + if(IS_SCREWDRIVER(used_item)) + TRANSFER_STATE(panel_state) + playsound(get_turf(machine), 'sound/items/Screwdriver.ogg', 50, 1) + machine.panel_open = !panel_open + to_chat(user, SPAN_NOTICE("You [machine.panel_open ? "open" : "close"] the maintenance hatch of \the [machine].")) + machine.update_icon() // could be done in a machinery level /state_transition() override but whatever + return TRUE + // sigh. copied from /decl/machine_construction/default/panel_open and /decl/machine_construction/default/panel_closed + // again done this way to avoid duplication because gyrotrons can have any combo of panel + emitter state + if(!panel_open) + // closed panel (taken from panel_closed) + // maybe these should be on the part replacer or something... + // there's so much code duplication between different panel open/closed states and i hate it. + // maybe we just need to separate it out to a separate state machine and let construct state determine if panel state can change + if(istype(used_item, /obj/item/part_replacer)) + var/obj/item/part_replacer/replacer = used_item + if(replacer.remote_interaction) + machine.part_replacement(user, replacer) + for(var/line in machine.get_part_info_strings(user)) + to_chat(user, line) + return TRUE + return FALSE + // open panel (taken from panel_open) + if(IS_CROWBAR(used_item)) + TRANSFER_STATE(/decl/machine_construction/default/deconstructed) + playsound(get_turf(machine), 'sound/items/Crowbar.ogg', 50, 1) + machine.visible_message(SPAN_NOTICE("\The [user] deconstructs \the [machine].")) + machine.dismantle() + return + if(istype(used_item, /obj/item/part_replacer)) + return machine.part_replacement(user, used_item) + if(istype(used_item)) + return machine.part_insertion(user, used_item) + return FALSE + +/decl/machine_construction/emitter/mechanics_info() + . = list() + if(!panel_state) + return + if(panel_open) + . += "Use a screwdriver to close the maintenance hatch." + . += "Use a parts replacer to upgrade some parts." + . += "Use a crowbar to remove the circuit and deconstruct the emitter." + . += "Insert a new part to install it." + else + . += "Use a screwdriver to open the maintenance hatch." + . += "Use a parts replacer to view installed parts." + +/decl/machine_construction/emitter/unsecured + down_state = /decl/machine_construction/emitter/anchored + +/decl/machine_construction/emitter/unsecured/wrench_interaction(obj/item/used_item, mob/user, obj/machinery/emitter/machine) + TRANSFER_STATE(down_state) + playsound(machine.loc, 'sound/items/Ratchet.ogg', 75, 1) + user.visible_message( + "\The [user] secures \the [machine] to the floor.", + "You secure the external reinforcing bolts to the floor.", + "You hear a ratchet.") + machine.anchored = TRUE + return TRUE + +/decl/machine_construction/emitter/unsecured/welder_interaction(obj/item/weldingtool/welder, mob/user, obj/machinery/emitter/machine) + to_chat(user, SPAN_WARNING("\The [machine] needs to be wrenched to the floor.")) + return TRUE + +/decl/machine_construction/emitter/unsecured/mechanics_info() + . = ..() + . += "Use a wrench to anchor the emitter to the floor." + +/decl/machine_construction/emitter/anchored + anchored = TRUE + down_state = /decl/machine_construction/emitter/welded + up_state = /decl/machine_construction/emitter/unsecured + +/decl/machine_construction/emitter/anchored/wrench_interaction(obj/item/used_item, mob/user, obj/machinery/emitter/machine) + TRANSFER_STATE(up_state) + playsound(machine.loc, 'sound/items/Ratchet.ogg', 75, 1) + user.visible_message( + "\The [user] unsecures \the [machine]'s reinforcing bolts from the floor.", + "You undo the external reinforcing bolts.", + "You hear a ratchet.") + machine.anchored = FALSE + return TRUE + +/decl/machine_construction/emitter/anchored/welder_interaction(obj/item/weldingtool/welder, mob/user, obj/machinery/emitter/machine) + if(!welder.do_tool_interaction(TOOL_WELDER, user, machine, 2 SECONDS, \ + "welding", \ + "welding", \ + "You fail to weld \the [machine] to the floor.", \ + fuel_expenditure = 1) \ + ) + return TRUE // failed for whatever reason + TRANSFER_STATE(down_state) + return TRUE + +/decl/machine_construction/emitter/anchored/mechanics_info() + . = ..() + . += "Use a wrench to undo the bolts anchoring the emitter to the floor." + . += "Use a welding tool to weld the emitter to the floor, allowing it to fire." + +/decl/machine_construction/emitter/welded + anchored = TRUE + up_state = /decl/machine_construction/emitter/anchored + +/decl/machine_construction/emitter/welded/wrench_interaction(obj/item/used_item, mob/user, obj/machinery/emitter/machine) + to_chat(user, SPAN_WARNING("\The [machine] needs to be unwelded from the floor.")) + return TRUE + +/decl/machine_construction/emitter/welded/welder_interaction(obj/item/weldingtool/welder, mob/user, obj/machinery/emitter/machine) + if(!welder.do_tool_interaction(TOOL_WELDER, user, machine, 2 SECONDS, \ + "cutting free", \ + "cutting free", \ + "You fail to cut \the [machine] free from the floor.", \ + fuel_expenditure = 1) \ + ) + return TRUE // failed for whatever reason + TRANSFER_STATE(up_state) + return TRUE + +/decl/machine_construction/emitter/welded/mechanics_info() + . = ..() + . += "Use a welding tool to cut the emitter free from the floor." + +// Emitters built from a circuitboard also have a maintenance hatch, giving one state per (bolting, hatch) pair. +/decl/machine_construction/emitter/unsecured/gyrotron + needs_board = "machine" + down_state = /decl/machine_construction/emitter/anchored/gyrotron + panel_state = /decl/machine_construction/emitter/unsecured/gyrotron/panel_open + +/decl/machine_construction/emitter/unsecured/gyrotron/panel_open + panel_open = TRUE + visible_components = TRUE + down_state = /decl/machine_construction/emitter/anchored/gyrotron/panel_open + panel_state = /decl/machine_construction/emitter/unsecured/gyrotron + +/decl/machine_construction/emitter/anchored/gyrotron + needs_board = "machine" + down_state = /decl/machine_construction/emitter/welded/gyrotron + up_state = /decl/machine_construction/emitter/unsecured/gyrotron + panel_state = /decl/machine_construction/emitter/anchored/gyrotron/panel_open + +/decl/machine_construction/emitter/anchored/gyrotron/panel_open + panel_open = TRUE + visible_components = TRUE + down_state = /decl/machine_construction/emitter/welded/gyrotron/panel_open + up_state = /decl/machine_construction/emitter/unsecured/gyrotron/panel_open + panel_state = /decl/machine_construction/emitter/anchored/gyrotron + +/decl/machine_construction/emitter/welded/gyrotron + needs_board = "machine" + up_state = /decl/machine_construction/emitter/anchored/gyrotron + panel_state = /decl/machine_construction/emitter/welded/gyrotron/panel_open + +/decl/machine_construction/emitter/welded/gyrotron/panel_open + panel_open = TRUE + visible_components = TRUE + up_state = /decl/machine_construction/emitter/anchored/gyrotron/panel_open + panel_state = /decl/machine_construction/emitter/welded/gyrotron diff --git a/code/game/machinery/emitter.dm b/code/game/machinery/emitter.dm index d46f3e2c2ec..13b2543c098 100644 --- a/code/game/machinery/emitter.dm +++ b/code/game/machinery/emitter.dm @@ -19,13 +19,10 @@ var/burst_shots = 3 var/last_shot = 0 var/shot_number = 0 - var/const/STATE_UNSECURE = 0 - var/const/STATE_BOLTED = 1 - var/const/STATE_WELDED = 2 - var/state = STATE_UNSECURE var/locked = FALSE var/powered = FALSE core_skill = SKILL_ENGINES + construct_state = /decl/machine_construction/emitter/unsecured uncreated_component_parts = list( /obj/item/stock_parts/radio/receiver, @@ -42,7 +39,11 @@ /obj/machinery/emitter/anchored anchored = TRUE - state = STATE_WELDED + construct_state = /decl/machine_construction/emitter/welded + +/// Returns TRUE if the emitter is able to fire based on its construction state (currently checks if welded down). +/obj/machinery/emitter/proc/can_fire() + return istype(construct_state, /decl/machine_construction/emitter/welded) /obj/machinery/emitter/Destroy() log_and_message_admins("deleted \the [src]") @@ -65,29 +66,28 @@ if(!istype(user)) user = null // safety, as the proc is publicly available. - if(state == STATE_WELDED) - if(!locked) - if(active) - active = FALSE - to_chat(user, "You turn off \the [src].") - log_and_message_admins("turned off \the [src]", user) - investigate_log("turned off by [key_name_admin(user)]","singulo") - else - active = TRUE - if(user) - operator_skill = user.get_skill_value(core_skill) - update_efficiency() - to_chat(user, "You turn on \the [src].") - shot_number = 0 - fire_delay = get_initial_fire_delay() - log_and_message_admins("turned on \the [src]", user) - investigate_log("turned on by [key_name_admin(user)]","singulo") - update_icon() + if(!can_fire()) + to_chat(user, SPAN_WARNING("\The [src] needs to be firmly secured to the floor first.")) + return 1 + if(!locked) + if(active) + active = FALSE + to_chat(user, SPAN_NOTICE("You turn off \the [src].")) + log_and_message_admins("turned off \the [src]", user) + investigate_log("turned off by [key_name_admin(user)]","singulo") else - to_chat(user, "The controls are locked!") + active = TRUE + if(user) + operator_skill = user.get_skill_value(core_skill) + update_efficiency() + to_chat(user, SPAN_NOTICE("You turn on \the [src].")) + shot_number = 0 + fire_delay = get_initial_fire_delay() + log_and_message_admins("turned on \the [src]", user) + investigate_log("turned on by [key_name_admin(user)]","singulo") + update_icon() else - to_chat(user, "\The [src] needs to be firmly secured to the floor first.") - return 1 + to_chat(user, SPAN_WARNING("The controls are locked!")) /obj/machinery/emitter/proc/update_efficiency() efficiency = initial(efficiency) @@ -102,7 +102,7 @@ /obj/machinery/emitter/Process() if(stat & (BROKEN)) return - if(state != STATE_WELDED) + if(!can_fire()) active = FALSE update_icon() return @@ -137,66 +137,6 @@ update_icon() /obj/machinery/emitter/attackby(obj/item/used_item, mob/user) - - if(IS_WRENCH(used_item)) - if(active) - to_chat(user, "Turn off [src] first.") - return TRUE - switch(state) - if(STATE_UNSECURE) - state = STATE_BOLTED - playsound(loc, 'sound/items/Ratchet.ogg', 75, 1) - user.visible_message("[user.name] secures [src] to the floor.", \ - "You secure the external reinforcing bolts to the floor.", \ - "You hear a ratchet.") - anchored = TRUE - if(STATE_BOLTED) - state = STATE_UNSECURE - playsound(loc, 'sound/items/Ratchet.ogg', 75, 1) - user.visible_message("[user.name] unsecures [src] reinforcing bolts from the floor.", \ - "You undo the external reinforcing bolts.", \ - "You hear a ratchet.") - anchored = FALSE - if(STATE_WELDED) - to_chat(user, "\The [src] needs to be unwelded from the floor.") - return TRUE - - if(IS_WELDER(used_item)) - var/obj/item/weldingtool/welder = used_item - if(active) - to_chat(user, "Turn off [src] first.") - return TRUE - switch(state) - if(STATE_UNSECURE) - to_chat(user, "\The [src] needs to be wrenched to the floor.") - if(STATE_BOLTED) - if (!welder.weld(0,user)) - to_chat(user, "You need more welding fuel to complete this task.") - return TRUE - playsound(loc, 'sound/items/Welder2.ogg', 50, 1) - user.visible_message("[user.name] starts to weld [src] to the floor.", \ - "You start to weld [src] to the floor.", \ - "You hear welding.") - if (!do_after(user, 2 SECONDS, src)) - return TRUE - if(!src || !welder.isOn()) return TRUE - state = STATE_WELDED - to_chat(user, "You weld [src] to the floor.") - if(STATE_WELDED) - if (welder.weld(0,user)) - playsound(loc, 'sound/items/Welder2.ogg', 50, 1) - user.visible_message("[user.name] starts to cut [src] free from the floor.", \ - "You start to cut [src] free from the floor.", \ - "You hear welding.") - if (!do_after(user, 2 SECONDS, src)) - return TRUE - if(!src || !welder.isOn()) return TRUE - state = STATE_BOLTED - to_chat(user, "You cut [src] free from the floor.") - else - to_chat(user, "You need more welding fuel to complete this task.") - return TRUE - if(istype(used_item, /obj/item/card/id) || istype(used_item, /obj/item/modular_computer)) if(emagged) to_chat(user, "The lock seems to be broken.") diff --git a/code/modules/fusion/gyrotron/gyrotron.dm b/code/modules/fusion/gyrotron/gyrotron.dm index f70c73ffb5d..9e7f5892920 100644 --- a/code/modules/fusion/gyrotron/gyrotron.dm +++ b/code/modules/fusion/gyrotron/gyrotron.dm @@ -13,7 +13,7 @@ var/rate = 3 var/mega_energy = 1 - construct_state = /decl/machine_construction/default/panel_closed + construct_state = /decl/machine_construction/emitter/unsecured/gyrotron uncreated_component_parts = list( /obj/item/stock_parts/radio/receiver ) @@ -21,7 +21,7 @@ /obj/machinery/emitter/gyrotron/anchored anchored = TRUE - state = 2 + construct_state = /decl/machine_construction/emitter/welded/gyrotron /obj/machinery/emitter/gyrotron/Initialize() set_extension(src, /datum/extension/local_network_member) diff --git a/maps/away/derelict/derelict-station.dmm b/maps/away/derelict/derelict-station.dmm index 8943ba108f6..327b778a292 100644 --- a/maps/away/derelict/derelict-station.dmm +++ b/maps/away/derelict/derelict-station.dmm @@ -3321,10 +3321,8 @@ /turf/floor/plating/airless, /area/AIsattele) "lZ" = ( -/obj/machinery/emitter{ - anchored = 1; - dir = 4; - state = 2 +/obj/machinery/emitter/anchored{ + dir = 4 }, /turf/floor/plating/airless, /area/constructionsite/engineering) @@ -3333,10 +3331,8 @@ /turf/floor/plating/airless, /area/constructionsite/engineering) "mb" = ( -/obj/machinery/emitter{ - anchored = 1; - dir = 8; - state = 2 +/obj/machinery/emitter/anchored{ + dir = 8 }, /turf/floor/plating/airless, /area/constructionsite/engineering) diff --git a/maps/exodus/exodus-2.dmm b/maps/exodus/exodus-2.dmm index 931f8121119..beecd85bba5 100644 --- a/maps/exodus/exodus-2.dmm +++ b/maps/exodus/exodus-2.dmm @@ -61848,10 +61848,8 @@ /turf/floor/plating, /area/exodus/engineering/engine_room) "cIa" = ( -/obj/machinery/emitter{ - anchored = 1; - id_tag = "EngineEmitter"; - state = 2 +/obj/machinery/emitter/anchored{ + id_tag = "EngineEmitter" }, /obj/structure/cable/cyan, /obj/machinery/power/terminal{ diff --git a/maps/ministation/ministation-0.dmm b/maps/ministation/ministation-0.dmm index e3f4b0c453f..5441d9f8411 100644 --- a/maps/ministation/ministation-0.dmm +++ b/maps/ministation/ministation-0.dmm @@ -11078,10 +11078,8 @@ /obj/machinery/power/terminal{ dir = 1 }, -/obj/machinery/emitter{ - anchored = 1; - id_tag = "EngineEmitter"; - state = 2 +/obj/machinery/emitter/anchored{ + id_tag = "EngineEmitter" }, /obj/structure/cable, /turf/floor/plating, diff --git a/nebula.dme b/nebula.dme index a4f48252924..0113eccdcfa 100644 --- a/nebula.dme +++ b/nebula.dme @@ -887,6 +887,7 @@ #include "code\game\machinery\_machines_base\machine_construction\blast_doors.dm" #include "code\game\machinery\_machines_base\machine_construction\computer.dm" #include "code\game\machinery\_machines_base\machine_construction\default.dm" +#include "code\game\machinery\_machines_base\machine_construction\emitter.dm" #include "code\game\machinery\_machines_base\machine_construction\frame.dm" #include "code\game\machinery\_machines_base\machine_construction\item_chassis.dm" #include "code\game\machinery\_machines_base\machine_construction\noninteractive.dm" diff --git a/tools/map_migrations/5401_emitter_construct_state.txt b/tools/map_migrations/5401_emitter_construct_state.txt new file mode 100644 index 00000000000..8ce764621f5 --- /dev/null +++ b/tools/map_migrations/5401_emitter_construct_state.txt @@ -0,0 +1,8 @@ +# emitters use construct states instead of a bespoke state var +# handle redundant var sets if present +/obj/machinery/emitter/anchored{state = 2} : @OLD{@OLD; state = @SKIP; anchored = @SKIP} +/obj/machinery/emitter/gyrotron/anchored{state = 2} : @OLD{@OLD; state = @SKIP; anchored = @SKIP} +# remove state/anchored vars, change subtype if needed +/obj/machinery/emitter/@SUBTYPES{state = 2} : /obj/machinery/emitter/@SUBTYPES/anchored{@OLD; state = @SKIP; anchored = @SKIP} +/obj/machinery/emitter/@SUBTYPES{state = 1} : /obj/machinery/emitter/@SUBTYPES{@OLD; state = @SKIP; anchored = @SKIP} +/obj/machinery/emitter/@SUBTYPES{state = 0} : /obj/machinery/emitter/@SUBTYPES{@OLD; state = @SKIP; anchored = @SKIP} From c07844b9dec45c4af9c4da943365500d5a96fd02 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Fri, 7 Aug 2026 17:15:04 -0400 Subject: [PATCH 75/79] Rename some emitter procs for my own sanity --- code/game/machinery/emitter.dm | 14 ++++++++------ code/modules/fusion/gyrotron/gyrotron.dm | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/code/game/machinery/emitter.dm b/code/game/machinery/emitter.dm index 13b2543c098..a497a6d2c5f 100644 --- a/code/game/machinery/emitter.dm +++ b/code/game/machinery/emitter.dm @@ -114,10 +114,10 @@ var/drawn_power = min(active_power_usage, active_power_usage - use_power_oneoff(active_power_usage)) last_shot = world.time if(shot_number < burst_shots) - fire_delay = get_burst_delay() + fire_delay = get_shot_delay() shot_number ++ else - fire_delay = get_rand_burst_delay() + fire_delay = get_burst_delay() shot_number = 0 //need to calculate the power per shot as the emitter doesn't fire continuously. @@ -163,13 +163,15 @@ return ..() /obj/machinery/emitter/proc/get_initial_fire_delay() - return 100 + return 10 SECONDS -/obj/machinery/emitter/proc/get_rand_burst_delay() +/// The number of deciseconds between each burst-fire grouping. +/obj/machinery/emitter/proc/get_burst_delay() return rand(min_burst_delay, max_burst_delay) -/obj/machinery/emitter/proc/get_burst_delay() - return 2 +/// The number of deciseconds between each shot in a burst. +/obj/machinery/emitter/proc/get_shot_delay() + return 0.2 SECONDS /obj/machinery/emitter/proc/get_emitter_beam() return new /obj/item/projectile/beam/emitter(get_turf(src)) diff --git a/code/modules/fusion/gyrotron/gyrotron.dm b/code/modules/fusion/gyrotron/gyrotron.dm index 9e7f5892920..27fef671223 100644 --- a/code/modules/fusion/gyrotron/gyrotron.dm +++ b/code/modules/fusion/gyrotron/gyrotron.dm @@ -10,6 +10,7 @@ active_power_usage = GYRO_POWER var/initial_id_tag + /// Time between shots, in SECONDS, NOT DECISECONDS var/rate = 3 var/mega_energy = 1 @@ -39,11 +40,11 @@ change_power_consumption(mega_energy * GYRO_POWER, POWER_USE_ACTIVE) . = ..() -/obj/machinery/emitter/gyrotron/get_rand_burst_delay() - return rate*10 - /obj/machinery/emitter/gyrotron/get_burst_delay() - return rate*10 + return rate SECONDS + +/obj/machinery/emitter/gyrotron/get_shot_delay() + return rate SECONDS /obj/machinery/emitter/gyrotron/get_emitter_beam() var/obj/item/projectile/beam/emitter/beam = ..() From 8c7fa0e92c9590b0eda09ea6e2ed00c4e72a815c Mon Sep 17 00:00:00 2001 From: markoatonc Date: Sun, 9 Aug 2026 05:22:52 +0200 Subject: [PATCH 76/79] this time it SHOULD work --- code/modules/clothing/spacesuits/void/void.dm | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/code/modules/clothing/spacesuits/void/void.dm b/code/modules/clothing/spacesuits/void/void.dm index bb33a5fd174..c24f2d54ad9 100644 --- a/code/modules/clothing/spacesuits/void/void.dm +++ b/code/modules/clothing/spacesuits/void/void.dm @@ -274,6 +274,17 @@ else if(##equipment_var) {\ playsound(loc, 'sound/items/Deconstruct.ogg', 50, 1) return TRUE + if(istype(W,/obj/item/suit_cooling_unit)) + if(user.get_equipped_slot_for_item(src) == slot_wear_suit_str) + to_chat(user, "You cannot modify \the [src] while it is being worn.") + else if(tank) + to_chat(user, "\The [src] already has an airtank installed.") + else if(user.try_unequip(W, src)) + to_chat(user, "You insert \the [W] into \the [src]'s storage compartment.") + tank = W + playsound(loc, 'sound/items/Deconstruct.ogg', 50, 1) + return TRUE + return ..() /obj/item/clothing/suit/space/void/attack_self() //sole purpose of existence is to toggle the helmet From 936e499c0a1eb68e32f8ae26e6a2b2bb6c676696 Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Thu, 13 Aug 2026 16:59:13 -0400 Subject: [PATCH 77/79] Replace inline parent with parent call in character info OnTopic --- .../client/preference_setup/records/01_character_info.dm | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/code/modules/client/preference_setup/records/01_character_info.dm b/code/modules/client/preference_setup/records/01_character_info.dm index cd2a6b27b5a..8818af28d2f 100644 --- a/code/modules/client/preference_setup/records/01_character_info.dm +++ b/code/modules/client/preference_setup/records/01_character_info.dm @@ -31,11 +31,8 @@ /datum/category_item/player_setup_item/records/character_info/OnTopic(var/href,var/list/href_list, var/mob/user) - if (record_key && href_list["set_record"]) - var/new_record = sanitize(input(user,"Enter new [lowertext(name)] here.", CHARACTER_PREFERENCE_INPUT_TITLE, html_decode(pref.records[record_key])) as message|null, MAX_PAPER_MESSAGE_LEN, extra = 0) - if(!isnull(new_record) && !jobban_isbanned(user, "Records") && !jobban_isbanned(user, name) && CanUseTopic(user)) - pref.records[record_key] = new_record - return TOPIC_REFRESH + if((. = ..())) // does nothing because it has no records_key + return var/datum/character_information/comments = pref.comments_record_id && SScharacter_info.get_record(pref.comments_record_id, TRUE) if(comments) From 418eb8bf3d144c9780878a8cd266395648f6626d Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Thu, 13 Aug 2026 17:00:38 -0400 Subject: [PATCH 78/79] Add helper for internal pressure difference checks --- code/game/objects/__objs.dm | 17 +++++++++++++++++ .../binary_devices/binary_atmos_base.dm | 6 +----- .../components/portables_connector.dm | 6 +----- code/modules/atmospherics/components/tvalve.dm | 6 +----- .../components/unary/heat_exchanger.dm | 7 +------ .../atmospherics/components/unary/vent_pump.dm | 4 +--- .../components/unary/vent_scrubber.dm | 4 +--- code/modules/atmospherics/components/valve.dm | 6 +----- code/modules/atmospherics/pipes.dm | 8 ++------ 9 files changed, 26 insertions(+), 38 deletions(-) diff --git a/code/game/objects/__objs.dm b/code/game/objects/__objs.dm index 7ed2505ff8c..6bde88df188 100644 --- a/code/game/objects/__objs.dm +++ b/code/game/objects/__objs.dm @@ -73,6 +73,23 @@ /obj/return_air() return loc?.return_air() +/obj/proc/get_internal_pressure_difference() + var/datum/gas_mixture/int_air = return_air() + var/datum/gas_mixture/env_air = loc.return_air() + return int_air.return_pressure()-env_air.return_pressure() + +/// Return TRUE if the internal pressure difference is over `limit`. +/obj/proc/check_internal_pressure_difference_over(limit) + var/datum/gas_mixture/int_air = return_air() + var/datum/gas_mixture/env_air = loc.return_air() + return (int_air.return_pressure()-env_air.return_pressure()) > limit + +/// Return TRUE if the internal pressure difference is under `limit`. +/obj/proc/check_internal_pressure_difference_under(limit) + var/datum/gas_mixture/int_air = return_air() + var/datum/gas_mixture/env_air = loc.return_air() + return (int_air.return_pressure()-env_air.return_pressure()) < limit + /obj/proc/updateUsrDialog() if(in_use) var/is_in_use = 0 diff --git a/code/modules/atmospherics/components/binary_devices/binary_atmos_base.dm b/code/modules/atmospherics/components/binary_devices/binary_atmos_base.dm index 70ccb935f75..f8bbb5b54bc 100644 --- a/code/modules/atmospherics/components/binary_devices/binary_atmos_base.dm +++ b/code/modules/atmospherics/components/binary_devices/binary_atmos_base.dm @@ -23,11 +23,7 @@ return air1 /obj/machinery/atmospherics/binary/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + return !check_internal_pressure_difference_over(2 ATM) // Will only be used if you set the anchorable obj flag. /obj/machinery/atmospherics/binary/wrench_floor_bolts(mob/user, delay = 2 SECONDS, obj/item/tool) diff --git a/code/modules/atmospherics/components/portables_connector.dm b/code/modules/atmospherics/components/portables_connector.dm index d6898b12987..61bc98beb27 100644 --- a/code/modules/atmospherics/components/portables_connector.dm +++ b/code/modules/atmospherics/components/portables_connector.dm @@ -60,11 +60,7 @@ return list(connection.merged_mixture) /obj/machinery/atmospherics/portables_connector/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + return !check_internal_pressure_difference_over(2 ATM) /obj/machinery/atmospherics/portables_connector/cannot_transition_to(state_path, mob/user) if(state_path == /decl/machine_construction/default/deconstructed) diff --git a/code/modules/atmospherics/components/tvalve.dm b/code/modules/atmospherics/components/tvalve.dm index eca39ebd937..f24062ba123 100644 --- a/code/modules/atmospherics/components/tvalve.dm +++ b/code/modules/atmospherics/components/tvalve.dm @@ -117,11 +117,7 @@ return null /obj/machinery/atmospherics/tvalve/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + return !check_internal_pressure_difference_over(2 ATM) /decl/public_access/public_variable/tvalve_state expected_type = /obj/machinery/atmospherics/tvalve diff --git a/code/modules/atmospherics/components/unary/heat_exchanger.dm b/code/modules/atmospherics/components/unary/heat_exchanger.dm index f19c4ecb4f7..6ed50ed3f46 100644 --- a/code/modules/atmospherics/components/unary/heat_exchanger.dm +++ b/code/modules/atmospherics/components/unary/heat_exchanger.dm @@ -76,12 +76,7 @@ partner.update_networks() /obj/machinery/atmospherics/unary/heat_exchanger/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + return !check_internal_pressure_difference_over(2 ATM) /obj/machinery/atmospherics/unary/heat_exchanger/cannot_transition_to(state_path, mob/user) if(state_path == /decl/machine_construction/default/deconstructed) diff --git a/code/modules/atmospherics/components/unary/vent_pump.dm b/code/modules/atmospherics/components/unary/vent_pump.dm index 49fcbbbb276..9041a595620 100644 --- a/code/modules/atmospherics/components/unary/vent_pump.dm +++ b/code/modules/atmospherics/components/unary/vent_pump.dm @@ -341,9 +341,7 @@ break if (hidden_pipe_check && isturf(T) && !T.is_plating()) return SPAN_WARNING("You must remove the plating first.") - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) + if (check_internal_pressure_difference_over(2 ATM)) return SPAN_WARNING("You cannot unwrench \the [src], it is too exerted due to internal pressure.") return ..() diff --git a/code/modules/atmospherics/components/unary/vent_scrubber.dm b/code/modules/atmospherics/components/unary/vent_scrubber.dm index 58fdc8f1717..a22a770b326 100644 --- a/code/modules/atmospherics/components/unary/vent_scrubber.dm +++ b/code/modules/atmospherics/components/unary/vent_scrubber.dm @@ -197,9 +197,7 @@ break if (hidden_pipe_check && isturf(T) && !T.is_plating()) return SPAN_WARNING("You must remove the plating first.") - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) + if (check_internal_pressure_difference_over(2 ATM)) return SPAN_WARNING("You cannot take this [src] apart, it too exerted due to internal pressure.") return ..() diff --git a/code/modules/atmospherics/components/valve.dm b/code/modules/atmospherics/components/valve.dm index 8b639036d73..5ffd62ff082 100644 --- a/code/modules/atmospherics/components/valve.dm +++ b/code/modules/atmospherics/components/valve.dm @@ -113,11 +113,7 @@ return null /obj/machinery/atmospherics/valve/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + return !check_internal_pressure_difference_over(2 ATM) /obj/machinery/atmospherics/valve/get_examine_strings(mob/user, distance, infix, suffix) . = ..() diff --git a/code/modules/atmospherics/pipes.dm b/code/modules/atmospherics/pipes.dm index f31cd7a5c53..db37eef0418 100644 --- a/code/modules/atmospherics/pipes.dm +++ b/code/modules/atmospherics/pipes.dm @@ -135,12 +135,8 @@ . = ..() /obj/machinery/atmospherics/pipe/deconstruction_pressure_check() - var/datum/gas_mixture/int_air = return_air() - var/datum/gas_mixture/env_air = loc.return_air() - - if ((int_air.return_pressure()-env_air.return_pressure()) > (2 ATM)) - return FALSE - return TRUE + // this uses !over instead of under so that it's <= instead of < + return !check_internal_pressure_difference_over(2 ATM) /obj/machinery/atmospherics/pipe/cannot_transition_to(state_path, mob/user) if(state_path == /decl/machine_construction/default/deconstructed) From c953ad257044337b95d2f97562f7e801d05d490c Mon Sep 17 00:00:00 2001 From: Penelope Haze Date: Thu, 13 Aug 2026 17:05:37 -0400 Subject: [PATCH 79/79] Move character info apply code to correct file --- .../client/preference_setup/records/01_character_info.dm | 6 ++++++ code/modules/client/preferences.dm | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/code/modules/client/preference_setup/records/01_character_info.dm b/code/modules/client/preference_setup/records/01_character_info.dm index 8818af28d2f..68cc61b5f7d 100644 --- a/code/modules/client/preference_setup/records/01_character_info.dm +++ b/code/modules/client/preference_setup/records/01_character_info.dm @@ -61,3 +61,9 @@ if(. == TOPIC_REFRESH && istext(pref.comments_record_id) && length(pref.comments_record_id)) SScharacter_info.queue_to_save(pref.comments_record_id) + +/datum/category_item/player_setup_item/records/character_info/apply_post_snapshot_preferences(mob/living/human/character, is_preview_copy = FALSE) + if(is_preview_copy) + return + pref.validate_comments_record() // Make sure a record has been generated for this character. + character.comments_record_id = pref.comments_record_id \ No newline at end of file diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index e851c251390..ad33367976e 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -352,12 +352,6 @@ var/global/list/time_prefs_fixed = list() update_setup_window(usr) return TRUE -/datum/category_item/player_setup_item/records/character_info/apply_post_snapshot_preferences(mob/living/human/character, is_preview_copy = FALSE) - if(is_preview_copy) - return - pref.validate_comments_record() // Make sure a record has been generated for this character. - character.comments_record_id = pref.comments_record_id - /datum/preferences/proc/create_character_from_snapshot(spawn_turf) // Sanitizing rather than saving as someone might still be editing. player_setup.sanitize_setup() @@ -368,7 +362,6 @@ var/global/list/time_prefs_fixed = list() apply_post_snapshot_preferences(character, FALSE) return character - /datum/preferences/proc/copy_to(mob/living/human/character, is_preview_copy = FALSE) apply_snapshot_to_mob(character, is_preview_copy) // this is effectively what create_character_from_snapshot does, but on an existing mob apply_post_snapshot_preferences(character, is_preview_copy) // this is the stuff we need to share