From 5a9e35824726ae5b6726c5ee5d1899c4b1e9f4c3 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:23:24 -0400 Subject: [PATCH 001/101] Flaky issue bot leaves comments on existing issues (#96364) --- tools/pull_request_hooks/rerunFlakyTests.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/pull_request_hooks/rerunFlakyTests.js b/tools/pull_request_hooks/rerunFlakyTests.js index 29fb5eeb9bf2..8170a0541bb0 100644 --- a/tools/pull_request_hooks/rerunFlakyTests.js +++ b/tools/pull_request_hooks/rerunFlakyTests.js @@ -277,8 +277,20 @@ export async function reportFlakyTests({ github, context }) { ); if (existingIssueId !== undefined) { - // Maybe in the future, if it's helpful, update the existing issue with new links console.log(`Existing issue found: #${existingIssueId}`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssueId, + body: createBody( + details, + `https://github.com/${context.repo.owner}/${ + context.repo.repo + }/actions/runs/${context.payload.workflow_run.id}/attempts/${ + context.payload.workflow_run.run_attempt - 1 + }`, + ), + }); return; } From 194ccf566fa5d6825d6cc65e74462fbe8e595fc3 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:23:46 -0400 Subject: [PATCH 002/101] Fix broken security cameras on runtimestation (#96362) --- _maps/map_files/debug/runtimestation.dmm | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/_maps/map_files/debug/runtimestation.dmm b/_maps/map_files/debug/runtimestation.dmm index 9779a6622eec..9196bf5d94cf 100644 --- a/_maps/map_files/debug/runtimestation.dmm +++ b/_maps/map_files/debug/runtimestation.dmm @@ -62,9 +62,9 @@ /turf/open/floor/iron, /area/station/engineering/main) "aq" = ( -/obj/machinery/camera/directional/north, /obj/machinery/computer/monitor, /obj/structure/cable, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/engineering/main) "av" = ( @@ -286,13 +286,13 @@ /turf/closed/wall/r_wall, /area/station/maintenance/aft) "bM" = ( -/obj/machinery/camera/directional/north, /obj/structure/table, /obj/item/construction/rld, /obj/item/construction/rcd/arcd, /obj/effect/turf_decal/tile/blue/half/contrasted{ dir = 4 }, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/command/bridge) "bO" = ( @@ -304,10 +304,10 @@ /turf/open/floor/iron/dark, /area/station/medical/chemistry) "bR" = ( -/obj/machinery/camera/directional/north, /obj/machinery/power/apc/auto_name/directional/north, /obj/structure/cable, /obj/machinery/chem_heater/debug, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron/dark, /area/station/medical/chemistry) "bS" = ( @@ -323,8 +323,8 @@ /area/station/security/brig) "bV" = ( /obj/effect/turf_decal/stripes/line, -/obj/machinery/camera/directional/north, /obj/machinery/status_display/evac/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/construction) "bY" = ( @@ -642,11 +642,11 @@ /area/station/security/brig) "dS" = ( /obj/machinery/atmospherics/components/tank/air, -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/plating, /area/station/engineering/atmos) "dT" = ( -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron/dark, /area/station/engineering/gravity_generator) "dV" = ( @@ -1040,7 +1040,7 @@ /turf/closed/wall/r_wall, /area/station/hallway/secondary/exit/departure_lounge) "gn" = ( -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/security/brig) "gv" = ( @@ -1061,7 +1061,7 @@ /turf/open/floor/iron, /area/station/hallway/secondary/exit/departure_lounge) "gE" = ( -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/hallway/secondary/entry) "gF" = ( @@ -1081,7 +1081,7 @@ /area/station/cargo/storage) "gI" = ( /obj/machinery/light/directional/north, -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/hallway/secondary/entry) "gJ" = ( @@ -1181,7 +1181,7 @@ dir = 4 }, /obj/structure/extinguisher_cabinet/directional/north, -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron/white/corner{ dir = 1 }, @@ -1317,13 +1317,13 @@ /area/station/commons/storage/primary) "pZ" = ( /obj/machinery/light/directional/north, -/obj/machinery/camera/directional/north, /obj/effect/turf_decal/plaque{ icon_state = "L11" }, /obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4, /obj/structure/cable, /obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/commons/storage/primary) "qb" = ( @@ -2034,10 +2034,10 @@ /turf/open/floor/iron, /area/station/construction) "Hc" = ( -/obj/machinery/camera/directional/north, /obj/effect/turf_decal/tile/blue/half/contrasted{ dir = 1 }, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/hallway/primary/central) "Hg" = ( @@ -2057,7 +2057,7 @@ /turf/open/floor/iron, /area/station/science) "Ih" = ( -/obj/machinery/camera/directional/north, +/obj/machinery/camera/autoname/directional/north, /turf/open/floor/iron, /area/station/hallway/secondary/exit/departure_lounge) "Ir" = ( From 67bbc096a8af5b8456c3d3881bf585aae1514701 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:33:45 +0200 Subject: [PATCH 003/101] Introduces particle weather, converts rain and ash storms to it (#96297) Co-authored-by: Lucy --- code/__DEFINES/layers.dm | 50 +++--- code/__DEFINES/weather.dm | 3 + .../plane_masters/plane_master_subtypes.dm | 16 ++ code/_onclick/hud/rendering/render_plate.dm | 39 +++++ code/controllers/subsystem/weather.dm | 64 ++++++-- .../datums/components/ai_listen_to_weather.dm | 2 +- code/datums/components/hide_weather_planes.dm | 9 +- code/datums/components/object_possession.dm | 8 +- code/datums/components/storm_hating.dm | 2 +- code/datums/weather/particle_weather.dm | 145 ++++++++++++++++++ code/datums/weather/weather.dm | 20 ++- .../datums/weather/weather_types/ash_storm.dm | 72 +++++++-- .../weather/weather_types/rain_storm.dm | 54 +++++-- code/game/machinery/mining_weather_monitor.dm | 4 +- .../effects/anomalies/anomalies_weather.dm | 4 +- code/game/objects/effects/particles/fire.dm | 2 +- code/game/objects/effects/particles/smoke.dm | 2 +- code/modules/admin/verbs/adminweather.dm | 2 +- .../voidwalker/voidwalker_window.dm | 2 +- code/modules/events/wizard/magical_rain.dm | 2 +- code/modules/mob/login.dm | 4 +- icons/effects/weather_overlay.dmi | Bin 0 -> 300 bytes tgstation.dme | 1 + 23 files changed, 418 insertions(+), 89 deletions(-) create mode 100644 code/datums/weather/particle_weather.dm create mode 100644 icons/effects/weather_overlay.dmi diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 4d368ccc6705..6bc5b96e0e00 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -13,6 +13,9 @@ #define PLANE_SPACE -21 #define PLANE_SPACE_PARALLAX -20 +#define WEATHER_MASK_PLANE -13 +#define WEATHER_MASK_RENDER_TARGET "*WEATHER_MASK_RENDER_TARGET" + #define DISPLACEMENT_PLANE -12 #define DISPLACEMENT_RENDER_TARGET "*DISPLACEMENT_RENDER_TARGET" @@ -65,76 +68,79 @@ #define RENDER_PLANE_SPECULAR_MASK 17 #define SPECULAR_MASK_RENDER_TARGET "*RENDER_PLANE_SPECULAR_MASK" +#define RENDER_PLANE_PARTICLE_WEATHER 18 +#define RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER 19 + //-------------------- Lighting --------------------- /// Main game plane to which everything renders, which then is multiplied by light /// Should not be lit directly as it is sourced for emissive bloom -#define RENDER_PLANE_UNLIT_GAME 18 +#define RENDER_PLANE_UNLIT_GAME 20 -#define RENDER_PLANE_O_LIGHTING 19 +#define RENDER_PLANE_O_LIGHTING 21 -#define RENDER_PLANE_LIGHTING 20 +#define RENDER_PLANE_LIGHTING 22 /// Masks the lighting plane with turfs, so we never light up the void /// Failing that, masks emissives and the overlay lighting plane -#define RENDER_PLANE_LIGHT_MASK 21 +#define RENDER_PLANE_LIGHT_MASK 23 #define LIGHT_MASK_RENDER_TARGET "*RENDER_PLANE_LIGHT_MASK" /// We cannot render speculars to ABOVE_LIGHTING, as then they give it alpha and end up masking things in darkness /// So we need to render it directly to RENDER_PLANE_GAME above RENDER_PLANE_LIGHTING -#define RENDER_PLANE_SPECULAR 22 +#define RENDER_PLANE_SPECULAR 24 /// Things that should render ignoring lighting -#define ABOVE_LIGHTING_PLANE 23 +#define ABOVE_LIGHTING_PLANE 25 -#define WEATHER_GLOW_PLANE 24 +#define WEATHER_GLOW_PLANE 26 ///---------------- MISC ----------------------- ///Pipecrawling images -#define PIPECRAWL_IMAGES_PLANE 25 +#define PIPECRAWL_IMAGES_PLANE 30 ///AI Camera Static -#define CAMERA_STATIC_PLANE 26 +#define CAMERA_STATIC_PLANE 31 ///Anything that wants to be part of the game plane, but also wants to draw above literally everything else -#define HIGH_GAME_PLANE 27 +#define HIGH_GAME_PLANE 32 -#define FULLSCREEN_PLANE 28 +#define FULLSCREEN_PLANE 33 ///--------------- FULLSCREEN RUNECHAT BUBBLES ------------ ///Popup Chat Messages -#define RUNECHAT_PLANE 30 +#define RUNECHAT_PLANE 34 /// Plane for balloon text (text that fades up) -#define BALLOON_CHAT_PLANE 31 +#define BALLOON_CHAT_PLANE 35 //-------------------- HUD --------------------- //HUD layer defines -#define HUD_PLANE 35 -#define ABOVE_HUD_PLANE 36 +#define HUD_PLANE 40 +#define ABOVE_HUD_PLANE 41 ///Plane of the "splash" icon used that shows on the lobby screen -#define SPLASHSCREEN_PLANE 37 +#define SPLASHSCREEN_PLANE 42 // The largest plane here must still be less than RENDER_PLANE_GAME //-------------------- Rendering --------------------- -#define RENDER_PLANE_GAME 40 +#define RENDER_PLANE_GAME 50 /// If fov is enabled we'll draw game to this and do shit to it -#define RENDER_PLANE_GAME_MASKED 41 +#define RENDER_PLANE_GAME_MASKED 51 /// The bit of the game plane that is let alone is sent here -#define RENDER_PLANE_GAME_UNMASKED 42 +#define RENDER_PLANE_GAME_UNMASKED 52 -#define RENDER_PLANE_NON_GAME 45 +#define RENDER_PLANE_NON_GAME 55 // Only VERY special planes should be here, as they are above not just the game, but the UI planes as well. /// Plane related to the menu when pressing Escape. /// Needed so that we can apply a blur effect to EVERYTHING, and guarantee we are above all UI. -#define ESCAPE_MENU_PLANE 46 +#define ESCAPE_MENU_PLANE 56 -#define RENDER_PLANE_MASTER 50 +#define RENDER_PLANE_MASTER 57 // Lummox I swear to god I will find you // NOTE! You can only ever have planes greater then -10000, if you add too many with large offsets you will brick multiz diff --git a/code/__DEFINES/weather.dm b/code/__DEFINES/weather.dm index 0bd041a497c7..ae2f50401e0a 100644 --- a/code/__DEFINES/weather.dm +++ b/code/__DEFINES/weather.dm @@ -67,3 +67,6 @@ GLOBAL_LIST_INIT(thunder_chance_options, list( #define WEATHER_FORCED_TELEGRAPH "Telegraph Duration" #define WEATHER_FORCED_END "End Duration" #define WEATHER_FORCED_DURATION "Weather Duration" + +/// Maximum possible weather severity value +#define MAXIMUM_WEATHER_SEVERITY 100 diff --git a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm index 6cfafcd6797a..094371cc9e68 100644 --- a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm +++ b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm @@ -328,6 +328,22 @@ return home.AddComponent(/datum/component/hide_weather_planes, src) +/atom/movable/screen/plane_master/weather_mask + name = "Weather Mask" + documentation = "Used to mask particle weather effects to cut out areas unaffected by weather." + plane = WEATHER_MASK_PLANE + appearance_flags = PLANE_MASTER | NO_CLIENT_COLOR + render_target = WEATHER_MASK_RENDER_TARGET + render_relay_planes = list() + start_hidden = TRUE + critical = PLANE_CRITICAL_DISPLAY + +/atom/movable/screen/plane_master/weather_mask/set_home(datum/plane_master_group/home) + . = ..() + if(!.) + return + home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) + /atom/movable/screen/plane_master/massive_obj name = "Massive object" documentation = "Huge objects need to render above everything else on the game plane, otherwise they'd well, get clipped and look not that huge. This does that." diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 9d42784bb2af..11979dde2ace 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -72,6 +72,45 @@ . = ..() add_relay_to(GET_NEW_PLANE(RENDER_PLANE_EMISSIVE_BLOOM, offset), blend_override = BLEND_MULTIPLY) +/atom/movable/screen/plane_master/rendering_plate/particle_weather + name = "Particle Weather" + documentation = "Plane used to render particle weather, masked by WEATHER_MASK_PLANE. \ + Cannot be a single screen object as it needs to be a planemaster in order to be properly masked by the weather mask." + plane = RENDER_PLANE_PARTICLE_WEATHER + start_hidden = TRUE + critical = PLANE_CRITICAL_DISPLAY + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset) + . = ..() + // We can actually do this just fine as we do not render anything onto ourselves but our particles + add_filter("weather_mask", 1, alpha_mask_filter(render_source = OFFSET_RENDER_TARGET(WEATHER_MASK_RENDER_TARGET, offset))) + SSweather.particle_planemasters += src + // And add all ongoing weather to ourselves + for (var/holder_offset, holder_list in SSweather.particle_holders) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + if (holder.plane == plane) + vis_contents += holder + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/Destroy() + SSweather.particle_planemasters -= src + return ..() + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/set_home(datum/plane_master_group/home) + . = ..() + if(!.) + return + home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive + name = "Emissive Particle Weather" + documentation = "Secondary particle weather plane for emissive parts of weather, which is additionally rendered onto the emissive plane after being masked." + plane = RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset) + . = ..() + // Render a copy of ourselves onto the emissive plane encoded into the bloom channel + add_relay_to(GET_NEW_PLANE(EMISSIVE_PLANE, offset), relay_color = GLOB.emissive_color) + /atom/movable/screen/plane_master/rendering_plate/turf_lighting name = "Turf lighting post-processing plate" documentation = "Used by overlay lighting, and possibly over plates, to mask out turf lighting." diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm index 7a89236e08a2..d50013baceed 100644 --- a/code/controllers/subsystem/weather.dm +++ b/code/controllers/subsystem/weather.dm @@ -8,13 +8,26 @@ SUBSYSTEM_DEF(weather) wait = 10 runlevels = RUNLEVEL_GAME var/list/processing = list() + /// Z levels on which weather can occur -> weather that can occur -> probability of said weather occuring var/list/eligible_zlevels = list() - var/list/next_hit_by_zlevel = list() //Used by barometers to know when the next storm is coming + /// Used by barometers to know when the next storm is coming + var/list/next_hit_by_zlevel = list() + /// Alist of all particle holders per Z-stack offset for particle weather to be shown to clients + var/alist/particle_holders = alist() + /// List of all RENDER_PLANE_PARTICLE_WEATHER and RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER planes + var/list/particle_planemasters = list() /datum/controller/subsystem/weather/fire(resumed = FALSE) // process active weather for(var/datum/weather/weather_event as anything in processing) - if(!length(weather_event.subsystem_tasks) || weather_event.stage != MAIN_STAGE) + if(!length(weather_event.subsystem_tasks)) + continue + + if(istype(weather_event, /datum/weather/particle)) + var/datum/weather/particle/particle_event = weather_event + particle_event.process_particles() + + if(weather_event.stage != MAIN_STAGE) continue if(weather_event.subsystem_tasks[weather_event.task_index] == SSWEATHER_MOBS) @@ -69,21 +82,42 @@ SUBSYSTEM_DEF(weather) next_hit_by_zlevel["[z]"] = addtimer(CALLBACK(src, PROC_REF(make_eligible), z, possible_weather), randTime + initial(weather_event.weather_duration_upper), TIMER_UNIQUE|TIMER_STOPPABLE) /datum/controller/subsystem/weather/Initialize() - for(var/V in subtypesof(/datum/weather)) - var/datum/weather/W = V - var/probability = initial(W.probability) - var/target_trait = initial(W.target_trait) + for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) + var/probability = initial(weather.probability) + var/target_trait = initial(weather.target_trait) // any weather with a probability set may occur at random if (probability) for(var/z in SSmapping.levels_by_trait(target_trait)) LAZYINITLIST(eligible_zlevels["[z]"]) - eligible_zlevels["[z]"][W] = probability + eligible_zlevels["[z]"][weather] = probability return SS_INIT_SUCCESS +/datum/controller/subsystem/weather/proc/add_weather_objects(list/new_holders, z_level) + for (var/offset in 1 to length(new_holders)) + var/list/holder_list = new_holders[offset] + if (isnull(particle_holders[offset])) + particle_holders[offset] = list() + particle_holders[offset] += holder_list + + // We add it to vis_contents of planemasters rather than client screen as planemasters already + // manage their own visibility based on owner's z level + for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + if (holder.plane == plane_master.plane) + plane_master.vis_contents |= holder + +/datum/controller/subsystem/weather/proc/remove_weather_objects(list/old_holders) + for (var/offset in 1 to length(old_holders)) + var/list/holder_list = old_holders[offset] + particle_holders[offset] -= holder_list + + for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters) + plane_master.vis_contents -= holder_list + /datum/controller/subsystem/weather/proc/update_z_level(datum/space_level/level) var/z = level.z_value - for(var/datum/weather/weather as anything in subtypesof(/datum/weather)) + for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) var/probability = initial(weather.probability) var/target_trait = initial(weather.target_trait) if(probability && level.traits[target_trait]) @@ -92,10 +126,9 @@ SUBSYSTEM_DEF(weather) /datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type, z_levels, list/weather_data) if (istext(weather_datum_type)) - for (var/V in subtypesof(/datum/weather)) - var/datum/weather/W = V - if (initial(W.name) == weather_datum_type) - weather_datum_type = V + for (var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) + if (initial(weather.name) == weather_datum_type) + weather_datum_type = weather break if (!ispath(weather_datum_type, /datum/weather)) CRASH("run_weather called with invalid weather_datum_type: [weather_datum_type || "null"]") @@ -107,10 +140,9 @@ SUBSYSTEM_DEF(weather) else if (!islist(z_levels)) CRASH("run_weather called with invalid z_levels: [z_levels || "null"]") - - var/datum/weather/W = new weather_datum_type(z_levels, weather_data) - W.telegraph(weather_data) - return W + var/datum/weather/weather = new weather_datum_type(z_levels, weather_data) + weather.telegraph(weather_data) + return weather /datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather) eligible_zlevels[z] = possible_weather diff --git a/code/datums/components/ai_listen_to_weather.dm b/code/datums/components/ai_listen_to_weather.dm index a7bb95ee8c13..29bcda3839b5 100644 --- a/code/datums/components/ai_listen_to_weather.dm +++ b/code/datums/components/ai_listen_to_weather.dm @@ -7,7 +7,7 @@ ///what blackboard key are we setting var/weather_key -/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/ash_storm, weather_key = BB_STORM_APPROACHING) +/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/particle/ash_storm, weather_key = BB_STORM_APPROACHING) if(!isliving(parent)) return COMPONENT_INCOMPATIBLE src.weather_type = weather_type diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm index 5b7addb199e7..7a7cb3655b8f 100644 --- a/code/datums/components/hide_weather_planes.dm +++ b/code/datums/components/hide_weather_planes.dm @@ -8,17 +8,20 @@ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS var/list/datum/weather/active_weather = list() var/list/atom/movable/screen/plane_master/plane_masters = list() + /// Do we care about all weather or only particle weather? + var/particle_only = FALSE -/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about) +/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about, particle_only = FALSE) if(!istype(parent, /datum/plane_master_group)) return COMPONENT_INCOMPATIBLE + src.particle_only = particle_only var/datum/plane_master_group/home = parent plane_masters += care_about RegisterSignal(care_about, COMSIG_QDELETING, PROC_REF(plane_master_deleted)) var/list/starting_signals = list() var/list/ending_signals = list() - for(var/datum/weather/weather_type as anything in typesof(/datum/weather)) + for(var/datum/weather/weather_type as anything in valid_subtypesof(particle_only ? /datum/weather/particle : /datum/weather)) starting_signals += COMSIG_WEATHER_TELEGRAPH(weather_type) ending_signals += COMSIG_WEATHER_END(weather_type) @@ -103,6 +106,8 @@ var/list/connected_levels = SSmapping.get_connected_levels(new_z) for(var/datum/weather/active as anything in SSweather.processing) + if(particle_only && !istype(active, /datum/weather/particle)) + continue if(length(connected_levels & active.impacted_z_levels)) active_weather += WEAKREF(active) diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm index 256f29526707..f13acee56c28 100644 --- a/code/datums/components/object_possession.dm +++ b/code/datums/components/object_possession.dm @@ -69,9 +69,9 @@ user.name = target.name user.reset_perspective(target) - target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + target.AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) target.AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds) - target.AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) + target.AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) target.AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds) RegisterSignal(target, COMSIG_QDELETING, PROC_REF(end_possession)) @@ -84,8 +84,8 @@ var/mob/poltergeist = parent - possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) - possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) + possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds) possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds) UnregisterSignal(possessed, COMSIG_QDELETING) diff --git a/code/datums/components/storm_hating.dm b/code/datums/components/storm_hating.dm index 62a59a0ad71a..0b4edbe84e78 100644 --- a/code/datums/components/storm_hating.dm +++ b/code/datums/components/storm_hating.dm @@ -4,7 +4,7 @@ /datum/component/storm_hating /// Types of weather which trigger the effect var/static/list/stormy_weather = list( - /datum/weather/ash_storm, + /datum/weather/particle/ash_storm, /datum/weather/snow_storm, /datum/weather/void_storm, ) diff --git a/code/datums/weather/particle_weather.dm b/code/datums/weather/particle_weather.dm new file mode 100644 index 000000000000..b8e1a5ac3fe2 --- /dev/null +++ b/code/datums/weather/particle_weather.dm @@ -0,0 +1,145 @@ +/// Subtype which uses particle systems instead of overlays to display its effects +/datum/weather/particle + abstract_type = /datum/weather/particle + /// Particles used to display the weather visuals + var/particles/weather/particle_type = null + /// Secondary (layered ontop) particle type which is rendered as emissive in case you want embers or whatever + var/particles/weather/emissive_type = null + /// Minimum possible severity for the weather, (0~100] + var/min_severity = 1 + /// Maximum possible severity for the weather, (0~100] + var/max_severity = MAXIMUM_WEATHER_SEVERITY + /// Maximum variation in severity each weather frame + var/severity_variation = 5 + /// Optimal point towards which severity will try to gravitate by influencing random values + var/optimal_severity = 70 + /// How often can we change our severity? + /// Don't set this too low or it'll look jank + var/severity_cooldown = 5 SECONDS + + /// Current weather severity + var/severity = 0 + /// Last tick during which we've changed our visual severity + var/last_severity_tick = 0 + /// List of weather object lists (as we can have multiple if emissives are involved) by plane offset + var/list/weather_objects = list() + /// Direction of our wind + var/wind_sign = 0 + +/datum/weather/particle/New(z_levels, list/weather_data) + . = ..() + if (isnull(particle_type) && isnull(emissive_type)) + CRASH("[src] ([type]) attempted to initialize without normal or emissive particle types!") + + for (var/offset in 0 to SSmapping.max_plane_offset) + var/list/object_list = list() + if (particle_type) + var/obj/effect/abstract/weather_holder/holder = new() + SET_PLANE_W_SCALAR(holder, RENDER_PLANE_PARTICLE_WEATHER, offset) + holder.particles = new particle_type() + object_list += holder + + if (emissive_type) + var/obj/effect/abstract/weather_holder/holder = new() + holder.particles = new emissive_type() + SET_PLANE_W_SCALAR(holder, RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER, offset) + object_list += holder + + weather_objects += list(object_list) + + SSweather.add_weather_objects(weather_objects) + +/datum/weather/particle/Destroy() + SSweather.remove_weather_objects(weather_objects) + for(var/list/object_list as anything in weather_objects) + QDEL_LIST(object_list) + weather_objects = null + return ..() + +/datum/weather/particle/telegraph(list/weather_data) + . = ..() + if (!.) + return + animate_severity(0) + +/datum/weather/particle/end() + . = ..() + if (!.) + return + animate_severity(0) + +/// Adjust our severity by a random number based on our stage +/datum/weather/particle/proc/process_particles() + if (last_severity_tick + severity_cooldown > world.time) + return + + last_severity_tick = world.time + var/new_severity = severity + switch (stage) + if (STARTUP_STAGE) + // Aims at minimum severity, and can only go up + new_severity += rand() * severity_variation * (1 - severity / min_severity) + if (MAIN_STAGE) + // Tries to stay close to optimal severity + new_severity += LERP(-severity_variation * clamp(INVERSE_LERP(min_severity, optimal_severity, severity), 0, 1), severity_variation * clamp(INVERSE_LERP(max_severity, optimal_severity, severity), 0, 1), rand()) + new_severity = clamp(new_severity, min(new_severity, min_severity), max_severity) + if (WIND_DOWN_STAGE) + // Slowly goes down to zero + new_severity += rand() * -severity_variation * (severity / min_severity) + + animate_severity(new_severity) + +/datum/weather/particle/proc/animate_severity(new_severity) + if (!wind_sign) + wind_sign = pick(-1, 1) + + severity = new_severity + for (var/list/holder_list as anything in weather_objects) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + var/particles/weather/particle_effect = holder.particles + particle_effect.animate_severity(severity / MAXIMUM_WEATHER_SEVERITY, wind_sign) + +/datum/weather/particle/generate_overlay_cache() + . = ..() + for (var/offset in 0 to SSmapping.max_plane_offset) + . += mutable_appearance('icons/effects/weather_overlay.dmi', "weather_overlay", overlay_layer, null, WEATHER_MASK_PLANE, offset_const = offset) + +/obj/effect/abstract/weather_holder + icon = null + appearance_flags = TILE_BOUND | PIXEL_SCALE + blocks_emissive = EMISSIVE_BLOCK_NONE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/particles/weather + spawning = 0 + // Wider than our view due to weird BYOND jank + width = 800 + height = 800 + count = 8000 + + lifespan = 30 SECONDS + fade = 1 SECONDS + fadein = 0.5 SECONDS + + /// Increase in speed per tick + var/wind_strength = 0 + /// Minimum number of spawned particles per tick for easing + var/min_spawn = 0 + /// Maximum amount of spawned particles at full strength + var/max_spawn = 0 + +/// Changes the strength of the weather visual effect, severity should be between 0 and 1 +/particles/weather/proc/animate_severity(severity, wind_sign) + // Stop spawning if severity is zero or negative + if (severity <= 0) + spawning = 0 + return + + var/wind = wind_strength * severity * wind_sign + spawning = LERP(min_spawn, max_spawn, severity) + // Might already be set on init, in which case we preserve y and z components + if (islist(gravity) && length(gravity)) + gravity[1] = wind + else + gravity = list(wind) + diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm index 50fa90c79e29..de0d5cfc0577 100644 --- a/code/datums/weather/weather.dm +++ b/code/datums/weather/weather.dm @@ -13,9 +13,10 @@ */ /datum/weather - /// name of weather + abstract_type = /datum/weather + /// Name of weather var/name = "space wind" - /// description of weather + /// Description of weather var/desc = "Heavy gusts of wind blanket the area, periodically knocking down anyone caught in the open." /// The message displayed in chat to foreshadow the weather's beginning var/telegraph_message = span_warning("The wind begins to pick up.") @@ -42,6 +43,8 @@ var/weather_overlay /// Color to apply to the area while weather is occuring var/weather_color = null + /// Alpha of the weather overlay + var/weather_alpha = 255 /// Displayed once the weather is over var/end_message = span_danger("The wind relents its assault.") @@ -326,7 +329,7 @@ */ /datum/weather/proc/start() if(stage >= MAIN_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_START(type), src) stage = MAIN_STAGE update_areas() @@ -335,6 +338,7 @@ addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration, TIMER_UNIQUE) for(var/area/impacted_area as anything in impacted_areas) SEND_SIGNAL(impacted_area, COMSIG_WEATHER_BEGAN_IN_AREA(type), src) + return TRUE /** * Weather enters the winding down phase, stops effects @@ -345,12 +349,13 @@ */ /datum/weather/proc/wind_down() if(stage >= WIND_DOWN_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_WINDDOWN(type), src) stage = WIND_DOWN_STAGE update_areas() send_alert(end_message, end_sound, end_sound_vol) addtimer(CALLBACK(src, PROC_REF(end)), end_duration, TIMER_UNIQUE) + return TRUE /** * Fully ends the weather @@ -361,7 +366,7 @@ */ /datum/weather/proc/end() if(stage == END_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_END(type), src) UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED) stage = END_STAGE @@ -373,6 +378,7 @@ if(target_trait) for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list) UnregisterSignal(affected, COMSIG_MOB_LOGIN) + return TRUE // handles sending all alerts /datum/weather/proc/send_alert(alert_msg, alert_sfx, alert_sfx_vol = 100) @@ -576,11 +582,11 @@ // I prefer it to creating 2 extra plane masters however, so it's a cost I'm willing to pay // LU if(use_glow) - var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100, offset_const = offset) + var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100 / 255 * weather_alpha, offset_const = offset) glow_overlay.color = weather_color gen_overlay_cache += glow_overlay - var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, offset_const = offset) + var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset) new_weather_overlay.color = weather_color gen_overlay_cache += new_weather_overlay diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm index 367977a90bca..ae12115e367f 100644 --- a/code/datums/weather/weather_types/ash_storm.dm +++ b/code/datums/weather/weather_types/ash_storm.dm @@ -1,8 +1,15 @@ -//Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside. -/datum/weather/ash_storm +/// Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside. +/datum/weather/particle/ash_storm name = "ash storm" desc = "An intense atmospheric storm lifts ash off of the planet's surface and billows it down across the area, dealing intense fire damage to the unprotected." + particle_type = /particles/weather/ash_storm + emissive_type = /particles/weather/ash_storm/embers + min_severity = 60 + optimal_severity = 80 + weather_alpha = 100 + wind_sign = -1 // Always blows left to sync with the animated overlays + telegraph_message = span_boldwarning("An eerie moan rises on the wind. Sheets of burning ash blacken the horizon. Seek shelter.") telegraph_duration = 30 SECONDS telegraph_overlay = "light_ash" @@ -28,10 +35,51 @@ var/list/weak_sounds = list() var/list/strong_sounds = list() -/datum/weather/ash_storm/get_playlist_ref() +/particles/weather/ash_storm + icon = 'icons/effects/particles/smoke.dmi' + icon_state = list("chill_1" = 4, "chill_2" = 3, "chill_3" = 2) + position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0)) + grow = list(-0.01, -0.01) + gravity = list(0, -7, 0.5) + drift = generator(GEN_BOX, list(-1, -1, 0), list(1, 0, 0)) + friction = 0.3 + min_spawn = 20 + max_spawn = 400 + wind_strength = 16 + spin = generator(GEN_NUM, -5, 5, NORMAL_RAND) + /// Y-coordinate of our gravity + var/gravity_power = -12 + +/particles/weather/ash_storm/animate_severity(severity) + . = ..() + if (length(gravity) > 1) + gravity[2] = gravity_power * severity + else + gravity = list(gravity[1], gravity_power * severity, 0.5) + +/particles/weather/ash_storm/embers + icon = 'icons/effects/particles/generic.dmi' + icon_state = list("dot" = 4, "cross" = 2, "curl" = 1) + color = LIGHT_COLOR_FIRE + min_spawn = 10 + max_spawn = 80 + +/particles/weather/ash_storm/emberfall + min_spawn = 20 + max_spawn = 200 + wind_strength = 3 + gravity_power = -2 + +/particles/weather/ash_storm/embers/emberfall + min_spawn = 20 + max_spawn = 200 + wind_strength = 3 + gravity_power = -2 + +/datum/weather/particle/ash_storm/get_playlist_ref() return GLOB.ash_storm_sounds -/datum/weather/ash_storm/telegraph() +/datum/weather/particle/ash_storm/telegraph() for(var/area/impacted_area as anything in impacted_areas) if(impacted_area.outdoors) weak_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm @@ -45,17 +93,17 @@ GLOB.ash_storm_sounds += weak_sounds return ..() -/datum/weather/ash_storm/start() +/datum/weather/particle/ash_storm/start() GLOB.ash_storm_sounds -= weak_sounds GLOB.ash_storm_sounds += strong_sounds return ..() -/datum/weather/ash_storm/wind_down() +/datum/weather/particle/ash_storm/wind_down() GLOB.ash_storm_sounds -= strong_sounds GLOB.ash_storm_sounds += weak_sounds return ..() -/datum/weather/ash_storm/recursive_weather_protection_check(atom/to_check) +/datum/weather/particle/ash_storm/recursive_weather_protection_check(atom/to_check) . = ..() if(. || !ishuman(to_check)) return @@ -63,12 +111,13 @@ if(human_to_check.get_thermal_protection() >= FIRE_IMMUNITY_MAX_TEMP_PROTECT) return TRUE -/datum/weather/ash_storm/weather_act_mob(mob/living/victim) +/datum/weather/particle/ash_storm/weather_act_mob(mob/living/victim) victim.adjust_fire_loss(4, required_bodytype = BODYTYPE_ORGANIC) return ..() -/datum/weather/ash_storm/end() +/datum/weather/particle/ash_storm/end() GLOB.ash_storm_sounds -= weak_sounds + GLOB.ash_storm_sounds -= strong_sounds for(var/turf/open/misc/asteroid/basalt/basalt as anything in GLOB.dug_up_basalt) if(!(basalt.loc in impacted_areas) || !(basalt.z in impacted_z_levels)) continue @@ -76,7 +125,7 @@ return ..() //Emberfalls are the result of an ash storm passing by close to the playable area of lavaland. They have a 10% chance to trigger in place of an ash storm. -/datum/weather/ash_storm/emberfall +/datum/weather/particle/ash_storm/emberfall name = "emberfall" desc = "A passing ash storm blankets the area in harmless embers." @@ -89,3 +138,6 @@ weather_flags = parent_type::weather_flags & ~(WEATHER_MOBS|WEATHER_THUNDER) probability = 10 + + particle_type = /particles/weather/ash_storm/emberfall + emissive_type = /particles/weather/ash_storm/embers/emberfall diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm index 4516112824c4..2b2998fff727 100644 --- a/code/datums/weather/weather_types/rain_storm.dm +++ b/code/datums/weather/weather_types/rain_storm.dm @@ -1,16 +1,16 @@ -/datum/weather/rain_storm +/datum/weather/particle/rain_storm name = "rain" desc = "Heavy thunderstorms rain down below, drenching anyone caught in it." + particle_type = /particles/weather/rain_storm + min_severity = 30 + telegraph_message = span_danger("Thunder rumbles far above. You hear droplets drumming against the canopy.") - telegraph_overlay = "rain_low" telegraph_duration = 30 SECONDS weather_message = span_userdanger("Rain pours down around you!") - weather_overlay = "rain_high" end_message = span_bolddanger("The downpour gradually slows to a light shower.") - end_overlay = "rain_low" end_duration = 30 SECONDS weather_duration_lower = 3 MINUTES @@ -27,10 +27,20 @@ weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_THUNDER | WEATHER_BAROMETER) whitelist_weather_reagents = list(/datum/reagent/water) -/datum/weather/rain_storm/get_playlist_ref() +/datum/weather/particle/rain_storm/New(z_levels, list/weather_data) + . = ..() + if (isnull(weather_reagent) || istype(weather_reagent, /datum/reagent/water) || !weather_color) + return + + // Non-water rain gets colored into their reagent's color + for (var/list/holder_list as anything in weather_objects) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + holder.particles.color = weather_color + +/datum/weather/particle/rain_storm/get_playlist_ref() return GLOB.rain_storm_sounds -/datum/weather/rain_storm/telegraph() +/datum/weather/particle/rain_storm/telegraph() GLOB.rain_storm_sounds.Cut() for(var/area/impacted_area as anything in impacted_areas) GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/start @@ -43,39 +53,53 @@ return ..() -/datum/weather/rain_storm/start() +/datum/weather/particle/rain_storm/start() GLOB.rain_storm_sounds.Cut() for(var/area/impacted_area as anything in impacted_areas) GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/middle return ..() -/datum/weather/rain_storm/wind_down() +/datum/weather/particle/rain_storm/wind_down() GLOB.rain_storm_sounds.Cut() for(var/area/impacted_area as anything in impacted_areas) GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/end return ..() -/datum/weather/rain_storm/end() +/datum/weather/particle/rain_storm/end() GLOB.rain_storm_sounds.Cut() return ..() -/datum/weather/rain_storm/blood +/particles/weather/rain_storm + icon = 'icons/effects/particles/generic.dmi' + icon_state = "drop" + color = "#ccffff" + position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0)) + grow = list(-0.01, -0.01) + gravity = list(0, -10, 0.5) + drift = generator(GEN_CIRCLE, 0, 1) + friction = 0.3 + min_spawn = 50 + max_spawn = 300 + wind_strength = 5 + spin = 0 + +/datum/weather/particle/rain_storm/blood whitelist_weather_reagents = list(/datum/reagent/blood) probability = 0 // admeme event // Fun fact - if you increase the weather_temperature higher than LIQUID_PLASMA_BP // the plasma rain will vaporize into a gas on whichever turf it lands on -/datum/weather/rain_storm/plasma +/datum/weather/particle/rain_storm/plasma whitelist_weather_reagents = list(/datum/reagent/toxin/plasma) probability = 0 // maybe for icebox maps one day? -/datum/weather/rain_storm/deep_fried +/datum/weather/particle/rain_storm/deep_fried weather_temperature = 455 // just hot enough to apply the fried effect whitelist_weather_reagents = list(/datum/reagent/consumable/nutriment/fat/oil) weather_flags = (WEATHER_TURFS | WEATHER_INDOORS) probability = 0 // admeme event -/datum/weather/rain_storm/acid +/datum/weather/particle/rain_storm/acid desc = "The planet's thunderstorms are by nature acidic, and will incinerate anyone standing beneath them without protection." telegraph_duration = 40 SECONDS @@ -97,7 +121,7 @@ ) probability = 0 -/datum/weather/rain_storm/wizard +/datum/weather/particle/rain_storm/wizard name = "magical rain" desc = "A magical thunderstorm rains down below, drenching anyone caught in it with mysterious rain." @@ -116,7 +140,7 @@ probability = 0 // shouldn't spawn normally weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_INDOORS | WEATHER_BAROMETER) -/datum/weather/rain_storm/wizard/New(z_levels, list/weather_data) +/datum/weather/particle/rain_storm/wizard/New(z_levels, list/weather_data) if(length(GLOB.wizard_rain_reagents)) // the wizard event has already been run once and setup the whitelist whitelist_weather_reagents = GLOB.wizard_rain_reagents return ..() diff --git a/code/game/machinery/mining_weather_monitor.dm b/code/game/machinery/mining_weather_monitor.dm index c057b44f56cd..402a40dd8f16 100644 --- a/code/game/machinery/mining_weather_monitor.dm +++ b/code/game/machinery/mining_weather_monitor.dm @@ -346,8 +346,8 @@ GLOBAL_LIST_EMPTY(weather_towers) /// Return a list of weather typepaths that this tower can summon when given a weather core. /obj/machinery/power/weather_tower/proc/get_summonable_weather_types() . = list( - /datum/weather/ash_storm, - /datum/weather/rain_storm, + /datum/weather/particle/ash_storm, + /datum/weather/particle/rain_storm, /datum/weather/sand_storm, /datum/weather/snow_storm, ) diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm index ea407acfa3af..17e6f415af58 100644 --- a/code/game/objects/effects/anomalies/anomalies_weather.dm +++ b/code/game/objects/effects/anomalies/anomalies_weather.dm @@ -55,7 +55,7 @@ /obj/effect/anomaly/weather/proc/select_weather() return pick( - /datum/weather/rain_storm, + /datum/weather/particle/rain_storm, /datum/weather/snow_storm, /datum/weather/sand_storm, ) @@ -97,4 +97,4 @@ // maybe we can put acid rain in this later? // though it'd feel unfair if it showed up and immediately dumped acid on people. // we would need an even longer telegraphing time for that - weather_type = /datum/weather/rain_storm + weather_type = /datum/weather/particle/rain_storm diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm index 481849c00eb1..c35b1d645489 100644 --- a/code/game/objects/effects/particles/fire.dm +++ b/code/game/objects/effects/particles/fire.dm @@ -31,7 +31,7 @@ gradient = list("#FBAF4D", "#FCE6B6", "#FD481C") position = generator(GEN_BOX, list(-12,-16,0), list(12,16,0), NORMAL_RAND) drift = generator(GEN_VECTOR, list(-0.1,0), list(0.1,0.025), UNIFORM_RAND) - spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND) + spin = generator(GEN_NUM, -15, 15, NORMAL_RAND) scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND) /particles/embers/minor diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm index c5626ef646c6..cd7766ccff7e 100644 --- a/code/game/objects/effects/particles/smoke.dm +++ b/code/game/objects/effects/particles/smoke.dm @@ -128,7 +128,7 @@ position = generator(GEN_BOX, list(-17,-15,0), list(24,15,0), NORMAL_RAND) scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND) drift = generator(GEN_SPHERE, list(-0.01,0), list(0.01,0.01), UNIFORM_RAND) - spin = generator(GEN_NUM, list(-2,2), NORMAL_RAND) + spin = generator(GEN_NUM, -2, 2, NORMAL_RAND) gravity = list(0.05, 0.28) friction = 0.3 grow = 0.037 diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm index 8721a1aabb25..290e52e5ee7c 100644 --- a/code/modules/admin/verbs/adminweather.dm +++ b/code/modules/admin/verbs/adminweather.dm @@ -2,7 +2,7 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather var/list/weather_choices = list() if(!length(weather_choices)) - for(var/datum/weather/weather_type as anything in subtypesof(/datum/weather)) + for(var/datum/weather/weather_type as anything in valid_subtypesof(/datum/weather)) weather_choices[initial(weather_type.type)] = weather_type var/datum/weather/weather_choice = tgui_input_list(user, "Choose a weather to run", "Weather", weather_choices) diff --git a/code/modules/antagonists/voidwalker/voidwalker_window.dm b/code/modules/antagonists/voidwalker/voidwalker_window.dm index b6dad0722dad..c62dbf68c528 100644 --- a/code/modules/antagonists/voidwalker/voidwalker_window.dm +++ b/code/modules/antagonists/voidwalker/voidwalker_window.dm @@ -26,7 +26,7 @@ velocity = list(0, 0.2, 0) position = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND) drift = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND) - spin = generator(GEN_NUM, list(-5,5), NORMAL_RAND) + spin = generator(GEN_NUM, -5, 5, NORMAL_RAND) friction = 0.5 gravity = list(0, 0) grow = 0.05 diff --git a/code/modules/events/wizard/magical_rain.dm b/code/modules/events/wizard/magical_rain.dm index 203a960d36e6..0885c9830648 100644 --- a/code/modules/events/wizard/magical_rain.dm +++ b/code/modules/events/wizard/magical_rain.dm @@ -21,7 +21,7 @@ if(!started) started = TRUE - SSweather.run_weather(/datum/weather/rain_storm/wizard) + SSweather.run_weather(/datum/weather/particle/rain_storm/wizard) /datum/round_event/wizard/magical_rain/end() for(var/mob/living/wizard in GLOB.alive_mob_list) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 541734ff8e4a..6c32c7a0e790 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -131,8 +131,8 @@ SEND_SIGNAL(client, COMSIG_CLIENT_MOB_LOGIN, src) client.init_verbs() - AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) - AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) + AddElement(/datum/element/weather_listener, /datum/weather/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + AddElement(/datum/element/weather_listener, /datum/weather/particle/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds) AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds) AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds) diff --git a/icons/effects/weather_overlay.dmi b/icons/effects/weather_overlay.dmi new file mode 100644 index 0000000000000000000000000000000000000000..7dc15fbe40c725449f65a46e561600518e625ace GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^3P5bY!3HD?)>PyJsfwzQh!U67;^d;tf|AVqJfH}p zYOgg{gMk3+>o=NWZ;!?&CM$PEybx7+ki}G|5Xp1zp6hN&x&F?LFMlN-DwBDj_WW&W zQg7;{Ca$tX<)9}Ecy6BE;1SuFepW?-^-|7>v(shD|GF#?d%Z_Ta>dn`&2@bnXW!EQ zz%(sOF8N7m-d>;^@;zM~Ln>~)y>XGD!9arHVcG;o4#g=z+#=uvA`S_(8MM@I-qC(n zu;%=9rKEWWg4lIT5|%Pb^IBva@MPALD!AOx&bG(o!D2>x-V-JZE;M*CJ2fycGO=(7 nC^$eE5^@dlLN%A)sw`lP)7e)1HF*;#JQzG({an^LB{Ts5P2g>a literal 0 HcmV?d00001 diff --git a/tgstation.dme b/tgstation.dme index f310217da6a2..fd48e58b2668 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2145,6 +2145,7 @@ #include "code\datums\votes\custom_vote.dm" #include "code\datums\votes\map_vote.dm" #include "code\datums\votes\restart_vote.dm" +#include "code\datums\weather\particle_weather.dm" #include "code\datums\weather\weather.dm" #include "code\datums\weather\weather_types\ash_storm.dm" #include "code\datums\weather\weather_types\floor_is_lava.dm" From ce200f6b3e4df8e870e6e512eee4da07ffaecc35 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:34:02 +0000 Subject: [PATCH 004/101] Automatic changelog for PR #96297 [ci skip] --- html/changelogs/AutoChangeLog-pr-96297.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96297.yml diff --git a/html/changelogs/AutoChangeLog-pr-96297.yml b/html/changelogs/AutoChangeLog-pr-96297.yml new file mode 100644 index 000000000000..c9d71fd89a36 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96297.yml @@ -0,0 +1,5 @@ +author: "SmArtKar" +delete-after: True +changes: + - bugfix: "Fixed an issue where Stop-All-Active-Weather verb would not remove ash storm sounds" + - refactor: "Rain and ash storms have been refactored to use particles rather than overlays" \ No newline at end of file From aaf5d48925089725b35c62154e26a89e8d551a98 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:00:23 +0000 Subject: [PATCH 005/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96297.yml | 5 ----- html/changelogs/archive/2026-06.yml | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96297.yml diff --git a/html/changelogs/AutoChangeLog-pr-96297.yml b/html/changelogs/AutoChangeLog-pr-96297.yml deleted file mode 100644 index c9d71fd89a36..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96297.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - bugfix: "Fixed an issue where Stop-All-Active-Weather verb would not remove ash storm sounds" - - refactor: "Rain and ash storms have been refactored to use particles rather than overlays" \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index ca7ab1a64ccc..7f835ae87acc 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -175,3 +175,9 @@ by ghosts or players with science goggles. lelandkemble: - bugfix: Pinpointers stop tracking a shunted ai when that ai is no longer shunted +2026-06-07: + SmArtKar: + - bugfix: Fixed an issue where Stop-All-Active-Weather verb would not remove ash + storm sounds + - refactor: Rain and ash storms have been refactored to use particles rather than + overlays From 7218b7aec04aaa4919e15ce12a56c89190fa9201 Mon Sep 17 00:00:00 2001 From: CatoChristopherMrow Date: Sat, 6 Jun 2026 21:25:25 -0400 Subject: [PATCH 006/101] reworks/fixes how we do iframe vs byondstorage (#96167) --- tgui/packages/common/storage.ts | 185 +++++++++++++-------- tgui/packages/tgui-panel/chat/renderer.tsx | 11 +- 2 files changed, 130 insertions(+), 66 deletions(-) diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts index 1955fa8ab8e1..9894fa1a95ec 100644 --- a/tgui/packages/common/storage.ts +++ b/tgui/packages/common/storage.ts @@ -33,6 +33,10 @@ const testHubStorage = testGeneric( () => window.hubStorage && !!window.hubStorage.getItem, ); +const STORAGE_CDN_TIMEOUT = 5000; +const persistedStorageKeys = ['panel-settings', 'chat-state', 'chat-messages']; +const legacyHubMigrationKeys = ['panel-settings']; + class HubStorageBackend implements StorageBackend { public impl: StorageImplementation; @@ -77,20 +81,35 @@ class IFrameIndexedDbBackend implements StorageBackend { iframe.src = Byond.storageCdn; const completePromise: Promise = new Promise((resolve) => { - fetch(Byond.storageCdn, { method: "HEAD" }).then((response) => { - if (response.status !== 200) { - resolve(false); + const listener = (message: MessageEvent) => { + if ( + message.source === iframe.contentWindow && + message.data === 'ready' + ) { + resolveReady(true); } + }; + const resolveReady = (ready: boolean) => { + clearTimeout(timeout); + window.removeEventListener('message', listener); + resolve(ready); + }; + const timeout = setTimeout( + () => resolveReady(false), + STORAGE_CDN_TIMEOUT, + ); + + fetch(Byond.storageCdn, { method: 'HEAD' }) + .then((response) => { + if (response.status !== 200) { + resolveReady(false); + } + }) + .catch(() => { + resolveReady(false); + }); - }).catch(() => { - resolve(false); - }) - - window.addEventListener('message', (message) => { - if (message.data === "ready") { - resolve(true); - } - }) + window.addEventListener('message', listener); }); this.documentElement = document.body.appendChild(iframe); @@ -105,11 +124,23 @@ class IFrameIndexedDbBackend implements StorageBackend { async get(key: string): Promise { const promise = new Promise((resolve) => { - window.addEventListener('message', (message) => { - if (message.data.key && message.data.key === key) { + const listener = (message: MessageEvent) => { + if ( + message.source === this.iframeWindow && + message.data.key && + message.data.key === key + ) { + clearTimeout(timeout); + window.removeEventListener('message', listener); resolve(message.data.value); } - }); + }; + const timeout = setTimeout(() => { + window.removeEventListener('message', listener); + resolve(undefined); + }, STORAGE_CDN_TIMEOUT); + + window.addEventListener('message', listener); }); this.iframeWindow.postMessage({ type: 'get', key: key }, '*'); @@ -143,65 +174,89 @@ class StorageProxy implements StorageBackend { constructor() { this.backendPromise = (async () => { - - // If we have not enabled byondstorage yet, we need to check - // if we can use the IFrame, or if we need to enable byondstorage - if (!testHubStorage()) { - - // If we have an IFrame URL we can use, and we haven't already enabled - // byondstorage, we should use the IFrame backend - if (Byond.storageCdn) { - const iframe = new IFrameIndexedDbBackend(); - - if ((await iframe.ready()) === true) { - if (await iframe.get('byondstorage-migrated')) return iframe; - - Byond.winset(null, 'browser-options', '+byondstorage'); - - await new Promise((resolve) => { - document.addEventListener('byondstorageupdated', async () => { - setTimeout(() => { - const hub = new HubStorageBackend(); - - // Migrate these existing settings from byondstorage to the IFrame - for (const setting of ['panel-settings', 'chat-state', 'chat-messages']) { - hub - .get(setting) - .then((settings) => iframe.set(setting, settings)); + // Prefer the configured iframe storage when available. hubStorage may + // already be enabled by another window/server, but the iframe origin is + // the server-configured storage boundary. + if (Byond.storageCdn) { + const iframe = new IFrameIndexedDbBackend(); + + if ((await iframe.ready()) === true) { + if (await iframe.get('byondstorage-migrated')) return iframe; + + const iframeHasPersistedStorage = ( + await Promise.all( + persistedStorageKeys.map((setting) => iframe.get(setting)), + ) + ).some((settings) => settings !== undefined); + + if (!iframeHasPersistedStorage) { + const hubStorageWasEnabled = testHubStorage(); + if (!hubStorageWasEnabled) { + Byond.winset(null, 'browser-options', '+byondstorage'); + + await new Promise((resolve) => { + document.addEventListener( + 'byondstorageupdated', + () => { + // This event is emitted *before* byondstorage is actually + // created, so we have to wait a little bit before using it. + setTimeout(resolve, 1); + }, + { once: true }, + ); + }); + } + + const hub = new HubStorageBackend(); + + // Migrate safe legacy settings from byondstorage to the IFrame. + // Chat history may contain server-specific HTML/components from + // other codebases that shared the old byondstorage namespace. + await Promise.all( + legacyHubMigrationKeys.map(async (setting) => { + try { + const settings = await hub.get(setting); + if (settings !== undefined) { + await iframe.set(setting, settings); } + } catch { + // Ignore unreadable legacy storage entries. A bad old cache + // key should not keep the client on byondstorage. + } + }), + ); + + if (!hubStorageWasEnabled) { + Byond.winset(null, 'browser-options', '-byondstorage'); + } + } - iframe.set('byondstorage-migrated', true); - Byond.winset(null, 'browser-options', '-byondstorage'); - - resolve(); - }, 1); - }); - }); + await iframe.set('byondstorage-migrated', true); - return iframe; - } + return iframe; + } - iframe.destroy(); - }; + iframe.destroy(); + } - // IFrame hasn't worked out for us, we'll need to enable byondstorage - Byond.winset(null, 'browser-options', '+byondstorage'); + if (testHubStorage()) { + return new HubStorageBackend(); + } - return new Promise((resolve) => { - const listener = () => { - document.removeEventListener('byondstorageupdated', listener); + // IFrame hasn't worked out for us, we'll need to enable byondstorage + Byond.winset(null, 'browser-options', '+byondstorage'); - // This event is emitted *before* byondstorage is actually created - // so we have to wait a little bit before we can use it - setTimeout(() => resolve(new HubStorageBackend()), 1); - }; + return new Promise((resolve) => { + const listener = () => { + document.removeEventListener('byondstorageupdated', listener); - document.addEventListener('byondstorageupdated', listener); - }); - } + // This event is emitted *before* byondstorage is actually created + // so we have to wait a little bit before we can use it + setTimeout(() => resolve(new HubStorageBackend()), 1); + }; - // byondstorage is already enabled, we can use it straight away - return new HubStorageBackend(); + document.addEventListener('byondstorageupdated', listener); + }); })(); } diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx index bee627b36ebb..8c28e1942dc5 100644 --- a/tgui/packages/tgui-panel/chat/renderer.tsx +++ b/tgui/packages/tgui-panel/chat/renderer.tsx @@ -422,10 +422,19 @@ class ChatRenderer { outputProps[canon_name] = working_value; } const oldHtml = { __html: childNode.innerHTML }; + const Element = TGUI_CHAT_COMPONENTS[targetName]; + if (!Element) { + logger.error( + `Error: unknown chat component "${targetName}" in message`, + message, + ); + childNode.removeAttribute('data-component'); + continue; + } + while (childNode.firstChild) { childNode.removeChild(childNode.firstChild); } - const Element = TGUI_CHAT_COMPONENTS[targetName]; const reactRoot = createRoot(childNode); From d2d7b0d9ece752dd2009ede432d31b290521fdb8 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:25:42 +0000 Subject: [PATCH 007/101] Automatic changelog for PR #96167 [ci skip] --- html/changelogs/AutoChangeLog-pr-96167.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96167.yml diff --git a/html/changelogs/AutoChangeLog-pr-96167.yml b/html/changelogs/AutoChangeLog-pr-96167.yml new file mode 100644 index 000000000000..619840e58f7c --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96167.yml @@ -0,0 +1,4 @@ +author: "UvvU" +delete-after: True +changes: + - bugfix: "fixed improper chat caching." \ No newline at end of file From e1c581e13a76c3cbf7771fc802c3c98ccbcd9f1e Mon Sep 17 00:00:00 2001 From: Ghom <42542238+Ghommie@users.noreply.github.com> Date: Sun, 7 Jun 2026 07:10:54 +0200 Subject: [PATCH 008/101] Post-merge code cleanup for botanic trays (#96352) ## About The Pull Request Melbert has tasked me to handle fixing and cleaning up a few things from #96236 after I merged it before they could review it a second time, plus a few things I've noticed on my own (changing the name and desc is best handled by the update_name and update_desc proc respectively, and soil shouldn't process while inside trays). --- .../machines/machine_circuitboards.dm | 17 ++--- code/modules/hydroponics/hydroponics.dm | 73 +++++++++---------- code/modules/hydroponics/soil.dm | 25 +++++-- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index ddbe0a190885..4a65d2432df9 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -1318,24 +1318,23 @@ /obj/item/circuitboard/machine/hydroponics/proc/changeindicators(mob/living/user, obj/item/I) if(build_path == /obj/machinery/hydroponics/constructable/oldstyle) - name = "Hydroponics Tray" + name = "Hydroponics Tray [name_extension]" build_path = /obj/machinery/hydroponics/constructable - balloon_alert(user, "defaulting indicator location.") + balloon_alert(user, "defaulting indicator location") else - name = "Old-Designed Hydropoincs Tray" + name = "Hydroponics Tray (Alt) [name_extension]" build_path = /obj/machinery/hydroponics/constructable/oldstyle - balloon_alert(user, "moving the indicators...") - return TRUE + balloon_alert(user, "moved indicators location") /obj/item/circuitboard/machine/hydroponics/item_interaction(mob/living/user, obj/item/I, list/modifiers) if(istype(I, /obj/item/plant_analyzer)) changeindicators(user) - else - return ..() + return ITEM_INTERACT_SUCCESS + return ..() /obj/item/circuitboard/machine/hydroponics/screwdriver_act(mob/living/user, obj/item/tool) - src.changeindicators(user) - return + changeindicators(user) + return ITEM_INTERACT_SUCCESS /obj/item/circuitboard/machine/hydroponics/fullupgrade build_path = /obj/machinery/hydroponics/constructable/fullupgrade diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm index 2e9b24675f17..46dfeaea46c3 100644 --- a/code/modules/hydroponics/hydroponics.dm +++ b/code/modules/hydroponics/hydroponics.dm @@ -52,8 +52,6 @@ var/datum/weakref/lastuser ///If the tray generates nutrients and water on its own var/self_sustaining = FALSE - ///If the tray is currently able to self-sustain - var/can_self_sustain = TRUE ///The icon state for the overlay used to represent that this tray is self-sustaining. var/self_sustaining_overlay_icon_state = "gaia_blessing" ///Whether the plant is currently being pollinated or polinating the nearby plants @@ -68,10 +66,8 @@ var/plant_offset_y = 0 ///Suffix things var/alt_tray = FALSE - var/indicatorsuffix = "" ///Soil things - var/obj/machinery/hydroponics/soil/current_soil = null - var/current_soil_overlay = null + var/obj/machinery/hydroponics/soil/current_soil /obj/machinery/hydroponics/Initialize(mapload) //ALRIGHT YOU DEGENERATES. YOU HAD REAGENT HOLDERS FOR AT LEAST 4 YEARS AND NONE OF YOU MADE HYDROPONICS TRAYS HOLD NUTRIENT CHEMS INSTEAD OF USING "Points". @@ -169,12 +165,10 @@ return NONE /obj/machinery/hydroponics/constructable - name = "hydroponics tray" icon = 'icons/obj/service/hydroponics/equipment.dmi' icon_state = "hydrotray3" /obj/machinery/hydroponics/constructable/oldstyle - name = "hydroponics tray" icon = 'icons/obj/service/hydroponics/equipment.dmi' icon_state = "hydrotray3-alt" alt_tray = TRUE @@ -191,9 +185,6 @@ AddComponent(/datum/component/usb_port, typecacheof(list(/obj/item/circuit_component/hydroponics), only_root_path = TRUE)) AddComponent(/datum/component/fishing_spot, /datum/fish_source/hydro_tray) -/obj/machinery/hydroponics/constructable/on_deconstruction(disassembled) - current_soil?.forceMove(drop_location()) - /obj/machinery/hydroponics/constructable/RefreshParts() . = ..() var/tmp_capacity = 0 @@ -247,21 +238,27 @@ if(myseed) QDEL_NULL(myseed) remove_shared_particles(/particles/pollen) + QDEL_NULL(current_soil) return ..() /obj/machinery/hydroponics/Exited(atom/movable/gone) . = ..() if(!QDELETED(src) && gone == myseed) set_seed(null, FALSE) - if(!istype(gone, /obj/item/mob_holder/snail)) return - var/obj/item/mob_holder/snail_object = gone - if(snail_object.held_mob) - UnregisterSignal(snail_object.held_mob, list( - COMSIG_LIVING_DEATH, - COMSIG_MOVABLE_ATTEMPTED_MOVE, - )) - QDEL_NULL(our_snail) + if(gone == current_soil) + current_soil = null + if(!QDELETED(src)) + update_appearance() + return + if(istype(gone, /obj/item/mob_holder/snail)) + var/obj/item/mob_holder/snail_object = gone + if(snail_object.held_mob) + UnregisterSignal(snail_object.held_mob, list( + COMSIG_LIVING_DEATH, + COMSIG_MOVABLE_ATTEMPTED_MOVE, + )) + QDEL_NULL(our_snail) /obj/machinery/hydroponics/constructable/screwdriver_act(mob/living/user, obj/item/tool) return default_deconstruction_screwdriver(user, tool) @@ -474,8 +471,19 @@ /obj/machinery/hydroponics/update_name(updates) . = ..() - if(!GetComponent(/datum/component/rename) && myseed) - name = "[initial(name)] ([myseed.plantname])" + if(GetComponent(/datum/component/rename)) + return + name = current_soil ? "botanic tray" : initial(name) + if(myseed) + name += " ([myseed.plantname])" + +/obj/machinery/hydroponics/update_desc(updates) + . = ..() + if(GetComponent(/datum/component/rename)) + return + desc = initial(desc) + if(current_soil) + desc += " Filled with [current_soil.name]." /obj/machinery/hydroponics/update_overlays() . = ..() @@ -483,8 +491,9 @@ . += myseed.get_tray_overlay(age, plant_status, plant_offset_y) . += update_status_light_overlays() - if(current_soil && current_soil_overlay) - . += mutable_appearance(icon, current_soil_overlay, OBJ_LAYER + 0.001) + if(current_soil) + var/soil_overlay = "[current_soil.icon_state]_tray" + . += mutable_appearance(icon, soil_overlay, OBJ_LAYER + 0.001) if(self_sustaining && self_sustaining_overlay_icon_state) . += mutable_appearance(icon, self_sustaining_overlay_icon_state, OBJ_LAYER + 0.002) @@ -492,8 +501,7 @@ /obj/machinery/hydroponics/proc/update_status_light_overlays() . = list() - if(alt_tray) - indicatorsuffix = "-alt" + var/indicatorsuffix = alt_tray ? "-alt" : "" if(waterlevel <= 10) . += mutable_appearance('icons/obj/service/hydroponics/equipment.dmi', "over_lowwater3[indicatorsuffix]") . += emissive_appearance(icon, "over_lowwater3[indicatorsuffix]", src, alpha = src.alpha) @@ -1101,28 +1109,21 @@ return if(!isnull(current_soil)) - balloon_alert(user, "tray is full") + balloon_alert(user, "tray is full!") return balloon_alert(user, "filling the tray...") if(!do_after(user, 2 SECONDS, src)) return - if(!oursoil.stored_soil) - balloon_alert(user, "sack is empty!") - return - - current_soil = new oursoil.stored_soil(src) + oursoil.transfer_soil(src, inside_tray = TRUE) RefreshParts() tray_flags = current_soil.tray_flags - current_soil_overlay = "[current_soil.icon_state]_tray" - name = "botanic tray" - desc = "A basin used to grow plants in. Filled with [current_soil.name]." qdel(oursoil) update_appearance() - return + return TRUE else return ..() @@ -1166,10 +1167,6 @@ update_use_power(NO_POWER_USE) return CLICK_ACTION_BLOCKING - if(!can_self_sustain) - balloon_alert(user, "no self-sustain mode!") - return CLICK_ACTION_BLOCKING - set_self_sustaining(!self_sustaining) to_chat(user, span_notice("You [self_sustaining ? "activate" : "deactivated"] [src]'s autogrow function[self_sustaining ? ", maintaining the tray's health while using high amounts of power" : ""].")) return CLICK_ACTION_SUCCESS diff --git a/code/modules/hydroponics/soil.dm b/code/modules/hydroponics/soil.dm index 315a773ca860..fb343bb29d08 100644 --- a/code/modules/hydroponics/soil.dm +++ b/code/modules/hydroponics/soil.dm @@ -150,6 +150,12 @@ animate(time = 100 MILLISECONDS, pixel_z = 0, easing = QUAD_EASING | EASE_IN) animate(time = 250 MILLISECONDS, pixel_x = rand(-6, 6), pixel_y = rand(-4, 4), flags = ANIMATION_PARALLEL) +/obj/item/soil_sack/Exited(atom/movable/gone) + . = ..() + if(gone == stored_soil) + stored_soil = null + qdel(src) + /obj/item/soil_sack/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) if(!isopenturf(interacting_with) || isgroundlessturf(interacting_with)) return ..() @@ -161,19 +167,24 @@ if(!do_after(user, 1 SECONDS, interacting_with)) return ITEM_INTERACT_BLOCKING + transfer_soil(interacting_with) + return ITEM_INTERACT_SUCCESS + +//Proc responsible for placing the soil inside track onto the turf or inside a hydroponic tray +/obj/item/soil_sack/proc/transfer_soil(atom/target, inside_tray = FALSE) if(ispath(stored_soil)) stored_soil = new stored_soil(src) + if(inside_tray) + STOP_PROCESSING(SSmachines, stored_soil) stored_soil.reagents.add_reagent(/datum/reagent/plantnutriment/eznutriment, stored_soil.maxnutri / 2) stored_soil.waterlevel = stored_soil.maxwater - else + else if(!inside_tray) START_PROCESSING(SSmachines, stored_soil) - - stored_soil.forceMove(interacting_with) - playsound(stored_soil, placement_sound, 65, vary = TRUE) - stored_soil.on_place() - qdel(src) - return ITEM_INTERACT_SUCCESS + playsound(target, placement_sound, 65, vary = TRUE) + if(!inside_tray) + stored_soil.on_place() + stored_soil.forceMove(target) //stored_soil is set to null at this point, and the soil sack is deleted when that happens /obj/item/soil_sack/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE) if(attack_type == OVERWHELMING_ATTACK) From 5c9539c99fc985220af06e0a8d86fe266cae2fa5 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 06:00:25 +0000 Subject: [PATCH 009/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96167.yml | 4 ---- html/changelogs/archive/2026-06.yml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96167.yml diff --git a/html/changelogs/AutoChangeLog-pr-96167.yml b/html/changelogs/AutoChangeLog-pr-96167.yml deleted file mode 100644 index 619840e58f7c..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96167.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "UvvU" -delete-after: True -changes: - - bugfix: "fixed improper chat caching." \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 7f835ae87acc..165386745747 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -181,3 +181,5 @@ storm sounds - refactor: Rain and ash storms have been refactored to use particles rather than overlays + UvvU: + - bugfix: fixed improper chat caching. From ddf8b83ea3fd074489ae4dd7e246b409886ec678 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:58:45 +0200 Subject: [PATCH 010/101] Makes volcanic pores more visually distinct (#96375) --- code/game/objects/structures/flora.dm | 2 +- icons/obj/fluff/flora/rocks.dmi | Bin 7819 -> 7813 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/objects/structures/flora.dm b/code/game/objects/structures/flora.dm index b02b29c2b72a..d46b7e133f4b 100644 --- a/code/game/objects/structures/flora.dm +++ b/code/game/objects/structures/flora.dm @@ -1126,7 +1126,7 @@ /obj/structure/flora/rock/volcano/Initialize(mapload) . = ..() - icon_state = "[base_icon_state]_[rand(1, 5)]" + icon_state = "[base_icon_state]_[rand(1, 4)]" update_appearance() /obj/structure/flora/rock/volcano/update_overlays() diff --git a/icons/obj/fluff/flora/rocks.dmi b/icons/obj/fluff/flora/rocks.dmi index 098f0b03fbb016f1f77d7ee182bfe992da9b8d5d..c68ddaef7d064f8c32ac0b9fa75d36647909e61d 100644 GIT binary patch delta 7512 zcma)>cTkf}x4<7lfIvV3NbjLHDN2zVLl;nbla5FSQ9z3L1f&L#CZKdgnzW#RNbk}l zD7~Z7i!|vGuJ3p6H+Sy+>)V+#d*;lZ-PwBPcQ#fjLJ6lq1PW`b-7*fC-nMohbQ-gm z=hRY<6jJ@f;6g+~p(-wsQhl3>sB|1-@O6k&w@VvVO^bHrexxm!{7_Q=9k3Sun z53Xn)KWN%%1iga4cuv=p_V&Iq8I-1Plb*1;zPOs{)!^z?dR4b4jvP>{ylrRo;lY=T zug7Bd>gtib$iB5&KWVNGa`%aMcJ1t{DA!O+z zugnrhL8+i?z+w-pHyfMZs6i)Q#I&3#$>dfN2H@&=4>}D{SbccBI`q|NXTCWC8@{yA z9DKZ{jL_*da$%9AEy|im1^Bs};;xY7jav~7REABqgarF+*G|0X)ic1%%0-1uH3c4Q zoSrLpoEriNvR~x+yUHBxYX)d!yArkBB4+WXp3kpOt5<%tUS8bfI7ha`y@0l1Nqi1J zUmucl1k0X%>9^YGxbaXHfDM`qP<0=tyCNLdkB1p;0%_ zx+wc=KA@Q8H*Qf@&Q&9|*NE++O}3|1f@$FsVGN@@^+l}6x|Omxf=PvR#l>{3JK3&z z4q4vX!1VRAato-DAm(aU?;3g6GisXp(nAjx@Er=~GEr{Z`YZ|PM@v*~KYyI5c((^i zGW;@bJVj8E=&j{cWiMx=_{}}gLf7Q2l$=0QbG+c zxtE^X;qUPUqQIi^0))HJzj3gudD@Y_rdzGJ9i zGf?hj6uVhC!CzXI%o!cH9EXMI!}~$B9``f)VFfOk7lFVL4nP>VIr;5lPIg8jD{#)Q7^|=w?Od(lEf>POgsq8bBF?`ZatN zCIT7KK|6hJU~^6)WIa)*nG7^H(J1=>$Xg+vE{<`mmOz&OEQds?&?gU3yk?{e?d{<2o{S_j@eiWYV*+o^AiXsE|?rYK0 z)Zcz9dSjowv?)-Hw{GU8~5pb0&nj6j&!~x^BeSK+JmnJe2GD{jmGa|YoRiKULH73$j}f)Uv@odwb0GftY3Q* z8wtU|!JdeZt1(}!1CKJ=19Lz5{d$34kgoro!k8c?4lhE#Y_3!@^Nk31n4#2ZUa(PW z3mu?lr^(&so6%Uygz<|3cJnS&35!VOgaglriWkr)-$A0HP(nbD*-?`O!Bz_jOdBF@ z=c-|9@p3~6GrGKYnG`a1{Bs+3iHj-6=k_8#u~xP^J)Ao@oow08D>g%5 z%|c=3!XUEb+qgLvzrY|7ebz=yJs7f7l0qswA2Kn^mu{t8e=vmCs{#}sE#2M`-#vn} zZExE4#PE%f_Vz+bupfh&R)-X~mfER~314Qr89|4K6V|q`yr>hCB*mCAf&ENMDgGG5 zWe*ZVKu^VKr>Jzb4v{1-B;B-DYp=$iU$6NBw%#F0>{AJWtesy_`sE1pv<^Y-Y!M22 z1?^5L8)@$o&v7c9a;Fn##;}%D6y44b=?H7OwmbQzYeP46^e96Xa`QbeYWaa|gXwOH za&z@si?*X}cZ%%EaREs?4W~8PxiJCyl-XSSS%SMd;D3?O^0w~F6LaBp?Qg=~31!J( zWC%P{U6<0@td6hAnz39fgwv>mjlWiHxoa9K0 zk326aN|w>%!?!ZTysF3DI(_EfK49dvlL;-L;r_P<4*uCldV8K88Mk_kdOaGK=l zX^80$k?E4Bw5@Tw{>?JB35I5XTfiAuqoT@4C=&6Q%CTDCKwqx><-0&w5;#-m+5J6=v!(Zk`|Fg4 zC!L?L#zenEz{t8&Gi@Nh%jD2BMCdin)n%(9jnl3{o~g5DRDPU3)boisH65gTI-Fq6 zMvIgqNepX1{jCX^BS&OR}O-7d~|FsjJO_U%0rs3~yOUX(Kj*a*Lc z08};~=NUfPo|bOhyZSyQ5qm`@9pu{?MLWiit%fs`e)YUm_Y86jMMi)Yn>OAKNoHJh+GPKW#k+; z3I>GfLK#7j&Xg%9!r^W-BdG~_=#8OKFSC%G#5N{n7cJv-0cVAo*YI{Bs}eUJ;~g zp~obDDQd^orDabP-bv9S*A)2l&*^5IMaIPJgCW0bB3WWLqu(COteEpRb&TZuCY+d% zL5)gk0;jxLk} zM3L*7BWIFaDBg(Pc>g%UT)B>0as3Z-&li=y_e0twqdu zg@;Owa*pGitwciB`Txw4mT(WJp3Gk<`RYN~FBo|jnns+CmuT*;(S9%lXI+%Dyx)Y$ zCM$?2G$I}qDRJ5hr)8neYkNiJVVFPOilr}BIPRjO)7ZF?>a&fP5W0vz0?G6z3QhYn zS>J3J(F&srKFWCgCT$fxmG+0zOb^lT_pxmu3_W9W5RL$03FhHdpA=eKqHaRWN%dX!YJ2M9zsN(2O@#xky}7*+8Qwp# zJ8visq)8#I_#u|w%~vjC$%WIN*?#4_6s{l}-YGXfZ+(?fHmmY7ve~Tv-mIZ)dgZrx z#>OYNm5rPuWylRc#ao9$OQphL*G|%p$$DS8-Y2@z<>!8R|GpEhv#&h;bA2eg)5)iy zAt?(0z4Q*+BhC5ja{udb6&Rc#5+<5_18K%5QF+?dKcczNTKmVq3@Vu@PNc@%PXe^UnfeUzo3{auo0&hR;nz5_9`?3$ z;-ff&7Ng54LLMyrjQ=w)`tVDNc#EX`uk$nIe-X`Lhe*LWO4Q~e6(*TnnDdut6d%cu z)M+aHz=1rGxzsj$l{6OgXM??;pPg^pE@rHPzX@w6XPZ^@!nDbkg>xz#b7T(RZ^ojz zj9J7id$T*gu{S(LC>9Ih9&`JPEmgbkl}elCu^A&WThAYbHhY*v_riPQ9}B*U0)L-8 z3}f1XZ(XckQg??2qXnB1G$kx+5*4Bf71pxJNI?AhdqN{3*YYt*|1FZelNUQzXH{RJ z)of5+S*q=ddtFZ=m+|M2sy_?vsy>T32#TGb6qsC#r0NBqxaRXh&sb~z;Re^AR?5^q zEA|57uSi@~*;YuFqF{*vVssS1lFH8zviPPpc<>5C#O?78H}#%p;v$!IV*amDn?>=D zPeH{lZGr6fID%euXA_2iAliTHgeJle1abrVd*MxllyuEwAJ%ryX3mxuwMAnVjsDxf zbJiR(u62jwvE72okGSis+^}ZqXJ$#}&)XziyA_^!ohWEk1@2SoW-i-)woFfLO}^%D zOQo_A1iyEI2%jfwEz>H$W%sJ)62Eh4iay1s*c^l(Ja0zni9H$7C!r73*590{vgO6R zM-0X``*@|SYM1hQyW!}g|8O858V$_dFeXn0Z^Kr)=jA^39gK>l{6esi3F>@|32=Cv zLPNs<3WFm$DH&dLdtLplqr`8!PLEh*C_`mf`(@tl!nU)Cxvr4ar1MMb)!i(I{W?Do zAzKYu8NU-`Z@w<%)6yJCrJDC8@;#N1B06{#i0&lXF4Wdb@w_4url1T?EPlIdGrKlr z$qFkNyX(3pO^<lP<|0=`k7uTI%-d zKxla*PxC6T(^Bo=Mv?ifb~?zsH=tJF^!5!7i9uC1UcAErlbw(}S|ipEKLb>rn(0&b z=nF)gn4oq*Ol0sNF)oS(#aATrp!gGE?78Hq_P;1I?>omK;DJIzd7-2}{F2-DS&+7* z_&J#v+f>;9G2`QTSQ|ZQTPc5ONvzfbh+E|YKBX~v+YhPiw89@z>4oBui%m??T;t32 zqiVzc^`sKK=ueL*)c(f7=U^nd%aHNb3~KWEKH6gb`1jFwE^};~hoG9MA59HaxRRjw z%jI#+78y3^2MC-3laD}i1wFT*xium*!Le61dsme^9QB*@12U}0s8Z=i)GtPMm=R4L zJFqtR`JHv4+>y1M2jc(Jr?Tm`aCh}ihy8PNFxbtoU0>kAo(LSsg8@g0ZwGrveIknJ zMjVd4f{WIByiH;HU!xQhPgC8hx9Vgy;1s`CTN_nkzACK2q{78Y!It zxl}}q)Z51H${#D$K@*R6`HQ6aD%d~3mJ2uj$mtk4`|i)P^&K|7zEfOjc;O|U$fp9p zi>=>c7%A4-0xZ>Ys)%(xVVL7Ok@}6*$Q7L~sep96(FJc5#@Ev1(;aMA@0T~Xx>TPV+jmU^AvZo_)#FL>9 z+%lpuVdXsGuMQ`Oq$0y^K&sa#30WCrV&H$&(kd;AgoEow#w+jZ+Pr4| zfvaxMeARBIJzMt}_WAq1mBN01p)^qn!wUX44~LK*^7@zFXLKh&GrYLHD9>?A{VGzL z@x$6z<>&t*HC&^1wtd?Bp3y%79_?3ReSst2Nr`P?;QdEWK6VHMF28M)#|VM1$dE zFflD}M@ik8H(8@g4V426_Y&uO9A4F_GrRBT)&d(=6Vl|W9c*T2p9mrLk>Bt;r0m4t z&9vlYTesz0l0?ao-VwL!?*S>P|CS;~PKDJhk`bWg_u_MlS>_Mi7`S-7-NSBb4$&*E zKV+j87ADVheGXH0uF1@YJSoN>w;SDM$NrkrkbvK;3rrlXr<0BqtHclxRZr6P6GQm< zbVYw^;b{kTY={}B5#g)tw&=5y*~i(6!3-#E5`<^hqOc&Xl^AP<3WlOsz(ohhP#zgi zgrjDNzcWY(mBDXaOlg8rgt$aXj`acCsT+?WOoNVUTYa@mE`wGCTFHhV6ye znfb?eJZU5b?eJI1S-yea|LiKfkc&dG4e3X}Dbaz@-*9|7d;a}$QjxHrEpYK|w^0C~ zh(c~Pq?aUcxLT?W_sYN7r16m`S0ZV{%AIp6&tL&94~qDumhIvNo8fKpGBV`G&AVGC zJKP4Fi3t$k^!1SK)6?wk=1bp&-Glw*0g7-N-kU(Rg*|e|Iuhhzfj|&Tcn^s%MB%#XyXDZxVaYs;B0EC3 z+yHz}&HDD_6+2~L-8b>1rQ1OfW<+ZdE>9;p96!~7db&Xx@c@SZ`!8b{W;}}CQ(Mze z&|(vYDrR(HY2wVEM^z{tbZo}FQ$r~qZA~>UwpU}X(OrIiVCGBRud?$viVWO+*9{_LU5-9!O75FN zXd4iYbmJ&9w^wK*g`eX;U+klAYcYmcWX9acOQJ3bv(n#nna~$B;GZ=RR%sK?Dld< zDhIPdP|7uP1;ftUZ$9eu?U3hf9cw`Bw#WxQ_zl$AHziOwcTfM)Li~KvatyCk!Z6gB zO7(r&seb(XN&K9VDkAk&hjfvM(z&lsSBfBZnQ{1{dA&3iBMsd?f0NaOfmgYR{pNnAZodNbjoY?`K?Tlj}4R&cZF$~=ZX6$w=N zl+NX!v68%5sHnyn2^#i8m%qn9cbRgxDAih1nn7S#_|D@jk8R9vP(4ms>3>;OUA*l% z99pC=YyS>r{=K{=F@jj`hdb+Md5NOoulKl&CG)?U%;3h~+Z7(1)>`>ysEzI89Jzy< zgBb7%A1KG3OGgiweF0bbPU>X6;~prBwZy$U-ciLeV6B~XBsfMtN!+$%&`Q-v>bbDQ z1*v4ds8g@`(uS`qE{Q|bosZFlaVY6{#G}}WBnV##L0u0{-Jet_RTb`GOw_OcHictbDe@3{ha(*;Zgu&~X;m*clPhXwE)*wsmDFLE-dLyDjk|A8D z>3jC2{mAn}7Lf1F@s#^;Mh?~l@re(I7OSGVZVaTAe53!=aC>S}s+i{eT)~~K?4V0g z4960;WnZ26!jKG~g^|ik9hN>coiR4?vk_GEA)P}JOP(%4hz@0_e$U?gZjvzOblA}k zX`dlOa5x_7g}7Y9N~SzAiTX$H{hJW}9|8ZZ{H@nofsAXqe?Rx!h|Ia;RDqwmB0TUs zvtAAX9ynKuilZsXu;x;tDBNzPFP8)t4ox&HwKjdqZ=xX%MFd@*g|YgfhWTBW9`Y7c z1Md(uB5%g@c!Cq+hDe5=+!y~F-XSimA^XCCY`lMaAW!dXVy0{34G~d{GO_@gxi8AA% delta 7536 zcma*rcTChzyD0GQE{iN3mfm|2q^KYr7FeVzpmaf+2#Pcjq<@#*lp<0@+R{;^3P@i% zN|m7xA4SnUO`iQtKxuBaK(Vnkt7O%mD(SUYjRclZM z+r}xK$1yVBi%fFRv<;*WxB0o?Nal1RM9_^D=SdnGK1*AW@!s;Ct2&(YE%onLJSrBb zomiQ!JbG+7u-Eh#Je~AiXgb?sp{4B=rk^C4{~=t;y1RFPvurMVAe3*_4+FSyeBvcC zzuw&Loa6BM!(|Lp<0WjQ2O`O!!Z%C}d5`_3-MYv#e79;_A_X#}z1E5fuAP_Q@}f|o z@3`N|t}LTyRP_hlApp+e^^EJq=|RqbBd&xk!B9CK4RC_3YoniuzAc}uWO8sEZB9w) z#mAld9&Rwr5!n!XlicIE+xmX54uO>jF&@MKBdNkx6NXfBueVsuJw^>}Up}Pel zt!#d=Qw&!2Ph#erB2+7a&N-#w-+c>#?WxYUEM1SS@Rt`oDtvs5N)I*4=ROz}qH9Gw z6Or{g^;xPAuki}I9)4lClg@bwcX`N@6ciqT zF8QPth~Vj`%Qs0?5mwg-IX#7vUVbA`8CPu&6+vI+(g0_8RquR7QA!aYJOkxW1m$D zc2#Z%Sz8^kj`OuN)%(#b+%+qCHrAO6HKHdn@68)D??McymBt)AJq^^X)qt1IQ`I8g zOT2c=RX^6MtG+$+ zE_QO-yG7zMrl>e&;JCHl`Tio#>4_&N7nB5%;IJp3#LjFO73yQx_kbKy*6P~JCJK;6 zBDV$+aA9bPwu)_680$*>x%q1~>TwSfkEfrb0oiG);4-3IH52!%Tzt#l@&NV}ROPan zMOh`M>H#DNPymJ2BMuVu4+(=htSc%k;@C#51yQ6@F5G2Fb394_kFpC;xN^fk_;HxW$49-~vDzXN=L&#Z;=m=zw)>m{Kt%g2SUU$LqWi&Z!R+@5^m{%b|Z( zo(v@Rj%Lf8Qp>L9asldv-~$`;bmQ(u@l8@=zW~3#Vl&&n@7uSD6wy6~KZ$>!z9$sN zmu^KNBNSK_B%QEsm?~P(uN)AisfL}guRG$b&P_eqrq8c(a)r;gamd*3pUBN!E~7%Q zw@i11S3eeieExDKiWW@~{8B-wE5ifF5M$A?$bK@>EsN69AD92b4#Ir+GD>OuWMHo6 zzEW@03;DTV!eRNjV&c#UQ!t&=R!{}enM8ndVomSfed=LIhUsu%zN0UG=*MYnUd%oC zik~aQ+$1fXqiS?f=i=N47&sOFz~pI#3Y4pFaQAL9o6qOg%N2FsF1Fo}Wt|Fhq;07j z*kJZoT3w>-e@_(!0=*gYR7JHP)MAs6YnmHI-$f4*= z*B}%Y?jtG38)6fRDsw1tCkPOI*YOpMVaNNOu5hm`&2%V8ltQ?h^TuVv-+R9~4i4<+ zalTiK1bj2y%+qQ&n5;aU21>#R0;#Jta(4rNaZh} z>SxEvnl>H#5RxchZ84p?Gt@ms(avkr0&H~M>BwZ7^_==rxd|a5c}hq_gY2r*b@BD@R4P|u)KE}ykGYAzsWuW z#|z3#+uH{{Y#+5w`L{%ktlSJ;vo^b$K#Q+v5RiWFrFHEAOHi&s#p)v=cai#Pm)@_s zX%vx=!DQ0q7yGgyM^wH7j|wSHj|0L)k8xT+o!w9!JAYbRR2hw}oa8i5+C$i+*K9O_ z6QS@9oeXnkdntFrOle(?fNcNFV25Efwlq&d z%VJmaPaD2{c2=8&zC-bH3=dP3kq4viaBr({o=MY_(h-KX-7WeUPIbcJy%gV4=dqq- z>i5MRK>$C<_`JW#?f5@9b*C8lq9pReD1Ofigjz1 zER(4?j*qG;uma%0paJZk6K4R=wds*si+-=7o@O~SKH(r@JU;fEk6v+SW|H`cd^?!OuKsA?p3B46@h}JCQ6(^%{*!;O~Ev=Na+BJ}N1q z?8y^6k^|uw`9TQka@rtt{?pHEHaiAx0ML8DsA!a<82(IM?cI{)5a{@zaklZEe6HeU zQiUh)e$cbc&2aels2!CE1M2aLd=jh~*vK!r$#_!071Y#_iE+GCBb-;rQItSn)`qo~ zSKh~AE3sMup-!A606a1)5&+%`G*$?IC-yfkqrij>{nY|`Nn5&ljjL4uNoC^8Q3TpE z>P7hNe8swAA!^WTN6I>M!Bj9N#LHSQol4`E4x8ubZV0S%TUj+be@VcUc*HFcz9C6p z^w;=L+ZhvoOY@)T3dx)NThxP}Vs9oJStTN>u(Io)$qVS>nLnhiM)@Q>e~*FyFw3Cc5Ie;yk!!|> ze%736{qCH!iTnHgTcMquo3_ITJF8mofhaF%Avb7fKaQ~c{_sHKH4A+d?_p z&2_^4dI1nhheo87p{j5Hq(vDzJi(Ea2fR@Cn*7&|HHQ?A8XR1&eZjBVzI|)K^q0Zz zBfF_r9^|oA8Rb84t9~RMA5AGnwR=h1QAkJ}w)iaN;IIK&qMI)GP~{=u@2%99ds4H( zW_|NuatEg;81(m4&&1=yW9bKK0e|ycJR`J8!9G9lC&b>6GiGkFOWezC`7d4rq~jR9 z`gA=)D(o)-AiDhSPl)BZ+xDKK#hNN(T|} z^6eaDR*CkP_ceD=qXoPMAx~YLrvKavFx_?Bpy&Q-A-ohXvqboU{e|6x>>%FTsJrsm z@GkbG^@ijm{j;YyF-Q9v2ZY^kpqyBu7TWX8CFO zn%R6v-|qN05AjqUKr!+m5idgLO|#>|!R@DEi2%FiLsS+!r(J8Y3p9lupL76GQNRyI z=}d7|l~?wKcr=zvWee9zT6|P@iGMF;n*AgnuwmCIt?H>TOd%Pt^UYDe4%MJ;(W1gi zoG`EwE^7ADTF_v|MtSTC37AsPZCYq$4?p88pG~AIVSu)w0Zd|g2J%6&_Wr@sc6zs|PHg+KF360y7cO$+vQ)`#tqfyUs*MN_u+<>(}3*4#wdA$U^~R|^V~ z`)D;nt5ryk(lcYIcZcPteS;(jObZ=pE-3cnJ{%!!3D>}LE8 zYJf#3FMcge&M7+|!2~lJemMfzA)E?nxkqGP zPonPe-GUdFtvXB|H!KfU1vk2EFtIjYUIg1`;#<&j2X#X6H)w!JDjw4{Zwq29kNqQa zkA;pla&>lz(}t}=)vhOo4j-|(w0eIvh+Wr@l{roMxxzDcxtLN+B654zZM!9$%(X9CtWjFA>0s<50Uhyo{8|Q{ z?aD+8GCeAs63ilI-RW&ZY*kMgee{&4@$HTT4LwGo5e?{R z>F}VFW$oBl`iPw-Yz7DZ3Ge^-C_8jo2D~w|5YEOfU(j>UFAihiyf^I4MU3sngxJ4e^fCXfyD9r( zT8^%11<6pDbT4T)B{iq6nE)-&4UT;ek8Q;E_%bAgYK^r%SM3Za{e+*Uuh#;z?hSX= z-k@|F(ku}D0@5ssi*YKU(DKjH^#D%%b%6}oLuhH^<>axVvw$>iZ{eefqgncwM>P~T zYZttMccPw_7(hVjDc2FO<##j0!FXwG}q++0r;dM zE_T>i>+~aGNMm-$N|@?Q0X1KpRPR)X_=JB?)f?UDcEvS`FA1wutYD#5&08-U$0Y?w zuGvIMH%Gmg?Q=Nid!^+}WcSSy@U~V$sx!+@`&#o5L2moWA+sl0U>chp%LA{F!+r== zIDza{0RI`Q5zO)|p(lI=-08f3*LwWVb+@L*Fg8_Ad-aJhT6RpMJJ7y>jv30$|E?HL z*V1QM>*V!)Mbz04tbRdb{q^)ZTVpj;4?W1NHD1=HdSk!L=GuuqeazzcZtj2V*W|hX zG9U6%80oNCJXO{fjH{)LWLi=;m5#FQN#KTN{7PTH9|E~o%&SUn_6Q-73|Cn?Y%rMl z;>Wc}z{-9Q;(o@L$u!gxIL}IM^I@l%rFmy?xo1EDv?Yq=qLxS>D@c4&sO+d6w2V-W zn9fLkZiY`f&N&6`y+&yIt8mlexyc0osPfh?5WhVtCxssq5SwnviNdDC30}1pL}3k$ zq&gPIoFZfsQ-u~8cRr(XJaV(kxY5Y!nKZ?h?{4jx*?By?vGsMmIqKLj`={&!eG_UV zr!i>hu4V$TT0;gSiqa=E+6qX^T1#h>^J<5c*|a7RFud&JR!7!09<;5v}7a;~mK{-EgvW^5e%;0GU-G9-?SnS@%{ z0#+MXMsyHbcRqRaI_6*eywYc0G^Z5RVOEn~vK)JWl?3rqLV8+2l`90fe{GLDs9L+^ zilMccVJh&zKjh15FsEd5-*(CwFBC3uI08-?>f!}QoJ2$$LbZ7bm&q; zP9f+LzhXIqDBHQvHj46?k=!86{oX|!J%FSJSh+M!gZ)bdm;9b0_qfcrMcngS0v2cNQ_yZ-FVf-3cVqpgA-fNb0?hM@gk`;5 z`j>oWb&?@PN5z6n&LUfL(WE?HK$fB!K88DH0{%+f?s%l~o1^KO`fN65S>c*e37DH1sm{H!I8*1PrFJ|3et6i&4LRvuGprPDMjotuC(1mb8Qa4 zFx+zrFOTEnr(*R`H`WlW9?%;#PI(6LTDYqLLo)c%%0rg+fl6cmxs(bwP&b}4Pi!SB zk91J=iRlxsRg?MB;bDPmuHjPXdYUqw{RTXE{nQM&SF)ZO!exD5=;8ssNnCX`**^F_ znpODwZt2%kF{ogQ<9oX*LN$Q!e|n$mS{S)@lC^xbBTmQlGdzoM)_uQDKZnEZ*!uSH zyv2N5L8jvQl%yEh*u~E-^|`s(osIbf{PEhnT&(=ine26_gYu=Xu8bb&pXsxJZ3^+G zgTP!z;vI=t?$3&UTP(ygU6jJDLKE;xHTcdyw~J?l=f-6aE%GF2%s{FtC0m;JSSf|4 zLb>BUz5v|b&>2yuMiQhQ{S;|ay4bM|<{6lgJ`MO;6Be9oIdR(~nn?T2uUV*+qO3MQ#Lkc69&?=k z$(u=EA%c3%xvh8Q761Xq(|b9;cy|X$A!U#V(@4nYAMFEQ9oBXs?M<#n*snL(VcusP z4X^i^xR~UJ+UBMvQvJ*Vz7X&!6)OQPKWTKdfi|@_k&O}kAYs-MWT2G4>n}8U`Po2| z;_*ylSAQ|wVDQb3et%b{?BI8~t#RACa|)0+XZ&9-~d+-Au zwPpL@`PM`AcBFM6%`ABSXFNQ{ngxQ^nHo@x^M?oiHh`3sSJ6bQOYly-S_kbJ(4Jn9 zFwS0W3W!N*_D#hf4n$BYPY5k(-t(7_)xoGfZZ9>ckhDIWP-k+Mxa==pT>vY9+g?2IU3OB6L4&^z4B9NE|O?f~@3voRL{ z0z`GSyT8)d929OKL5iV%wOukcD$_v5`$g39jymwBL}t#2%&t2s8;O8}|M+0_AaAIV zkuxM~n7s9wQgBGO!Ws;*F^30jA!U2feQas|w5;s~N8{cHH#M518x~#w3BYZ@H8`}q z;XY2xZZhuTWF~oeVH-=?)fVu4b3+imp8rg``&OmusZ_zm^1GDj{G2TPPaj3qvO;Qj zapG~EfvY=aOS&>coutcACyt~@2HC)KpqP?{4x7qoVq;clS^LMbGpJilgGw2+)hEc)85WO+Ju|;Ro>_IAbIu0%<`Vcg~wgr+CI8`#2V3=GE!j959C% zh+_`$KG^8ED0km~y(uS24zX2DO*&79qcPWDSQTn2G!z4AR279}={vY-_-($p{{2-2 z|J5D6=b=jw2IuIeKqun@}nHO2^-6tMpA<=~(-PcPkJg98DyQS^NjFu1V1 z`qH`OLO%Dx!UVF-+Dc<%p?s(UXq9w?nIghWsHtR`DjEMulBngQ-BR-C?|*qr$FzBi z*u~GB`XS)m|i{u$e$GR))FATQa6 zJbN4Gpl*>sy64_bDVQhas6ejLhklPT5#>_1%j_Ok*wQcr`ZWfpy8ti|Hi2_KLnXgLj|E{|K8CQl@v#3S^Prv=t zwoR7(AP1q1Hr9jbbz6#;LjUk+9qYrykCv{=HdHV-)#@aAdl?0Iuz2x>@E8s>yO>E4 z-sT3T2JgZ2j_OQl!EP=v%&-_!ti?FfWB17trcB!Sh~dV^Xp!w^yK ks}%_TKRW9_0Vyu$Wb~Vo+1h8{vVp6nsdgV#qGA>FUoU(#`v3p{ From 6b738868915f43e84a72f5c8d267b89ccfcee23a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:59:06 +0000 Subject: [PATCH 011/101] Automatic changelog for PR #96375 [ci skip] --- html/changelogs/AutoChangeLog-pr-96375.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96375.yml diff --git a/html/changelogs/AutoChangeLog-pr-96375.yml b/html/changelogs/AutoChangeLog-pr-96375.yml new file mode 100644 index 000000000000..84e3a8b850cd --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96375.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - image: "Made volcanic pores more noticeable" \ No newline at end of file From e2ee60a496eda639d69472007fb70c47bc98beb2 Mon Sep 17 00:00:00 2001 From: soulware <81992759+soulware1@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:07:40 -0500 Subject: [PATCH 012/101] Parthenogenesis no longer makes your material fish have null weights (#96376) ## About The Pull Request Removes a check that checks if material_weight_mult is equal to 1 in fish's apply_material_effects. ## Why It's Good For The Game Fixes #96308 Also fixes #96357 ## Changelog :cl: fix: Parthenogenesis no longer makes your material fish have null weights /:cl: --- code/modules/fishing/fish/_fish.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/fishing/fish/_fish.dm b/code/modules/fishing/fish/_fish.dm index 8799735505ea..195e34f734bd 100644 --- a/code/modules/fishing/fish/_fish.dm +++ b/code/modules/fishing/fish/_fish.dm @@ -816,14 +816,14 @@ GLOBAL_LIST_INIT(fish_compatible_fluid_types, list( /obj/item/fish/apply_material_effects() . = ..() //Either effects aren't applied of he materials are simply being increased/decreased along with the weight. Avoids recursion. - if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) || material_weight_mult == 1) + if(!(material_flags & MATERIAL_EFFECTS) || (fish_flags & FISH_FLAG_UPDATING_SIZE_AND_WEIGHT) ) return maximum_weight *= material_weight_mult update_size_and_weight(size, (temp_weight || weight) * material_weight_mult, update_materials = FALSE) /obj/item/fish/remove_material_effects(replace_mats = TRUE) . = ..() - if(replace_mats || !(material_flags & MATERIAL_EFFECTS) || material_weight_mult == 1) + if(replace_mats || !(material_flags & MATERIAL_EFFECTS) ) return maximum_weight /= material_weight_mult update_size_and_weight(size, weight / material_weight_mult) From 2e3bbb14854fc42acb0f1cbe5e2197daecf4d7e1 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:07:59 +0000 Subject: [PATCH 013/101] Automatic changelog for PR #96376 [ci skip] --- html/changelogs/AutoChangeLog-pr-96376.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96376.yml diff --git a/html/changelogs/AutoChangeLog-pr-96376.yml b/html/changelogs/AutoChangeLog-pr-96376.yml new file mode 100644 index 000000000000..b1dafaa621c9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96376.yml @@ -0,0 +1,4 @@ +author: "soulware1" +delete-after: True +changes: + - bugfix: "Parthenogenesis no longer makes your material fish have null weights" \ No newline at end of file From feb999ad2533c482d2df3810fc219b352c16b2d6 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:08:42 -0400 Subject: [PATCH 014/101] Swap light/dark mode colors for span_green (#96378) ## About The Pull Request The light mode colors for `span_green` are bright and hard to read, whereas the dark mode ones are weirdly dark, so I swapped them. image ## Why It's Good For The Game Improve text contrast in light mode ## Changelog idk --- tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss | 2 +- tgui/packages/tgui-panel/styles/tgchat/chat-light.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index e12b3f6cdf65..8bbc44edfd1d 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -567,7 +567,7 @@ em { } .green { - color: hsl(132.8, 93.4%, 29.6%); + color: hsl(132.9, 100%, 50.6%); } .grey { diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index efaeb41b6a50..ea2b3aaabc48 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -585,7 +585,7 @@ h2.alert { } .green { - color: hsl(132.9, 100%, 50.6%); + color: hsl(132.8, 93.4%, 29.6%); } .grey { From f8d6a01a37fe3cc49d95e96f553c483b0daa6030 Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:44:45 -0400 Subject: [PATCH 015/101] Pinpointer tweaks and ventpointer buff. (#96377) ## About The Pull Request Pretty simple one this time. Cleans up pinpointer code, previously the ranges at which pinpointer arrows determined how close you were to an object were hardcoded numbers, now I've moved those numbers over to a set of variables so you can determine what's close/medium/far. Autodocs pinpointer variables and subtype variables as well. Removed a functionally useless variable off of crew pinpointers, moving from a boolean-`has_owner` to just checking against the assigned `pinpointer_owner` mob. Lastly, tweaked some of the numbers on the ventpointer, a pinpointer purchasable by shaft miners for points. Namely, I've lowered it's minimum range to 8 from 14, meaning that you can get closer before it says that you've arrived at your vent, as before you could still be completely off screen and be unable to find the vent. In addition, I adjusted the close and medium range variables to be wider than the standard range, since in practice using the ventpointer in-round tends to just show a red arrow for the vast majority of the time you're using it. ## Why It's Good For The Game Pinpointer code improvements were mostly because I was in the area and in theory, pinpointer code should be more flexible considering how much of the game's core game-modes rely on pinpointers to function properly. On ventpointers, these things are clearly meant to be somewhat inaccurate, but due to having the distance values on pinpointers hardcoded, It was previously impossible to ever even reach close range on the vent before it claimed you had arrived. I've lowered the close range value to be in the ballpark of a single screenwidth of tiles in all directions, and adjusted the other colors so that you can get a better idea if you've switched vent targets. As a result, the ventpointer should be a more valuable investment as a direct result. ## Changelog :cl: balance: The mining ventpointer has been adjusted so that it can now more accurately show the distance to the nearest vent, and will now claim you've arrived if you're within 8 tiles away from a vent, down from 14. code: Several backend improvements to the pinpointer functionality and adjustability. /:cl: --- code/game/objects/items/pinpointer.dm | 48 +++++++++++++------ .../traitor/contractor/contractor_items.dm | 1 - code/modules/mining/equipment/vent_pointer.dm | 4 +- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/code/game/objects/items/pinpointer.dm b/code/game/objects/items/pinpointer.dm index 2e23ffdfbb77..db79f9388df7 100644 --- a/code/game/objects/items/pinpointer.dm +++ b/code/game/objects/items/pinpointer.dm @@ -17,12 +17,23 @@ sound_vary = TRUE pickup_sound = SFX_GENERIC_DEVICE_PICKUP drop_sound = SFX_GENERIC_DEVICE_DROP + /// Is the pinpointer on? var/active = FALSE - var/atom/movable/target //The thing we're searching for - var/minimum_range = 0 //at what range the pinpointer declares you to be at your destination - var/alert = FALSE // TRUE to display things more seriously - var/process_scan = TRUE // some pinpointers change target every time they scan, which means we can't have it change very process but instead when it turns on. - var/icon_suffix = "" // for special pinpointer icons + ///The thing we're searching for + var/atom/movable/target + /// TRUE to display things more seriously + var/alert = FALSE + /// Some pinpointers change target every time they scan, which means we can't have it change every process() but instead when it turns on. + var/process_scan = TRUE + /// Icon_state suffix for special pinpointer icons + var/icon_suffix = "" + + /// At what range the pinpointer declares you to be at your destination. Use to hide the exact location of your target. + var/minimum_range = 0 + /// From 1 to this value, the sprite will display as though you're close. + var/close_range = 8 + /// From close_range + 1 to this value, the sprite will display as though you're medium distance away. Past this value, we'll display as though you're far. + var/medium_range = 16 /obj/item/pinpointer/Initialize(mapload) . = ..() @@ -85,13 +96,13 @@ return "pinon[alert ? "alert" : ""]direct[icon_suffix]" else setDir(get_dir(here, there)) - switch(get_dist(here, there)) - if(1 to 8) - return "pinon[alert ? "alert" : "close"][icon_suffix]" - if(9 to 16) - return "pinon[alert ? "alert" : "medium"][icon_suffix]" - if(16 to INFINITY) - return "pinon[alert ? "alert" : "far"][icon_suffix]" + var/current_distance = get_dist(here, there) + if(current_distance >= 1 && current_distance <= close_range) + return "pinon[alert ? "alert" : "close"][icon_suffix]" + else if(current_distance > (close_range + 1) && current_distance <= medium_range) + return "pinon[alert ? "alert" : "medium"][icon_suffix]" + else if(current_distance > medium_range) + return "pinon[alert ? "alert" : "far"][icon_suffix]" /obj/item/pinpointer/crew // A replacement for the old crew monitoring consoles name = "crew pinpointer" @@ -100,9 +111,14 @@ worn_icon_state = "pinpointer_crew" custom_price = PAYCHECK_CREW * 6 custom_premium_price = PAYCHECK_CREW * 6 - var/has_owner = FALSE + /// The mob that the pinpointer is owned by. var/pinpointer_owner = null - var/ignore_suit_sensor_level = FALSE /// Do we find people even if their suit sensors are turned off + /// Do we find people even if their suit sensors are turned off + var/ignore_suit_sensor_level = FALSE + +/obj/item/pinpointer/crew/Destroy() + . = ..() + pinpointer_owner = null /obj/item/pinpointer/crew/proc/trackable(mob/living/carbon/human/H) var/turf/here = get_turf(src) @@ -120,7 +136,7 @@ user.visible_message(span_notice("[user] deactivates [user.p_their()] pinpointer."), span_notice("You deactivate your pinpointer.")) return - if (has_owner && !pinpointer_owner) + if (!pinpointer_owner) pinpointer_owner = user if (pinpointer_owner && pinpointer_owner != user) @@ -173,6 +189,7 @@ /obj/item/pinpointer/pair name = "pair pinpointer" desc = "A handheld tracking device that locks onto its other half of the matching pair." + /// Reference to the other, specific pinpointer that it's bought with. Assigned on /obj/item/storage/box/pinpointer_pairs. var/other_pair /obj/item/pinpointer/pair/Destroy() @@ -197,6 +214,7 @@ icon_state = "pinpointer_hunter" worn_icon_state = "pinpointer_black" icon_suffix = "_hunter" + /// Reference to the bounty hunter shuttle's docking port. var/obj/docking_port/mobile/shuttleport /obj/item/pinpointer/shuttle/Initialize(mapload) diff --git a/code/modules/antagonists/traitor/contractor/contractor_items.dm b/code/modules/antagonists/traitor/contractor/contractor_items.dm index 86bbaf4168c5..7375660f969d 100644 --- a/code/modules/antagonists/traitor/contractor/contractor_items.dm +++ b/code/modules/antagonists/traitor/contractor/contractor_items.dm @@ -4,7 +4,6 @@ icon_state = "pinpointer_syndicate" worn_icon_state = "pinpointer_black" minimum_range = 25 - has_owner = TRUE ignore_suit_sensor_level = TRUE /obj/item/paper/contractor_guide diff --git a/code/modules/mining/equipment/vent_pointer.dm b/code/modules/mining/equipment/vent_pointer.dm index 4edab185597b..5c276b746ee0 100644 --- a/code/modules/mining/equipment/vent_pointer.dm +++ b/code/modules/mining/equipment/vent_pointer.dm @@ -2,7 +2,9 @@ name = "ventpointer" desc = "A handheld tracking device. It will locate and point to nearby vents. A bit unreliable though." icon_state = "pinpointer_vent" - minimum_range = 14 //gotta use them eyes + minimum_range = 8 //gotta use them eyes + close_range = 12 + medium_range = 20 /obj/item/pinpointer/vent/scan_for_target() var/closest_dist = INFINITY From 929eb5652ff42f7e0bcaf94173743ee2c82723d4 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:45:05 +0000 Subject: [PATCH 016/101] Automatic changelog for PR #96377 [ci skip] --- html/changelogs/AutoChangeLog-pr-96377.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96377.yml diff --git a/html/changelogs/AutoChangeLog-pr-96377.yml b/html/changelogs/AutoChangeLog-pr-96377.yml new file mode 100644 index 000000000000..099580992753 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96377.yml @@ -0,0 +1,5 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - balance: "The mining ventpointer has been adjusted so that it can now more accurately show the distance to the nearest vent, and will now claim you've arrived if you're within 8 tiles away from a vent, down from 14." + - code_imp: "Several backend improvements to the pinpointer functionality and adjustability." \ No newline at end of file From cd11a9ad000a8f7efcf9099e49e4a30c1af082f4 Mon Sep 17 00:00:00 2001 From: CabinetOnFire Date: Sun, 7 Jun 2026 17:54:50 +0200 Subject: [PATCH 017/101] Makes playsound respect max_range and makes sound use euclidean distance (#96369) ## About The Pull Request This PR makes a few changes: 1. It makes audio respect max_range correctly. Due to the way we used fall-off before, we would often get sounds that ended up going below 3 volume, due to an earlier PR, we made any sound below 3 volume not play. This meant that if you had a sound wiht a max_range of 17, it would not even play beyond 15~ range. This change makes it so there's a min volume of 3 (changeable per playsound use, but I havnt changed the default anywhere), and falloff caps at this volume. This makes it so we always play at max_range still. I reduced default sound range to 15 to compensate for this, which is roughly what the distance would have been in the old system. The sound curve now looks like this: image The second change is that instead of using chebyshev distance, we now use euclidean distance to determine the volume of sound. The reason for this is that chebyshev distance does not respect diagonals, which means a lot of tiles are treated as the same distance which makes them sound the exact same too. image With Euclidean distance, we use the literal distance for diagonals, which results in much more accurate falloff, and means you notice falloff much better when walking past something image BEFORE: https://github.com/user-attachments/assets/1c327557-283c-49fc-9231-8425a29f7a83 AFTER: https://github.com/user-attachments/assets/e7f88409-c2fc-4ba8-8dd9-3e23b2eec5c2 ## Why It's Good For The Game makes falloff more accurate to your position, which to me sounds better. also it makes max_range actually do what it says; be the range at which you can hear the sound. ## Changelog :cl: CabinetOnFire sound: Sound now makes use of euclidean distance for falloff code: max_range on playsound now ensures the sound is audible up to that range. /:cl: --- code/__DEFINES/sound.dm | 38 ++++--------------- .../datums/looping_sounds/machinery_sounds.dm | 2 +- code/datums/sound_token.dm | 4 +- code/game/sound/sound.dm | 37 ++++++++---------- 4 files changed, 26 insertions(+), 55 deletions(-) diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index ce7c9cfeac4d..5a89def0f73e 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -18,33 +18,9 @@ #define MAX_INSTRUMENT_CHANNELS (128 * 6) -/// This is the lowest volume that can be used by playsound otherwise it gets ignored -/// Most sounds around 10 volume can barely be heard. Almost all sounds at 5 volume or below are inaudible -/// This is to prevent sound being spammed at really low volumes due to distance calculations -/// Recommend setting this to anywhere from 10-3 (or 0 to disable any sound minimum volume restrictions) -/// Ex. For a 70 volume sound, 17 tile range, 3 exponent, 2 falloff_distance: -/// Setting SOUND_AUDIBLE_VOLUME_MIN to 0 for the above will result in 17x17 radius (289 turfs) -/// Setting SOUND_AUDIBLE_VOLUME_MIN to 5 for the above will result in 14x14 radius (196 turfs) -/// Setting SOUND_AUDIBLE_VOLUME_MIN to 10 for the above will result in 11x11 radius (121 turfs) -#define SOUND_AUDIBLE_VOLUME_MIN 3 - -/* Calculates the max distance of a sound based on audible volume - * - * Note - you should NEVER pass in a volume that is lower than SOUND_AUDIBLE_VOLUME_MIN otherwise distance will be insanely large (like +250,000) - * - * Arguments: - * * volume: The initial volume of the sound being played - * * max_distance: The range of the sound in tiles (technically not real max distance since the furthest areas gets pruned due to SOUND_AUDIBLE_VOLUME_MIN) - * * falloff_distance: Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. - * * falloff_exponent: Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. - * Returns: The max distance of a sound based on audible volume range - */ -#define CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(volume, max_distance, falloff_distance, falloff_exponent)\ - floor(((((-(max(max_distance - falloff_distance, 0) ** (1 / falloff_exponent)) / volume) * (SOUND_AUDIBLE_VOLUME_MIN - volume)) ** falloff_exponent) + falloff_distance)) - /* Calculates the volume of a sound based on distance * - * https://www.desmos.com/calculator/shjpmz3ck7 + * https://www.desmos.com/calculator/ing6lxgd0m Update this when changed please * * Arguments: * * volume: The initial volume of the sound being played @@ -53,16 +29,16 @@ * * falloff_exponent: Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. * Returns: The max distance of a sound based on audible volume range */ -#define CALCULATE_SOUND_VOLUME(volume, distance, max_distance, falloff_distance, falloff_exponent)\ - ((max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * volume) +#define CALCULATE_SOUND_VOLUME_RATIO(volume, distance, max_distance, falloff_distance, falloff_exponent)\ + ((max(distance - falloff_distance, 0) / (max(max_distance, distance) - falloff_distance)) ** (1 / falloff_exponent)) ///Default range of a sound. -#define SOUND_RANGE 17 -#define MEDIUM_RANGE_SOUND_EXTRARANGE -5 +#define SOUND_RANGE 15 +#define MEDIUM_RANGE_SOUND_EXTRARANGE -3 ///default extra range for sounds considered to be quieter -#define SHORT_RANGE_SOUND_EXTRARANGE -9 +#define SHORT_RANGE_SOUND_EXTRARANGE -7 ///The range deducted from sound range for things that are considered silent / sneaky -#define SILENCED_SOUND_EXTRARANGE -11 +#define SILENCED_SOUND_EXTRARANGE -10 ///Percentage of sound's range where no falloff is applied #define SOUND_DEFAULT_FALLOFF_DISTANCE 0 //Disabled because it doesn't actually have a nice effect, it just makes the jump to fall-off more shocking. maybe delete ///The default exponent of sound falloff diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 9ab93ae378e8..a8549ab1418b 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -140,7 +140,7 @@ mid_length = 1.8 SECONDS end_sound = 'sound/machines/computer/computer_end.ogg' end_volume = 1 SECONDS - volume = SOUND_AUDIBLE_VOLUME_MIN + volume = 3 falloff_exponent = 4 //Ultra quiet very fast extra_range = -12 falloff_distance = 0 //Instant falloff after initial tile diff --git a/code/datums/sound_token.dm b/code/datums/sound_token.dm index d9ad9903c283..93fdfb8213bb 100644 --- a/code/datums/sound_token.dm +++ b/code/datums/sound_token.dm @@ -123,7 +123,7 @@ if(source_turf.z != listener_turf.z) should_be_muted = TRUE - var/distance = get_dist(source_turf, listener_turf) + var/distance = get_dist_euclidean(source_turf, listener_turf) if(distance > range) should_be_muted = TRUE if(should_be_muted && is_muted) @@ -195,7 +195,7 @@ return if(player_turf.z != source_turf.z) return - if(get_dist(source_turf, player_turf) > range) + if(get_dist_euclidean(source_turf, player_turf) > range) return add_or_update_listener(player) diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm index 92b2e6f19e9b..6f1423dd9afe 100644 --- a/code/game/sound/sound.dm +++ b/code/game/sound/sound.dm @@ -14,8 +14,9 @@ * * ignore_walls - Whether or not the sound can pass through walls. * * falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. * * volume_preference - Optional: Will be checked to modify the volume of the sound for each listener. + * * min_volume - minimum volume the sound can reach at max_range. */ -/proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null) +/proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null, min_volume = 3) if(isarea(source)) CRASH("playsound(): source is an area") @@ -25,8 +26,6 @@ if(!soundin) CRASH("playsound(): no soundin passed") - if(vol < SOUND_AUDIBLE_VOLUME_MIN) // never let sound go below SOUND_AUDIBLE_VOLUME_MIN or bad things will happen - CRASH("playsound(): volume below SOUND_AUDIBLE_VOLUME_MIN. [vol] < [SOUND_AUDIBLE_VOLUME_MIN]") var/turf/turf_source = get_turf(source) if (!turf_source) @@ -50,30 +49,28 @@ var/turf/above_turf = GET_TURF_ABOVE(turf_source) var/turf/below_turf = GET_TURF_BELOW(turf_source) - var/audible_distance = CALCULATE_MAX_SOUND_AUDIBLE_DISTANCE(vol, maxdistance, falloff_distance, falloff_exponent) - if(ignore_walls) - listeners = get_hearers_in_range(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners = get_hearers_in_range(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) if(above_turf && istransparentturf(above_turf)) - listeners += get_hearers_in_range(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners += get_hearers_in_range(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) if(below_turf && istransparentturf(turf_source)) - listeners += get_hearers_in_range(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners += get_hearers_in_range(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) else //these sounds don't carry through walls - listeners = get_hearers_in_view(audible_distance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners = get_hearers_in_view(maxdistance, turf_source, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) if(above_turf && istransparentturf(above_turf)) - listeners += get_hearers_in_view(audible_distance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners += get_hearers_in_view(maxdistance, above_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) if(below_turf && istransparentturf(turf_source)) - listeners += get_hearers_in_view(audible_distance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) + listeners += get_hearers_in_view(maxdistance, below_turf, RECURSIVE_CONTENTS_CLIENT_MOBS, TRUE) for(var/mob/listening_ghost as anything in SSmobs.dead_players_by_zlevel[source_z]) - if(get_dist(listening_ghost, turf_source) <= audible_distance) - listeners += listening_ghost + listeners += listening_ghost for(var/mob/listening_mob in listeners)//had nulls sneak in here, hence the typecheck - listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference) + if(get_dist_euclidean(listening_mob, turf_source) <= maxdistance) + listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb, volume_preference) return listeners @@ -96,8 +93,9 @@ * * distance_multiplier - Default 1, multiplies the maximum distance of our sound * * use_reverb - bool default TRUE, determines if our sound has reverb * * volume_preference - Optional: Will be checked to modify the volume of the sound. + * * min_volume - minimum volume the sound can reach at max_range. */ -/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null) +/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE, datum/preference/numeric/volume/volume_preference = null, min_volume = 5) if(!client || HAS_TRAIT(src, TRAIT_DEAF)) return @@ -120,10 +118,10 @@ var/turf/turf_loc = get_turf(src) //sound volume falloff with distance - distance = get_dist(turf_loc, turf_source) * distance_multiplier + distance = get_dist_euclidean(turf_loc, turf_source) * distance_multiplier if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff. - sound_to_use.volume -= CALCULATE_SOUND_VOLUME(vol, distance, max_distance, falloff_distance, falloff_exponent) + sound_to_use.volume -= CALCULATE_SOUND_VOLUME_RATIO(vol, distance, max_distance, falloff_distance, falloff_exponent) * (vol - min_volume) if(pressure_affected) //Atmosphere affects sound @@ -144,9 +142,6 @@ sound_to_use.volume *= pressure_factor //End Atmosphere affecting sound - if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN) - return FALSE - var/dx = turf_source.x - turf_loc.x // Hearing from the right/left sound_to_use.x = dx * distance_multiplier var/dz = turf_source.y - turf_loc.y // Hearing from infront/behind @@ -174,7 +169,7 @@ if(ispath(volume_preference) && client.prefs) var/client_volume_modifier = client.prefs.read_preference(volume_preference) sound_to_use.volume *= (client_volume_modifier / 100) - if(sound_to_use.volume < SOUND_AUDIBLE_VOLUME_MIN) + if(sound_to_use.volume < 0.1) return FALSE if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED)) From 8844838dcd7b830af7f26df7fcbe006e2a3f39ea Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:55:08 +0000 Subject: [PATCH 018/101] Automatic changelog for PR #96369 [ci skip] --- html/changelogs/AutoChangeLog-pr-96369.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96369.yml diff --git a/html/changelogs/AutoChangeLog-pr-96369.yml b/html/changelogs/AutoChangeLog-pr-96369.yml new file mode 100644 index 000000000000..bf3fae567b49 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96369.yml @@ -0,0 +1,5 @@ +author: "CabinetOnFire" +delete-after: True +changes: + - sound: "Sound now makes use of euclidean distance for falloff" + - code_imp: "max_range on playsound now ensures the sound is audible up to that range." \ No newline at end of file From f7ed9387eae02a28bd35243880d088bbc67646cf Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:00:28 +0000 Subject: [PATCH 019/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96369.yml | 5 ----- html/changelogs/AutoChangeLog-pr-96375.yml | 4 ---- html/changelogs/AutoChangeLog-pr-96376.yml | 4 ---- html/changelogs/AutoChangeLog-pr-96377.yml | 5 ----- html/changelogs/archive/2026-06.yml | 11 +++++++++++ 5 files changed, 11 insertions(+), 18 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96369.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96375.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96376.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96377.yml diff --git a/html/changelogs/AutoChangeLog-pr-96369.yml b/html/changelogs/AutoChangeLog-pr-96369.yml deleted file mode 100644 index bf3fae567b49..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96369.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "CabinetOnFire" -delete-after: True -changes: - - sound: "Sound now makes use of euclidean distance for falloff" - - code_imp: "max_range on playsound now ensures the sound is audible up to that range." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96375.yml b/html/changelogs/AutoChangeLog-pr-96375.yml deleted file mode 100644 index 84e3a8b850cd..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96375.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - image: "Made volcanic pores more noticeable" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96376.yml b/html/changelogs/AutoChangeLog-pr-96376.yml deleted file mode 100644 index b1dafaa621c9..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96376.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "soulware1" -delete-after: True -changes: - - bugfix: "Parthenogenesis no longer makes your material fish have null weights" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96377.yml b/html/changelogs/AutoChangeLog-pr-96377.yml deleted file mode 100644 index 099580992753..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96377.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "ArcaneMusic" -delete-after: True -changes: - - balance: "The mining ventpointer has been adjusted so that it can now more accurately show the distance to the nearest vent, and will now claim you've arrived if you're within 8 tiles away from a vent, down from 14." - - code_imp: "Several backend improvements to the pinpointer functionality and adjustability." \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 165386745747..4771e5786d4f 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -176,10 +176,21 @@ lelandkemble: - bugfix: Pinpointers stop tracking a shunted ai when that ai is no longer shunted 2026-06-07: + ArcaneMusic: + - balance: The mining ventpointer has been adjusted so that it can now more accurately + show the distance to the nearest vent, and will now claim you've arrived if + you're within 8 tiles away from a vent, down from 14. + - code_imp: Several backend improvements to the pinpointer functionality and adjustability. + CabinetOnFire: + - sound: Sound now makes use of euclidean distance for falloff + - code_imp: max_range on playsound now ensures the sound is audible up to that range. SmArtKar: - bugfix: Fixed an issue where Stop-All-Active-Weather verb would not remove ash storm sounds - refactor: Rain and ash storms have been refactored to use particles rather than overlays + - image: Made volcanic pores more noticeable UvvU: - bugfix: fixed improper chat caching. + soulware1: + - bugfix: Parthenogenesis no longer makes your material fish have null weights From cdd5bee244fcc2f11c974a7e4b8ccdeca00767a7 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:12:58 -0500 Subject: [PATCH 020/101] Fix disease copies not registering relevant disease signals (#96379) ## About The Pull Request `/datum/disease/proc/infect(mob/living/infectee, make_copy = TRUE)` does NOT always represent the disease infecting someone. If you pass `make_copy = TRUE`, the proc actually handles the *copy* infecting the mob, rather than the disease itself. (The issue here arises from `register_disease_signals` here being called on the disease itself rather than the potential copy. ) This itself could(should) easily be fixed, by making a copy before going into `infect`, but this is an easy fix in the meanwhile. Fixes #96372 ## Changelog :cl: Melbert fix: Fixed a bug where diseases wouldn't trigger their regular effects when applied in certain contexts fix: Fixed a bug where airborne diseases wouldn't spread via breathing when applied in certain contexts /:cl: --- code/datums/diseases/_disease.dm | 2 +- code/datums/quirks/negative_quirks/chronic_illness.dm | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm index e8054369d88e..62f545e6ee33 100644 --- a/code/datums/diseases/_disease.dm +++ b/code/datums/diseases/_disease.dm @@ -77,8 +77,8 @@ SSdisease.active_diseases += D //Add it to the active diseases list, now that it's actually in a mob and being processed. D.after_add() + D.register_disease_signals() infectee.med_hud_set_status() - register_disease_signals() var/turf/source_turf = get_turf(infectee) log_virus("[key_name(infectee)] was infected by virus: [src.admin_details()] at [loc_name(source_turf)]") diff --git a/code/datums/quirks/negative_quirks/chronic_illness.dm b/code/datums/quirks/negative_quirks/chronic_illness.dm index 49438160c489..a96c787e37dc 100644 --- a/code/datums/quirks/negative_quirks/chronic_illness.dm +++ b/code/datums/quirks/negative_quirks/chronic_illness.dm @@ -10,8 +10,8 @@ mail_goodies = list(/obj/item/storage/pill_bottle/sansufentanyl) /datum/quirk/item_quirk/chronic_illness/add(client/client_source) - var/datum/disease/chronic_illness/hms = new /datum/disease/chronic_illness() - quirk_holder.ForceContractDisease(hms) + var/datum/disease/chronic_illness/hms = new() + quirk_holder.ForceContractDisease(hms, make_copy = FALSE, del_on_fail = TRUE) /datum/quirk/item_quirk/chronic_illness/add_unique(client/client_source) give_item_to_holder(/obj/item/storage/pill_bottle/sansufentanyl, list(LOCATION_BACKPACK), flavour_text = "You've been provided with medication to help manage your condition. Take it regularly to avoid complications.", notify_player = TRUE) From 4f21b59f9421b078a20df6ce778c01d8e5277424 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:13:18 +0000 Subject: [PATCH 021/101] Automatic changelog for PR #96379 [ci skip] --- html/changelogs/AutoChangeLog-pr-96379.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96379.yml diff --git a/html/changelogs/AutoChangeLog-pr-96379.yml b/html/changelogs/AutoChangeLog-pr-96379.yml new file mode 100644 index 000000000000..984f30726c24 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96379.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "Fixed a bug where diseases wouldn't trigger their regular effects when applied in certain contexts" + - bugfix: "Fixed a bug where airborne diseases wouldn't spread via breathing when applied in certain contexts" \ No newline at end of file From a4111e0581449f5d5cd2a8b00d0f7d09aed9ad33 Mon Sep 17 00:00:00 2001 From: EnterTheJake <102721711+EnterTheJake@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:37:13 +0200 Subject: [PATCH 022/101] Fix antag teams not clearing refs from objective properly when deleted (#96381) ## About The Pull Request Whenever an antag team is deleted, it wouldn't properly clear the `team` var from its objectives - resulting in a hard delete. ## Why It's Good For The Game Fixes a hard delete. ## Changelog :cl: /:cl: --- code/modules/antagonists/_common/antag_team.dm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm index 527196c51c3e..178ec2d59a16 100644 --- a/code/modules/antagonists/_common/antag_team.dm +++ b/code/modules/antagonists/_common/antag_team.dm @@ -29,6 +29,9 @@ GLOBAL_LIST_EMPTY(antagonist_teams) /datum/team/Destroy(force) GLOB.antagonist_teams -= src members = null + for(var/datum/objective/objective as anything in objectives) + if(objective.team == src) + objective.team = null objectives = null return ..() From e843f46d1f42a69db4331a05e03628447f8c0f51 Mon Sep 17 00:00:00 2001 From: soulware <81992759+soulware1@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:52:18 -0500 Subject: [PATCH 023/101] Move carving block to basic recipes (Allows carving blocks to be made out of any material) (#96347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## About The Pull Request Allow carving blocks to be made by any material, instead of just rigid materials. ## Why It's Good For The Game pride This allows for the construction of Ice Statues (Hot Ice) Beige Statues (Sand + Sandstone) Snowpeople (Snow) Origami Statues (Paper) Cardboard (Cardboard) REAL ghosts (Hauntium) Meat Statue... 🤤🤤🤤🤤 (Meat) (Not pictured) Pizzer Statue (Pizza) Also more player expression + if you could make chairs and airlocks and toilets and sinks and all that other shit out of these, why the hell NOT a carving block!? ## Changelog :cl: balance: Carving blocks can be made out of any material now! /:cl: --- code/__DEFINES/construction/material.dm | 2 +- code/controllers/subsystem/materials.dm | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/code/__DEFINES/construction/material.dm b/code/__DEFINES/construction/material.dm index 59a7b1b74ec2..413de505765d 100644 --- a/code/__DEFINES/construction/material.dm +++ b/code/__DEFINES/construction/material.dm @@ -26,7 +26,7 @@ #define MATERIAL_SILO_STORED (1 << 0) /// This material can be used in basic recipes, such as chairs/toilets/tiles #define MATERIAL_BASIC_RECIPES (1 << 1) -/// This material counts as a rigid enough solid to be used to craft tough objects like carving blocks or air tanks +/// This material counts as a rigid enough solid to be used to craft tough objects like air tanks #define MATERIAL_CLASS_RIGID (1 << 2) /// The opposite of rigid, this means that the material cannot hold a solid form (like sand) and cannot be used in item crafting #define MATERIAL_CLASS_AMORPHOUS (1 << 3) diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm index b4bf65a501c1..d30d3aab73e4 100644 --- a/code/controllers/subsystem/materials.dm +++ b/code/controllers/subsystem/materials.dm @@ -23,10 +23,11 @@ SUBSYSTEM_DEF(materials) new /datum/stack_recipe("Material floor tile", /obj/item/stack/tile/material, 1, 4, 20, crafting_flags = CRAFT_SKIP_MATERIALS_PARITY, category = CAT_TILES), new /datum/stack_recipe("Material airlock assembly", /obj/structure/door_assembly/door_assembly_material, 4, time = 5 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_DOORS), new /datum/stack_recipe("Material platform", /obj/structure/platform/material, 2, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, trait_booster = TRAIT_QUICK_BUILD, trait_modifier = 0.75, category = CAT_STRUCTURE), \ + new /datum/stack_recipe("Carving block", /obj/structure/carving_block, 5, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_STRUCTURE), ) - /// List of stackcrafting recipes for materials using rigid recipes + /// List of stackcrafting recipes for materials using rigid recipes (none yet) var/list/rigid_stack_recipes = list( - new /datum/stack_recipe("Carving block", /obj/structure/carving_block, 5, time = 3 SECONDS, crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_ONE_PER_TURF | CRAFT_ON_SOLID_GROUND | CRAFT_SKIP_MATERIALS_PARITY, category = CAT_STRUCTURE), + ) /// A list of dimensional themes used by the dimensional anomaly and other things, most of which require materials to function. From a14ff0658277c94df5186be03bdd4840eddf310c Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:52:36 +0000 Subject: [PATCH 024/101] Automatic changelog for PR #96347 [ci skip] --- html/changelogs/AutoChangeLog-pr-96347.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96347.yml diff --git a/html/changelogs/AutoChangeLog-pr-96347.yml b/html/changelogs/AutoChangeLog-pr-96347.yml new file mode 100644 index 000000000000..069263d72712 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96347.yml @@ -0,0 +1,4 @@ +author: "soulware1" +delete-after: True +changes: + - balance: "Carving blocks can be made out of any material now!" \ No newline at end of file From e2c9516687e75917b188af9303ac5f776f28d32f Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:47:57 -0700 Subject: [PATCH 025/101] Extends CI's unit tests timeout to 55 minutes (#96365) ## About The Pull Request We allow C&D to run at most 50 minutes as its hard timeout, so we should have a matching timeout on the tests. I'm open to lowering both of these, I do think 15 is far too small in the case of hard deletes however, you want to be able to track those down effectively and searching can take a while. --- .github/workflows/run_integration_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_integration_tests.yml b/.github/workflows/run_integration_tests.yml index 89f6cd236769..a3975538817a 100644 --- a/.github/workflows/run_integration_tests.yml +++ b/.github/workflows/run_integration_tests.yml @@ -27,7 +27,7 @@ jobs: # For example, `Run Tests (runtimestation; 515)`. name: Run Tests (${{ inputs.major && format('{0}.{1}; ', inputs.major, inputs.minor) || '' }}${{ inputs.map }}; ${{ inputs.max_required_byond_client }}) runs-on: ubuntu-24.04 - timeout-minutes: 15 + timeout-minutes: 55 steps: - uses: actions/checkout@v6 - name: Restore BYOND from Cache From ce3a7c075645418c06d3fa2e9019df110c4e596a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:00:22 +0000 Subject: [PATCH 026/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96347.yml | 4 ---- html/changelogs/AutoChangeLog-pr-96379.yml | 5 ----- html/changelogs/archive/2026-06.yml | 8 ++++++++ 3 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96347.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96379.yml diff --git a/html/changelogs/AutoChangeLog-pr-96347.yml b/html/changelogs/AutoChangeLog-pr-96347.yml deleted file mode 100644 index 069263d72712..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96347.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "soulware1" -delete-after: True -changes: - - balance: "Carving blocks can be made out of any material now!" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96379.yml b/html/changelogs/AutoChangeLog-pr-96379.yml deleted file mode 100644 index 984f30726c24..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96379.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Fixed a bug where diseases wouldn't trigger their regular effects when applied in certain contexts" - - bugfix: "Fixed a bug where airborne diseases wouldn't spread via breathing when applied in certain contexts" \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 4771e5786d4f..3c5c8d141b6e 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -194,3 +194,11 @@ - bugfix: fixed improper chat caching. soulware1: - bugfix: Parthenogenesis no longer makes your material fish have null weights +2026-06-08: + Melbert: + - bugfix: Fixed a bug where diseases wouldn't trigger their regular effects when + applied in certain contexts + - bugfix: Fixed a bug where airborne diseases wouldn't spread via breathing when + applied in certain contexts + soulware1: + - balance: Carving blocks can be made out of any material now! From 818008bca0f28dff511fc2587d882bdcb9002ef2 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:36:44 -0700 Subject: [PATCH 027/101] Fixes process_cell runtimes during lazyloading (#96367) ## About The Pull Request I normally really don't like doing this but because this is so in the weeds this is an alt pr. Alt of #96366, I really don't want to add overhead to process_cell(). The issue here is when we request a reservation the turfs are empty()'d which clears them out and places them in SSair's adjacent_turf recalc queue (which will eventually cause an activation to clear out excited groups). We then immediately start maploading, which creates new, uninitialized turfs (that remain in the queue because turf refs are based off position instead of the actual datum). These unintialized turfs are able to interact with the other, yet to be new()'d over turfs in the recalc queue. This causes runtimes. Later, while we initialize the turfs we loaded, we also have interactions between initialized and uninitialized turfs, which also causes runtimes. The solution here is to prevent atmos processing in a loading template. I do that here by first clearing out all relevant turfs from atmos_adjacent_turf lists and deactivating them. We also have to clear them from the recalc queue to prevent "fixing" this problem. Then, after initialize is complete, we requeue them for recalcing, to restore them to a workable state. The underlying problem here is the logic of maploading is kind of designed to run before everything else has initialized. It's something we can dodge, but it will also show up in things that start processing on initialize and expect their turf to immediately be ready for them. S a bit of a mess. Of note, it's technically possible for a turf to be initialized, activate, and then attempt to process with an unitialized neighbor with this solution. My gut fix for this would be using blocks_air to prevent adjacent turfs from being recalculated until after InitializeAtoms is finished, but that breaks turf/open/Initialize where we setup gasmixtures, and I don't want to duplicate that code. So we'll just bite the small risk of runtimes, they won't actually break anything after all just make a bit of noise. ## Why It's Good For The Game Closes #89649 This adds about 110ms of cost to loading the nukie station on my machine (I used line by line macros around the two for loops), which is significant but not huge, and it's cost that can be safely CHECK_TICK'd. I think the cost of actually loading outweighs that significantly enough (6.6s on my machine) that it's fine. I prefer this solution to the alternative of adding guard checks to process_cell because I do not want to add overhead to atmos code, and also I don't like the idea that we cannot reliably prevent this (shuttle code handles it effectively using blocks_air, as an example). Also, it doesn't really resolve the uninitialized bit of this problem, which isn't... good. Technically changes the math on share() too) --- code/datums/lazy_template.dm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm index d527d46b6d65..41d837993c4e 100644 --- a/code/datums/lazy_template.dm +++ b/code/datums/lazy_template.dm @@ -81,6 +81,17 @@ var/turf/bottom_left = reservation.bottom_left_turfs[z_idx] var/turf/top_right = reservation.top_right_turfs[z_idx] + // Make our turfs dead to atmos + // Cache for sonic speed + var/list/to_rebuild = SSair.adjacent_rebuild + for(var/turf/contained_turf as anything in block(bottom_left, top_right)) + SSair.remove_from_active(contained_turf) + to_rebuild -= contained_turf + for(var/turf/sub_turf as anything in contained_turf.atmos_adjacent_turfs) + sub_turf.atmos_adjacent_turfs?.Remove(contained_turf) + contained_turf.atmos_adjacent_turfs?.Cut() + CHECK_TICK + load_map( file(load_path), bottom_left.x, @@ -104,6 +115,10 @@ loaded_atom_movables |= thing SSatoms.InitializeAtoms(loaded_areas + loaded_atom_movables + loaded_turfs) + for(var/turf/turf as anything in loaded_turfs) + CALCULATE_ADJACENT_TURFS(turf, NORMAL_TURF) + CHECK_TICK + SSlighting.setup_static_lighting_if_needed(loaded_turfs) SSmachines.setup_template_powernets(loaded_cables) SSair.setup_template_machinery(loaded_atmospherics) From 8994933b626f62f714b4854c760e96ab1f3c3e2c Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 7 Jun 2026 22:31:33 -0400 Subject: [PATCH 028/101] Add a unit test for language key conflicts (#96382) ## About The Pull Request simply adds a new unit test, `language_key_conflicts`, ported from my work on monkestation. it is exactly what it says on the tin - it ensures two languages do not have the same exact key. i don't think that's an issue here, but better safe than sorry. ## Why It's Good For The Game more unit test coverage for subtle bugs that might be hard to realize is always nice ## Changelog No user-facing changes. --- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/language_key_conflicts.dm | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 code/modules/unit_tests/language_key_conflicts.dm diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 23153ef32f69..d53bd596bfad 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -212,6 +212,7 @@ #include "keybinding_init.dm" #include "kinetic_crusher.dm" #include "knockoff_component.dm" +#include "language_key_conflicts.dm" #include "language_transfer.dm" #include "leash.dm" #include "lesserform.dm" diff --git a/code/modules/unit_tests/language_key_conflicts.dm b/code/modules/unit_tests/language_key_conflicts.dm new file mode 100644 index 000000000000..8e0af55b122b --- /dev/null +++ b/code/modules/unit_tests/language_key_conflicts.dm @@ -0,0 +1,15 @@ +/// This test ensures that multiple languages aren't mapped to the same prefix key. +/datum/unit_test/language_key_conflicts + +/datum/unit_test/language_key_conflicts/Run() + var/list/used_keys = list() + for(var/datum/language/language as anything in subtypesof(/datum/language)) + var/name = language::name + var/key = language::key + if(!key) + TEST_FAIL("[name] ([language]) does not have a prefix!") + else if(used_keys[key]) + var/datum/language/conflicting_language = used_keys[key] + TEST_FAIL("[name] ([language]) uses the '[key]' prefix, which is also used by [conflicting_language::name] ([conflicting_language])!") + else + used_keys[key] = language From a7376598997732f18ab472d3585a74b79d1e5d43 Mon Sep 17 00:00:00 2001 From: mcbalaam <104003807+mcbalaam@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:36:43 +0700 Subject: [PATCH 029/101] feat: A Plastic World (plastic floral decorations) (#96350) ## About The Pull Request Adds the Rapid Decoration Device and plastic plants and stones!
image image image
image image

Plastic plants are very cheap to make but at the same time are very fragile (20 integrity points). They cannot be deconstructed by wrenching (only by destroying them or using a RDD to suck them in) and yield no resources.

The RDD is available in the service protolathe roundstart.

image ## Why It's Good For The Game RDD is a great way to decorate the station or your passion projects without having to hunt down and dig out decorative flora piece by piece. I think this will also pair greatly with https://github.com/tgstation/tgstation/pull/96345 ## Changelog :cl: add: Added fake plastic plants to be used for decoration add: RDD: Rapid Decoration Device is now available for the service department /:cl: --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> --- code/game/objects/items/rcd/RDD.dm | 344 ++++++++++ code/game/objects/items/rcd/RHD.dm | 6 +- code/game/objects/structures/decorations.dm | 609 ++++++++++++++++++ code/modules/asset_cache/assets/rdd.dm | 11 + .../designs/autolathe/service_designs.dm | 17 + .../research/techweb/nodes/service_nodes.dm | 1 + .../mob/inhands/equipment/tools_lefthand.dmi | Bin 20999 -> 22686 bytes .../mob/inhands/equipment/tools_righthand.dmi | Bin 22885 -> 22621 bytes icons/obj/tools.dmi | Bin 38322 -> 32204 bytes tgstation.dme | 3 + .../tgui/interfaces/RapidDecorationDevice.tsx | 102 +++ 11 files changed, 1091 insertions(+), 2 deletions(-) create mode 100644 code/game/objects/items/rcd/RDD.dm create mode 100644 code/game/objects/structures/decorations.dm create mode 100644 code/modules/asset_cache/assets/rdd.dm create mode 100644 tgui/packages/tgui/interfaces/RapidDecorationDevice.tsx diff --git a/code/game/objects/items/rcd/RDD.dm b/code/game/objects/items/rcd/RDD.dm new file mode 100644 index 000000000000..b3b65931fe17 --- /dev/null +++ b/code/game/objects/items/rcd/RDD.dm @@ -0,0 +1,344 @@ +//RAPID DECORATION DEVICE + +/// Multiplier applied to cost when using RDD — each decoration costs this many matter units +#define RDD_COST_MULTIPLIER 2 + +/// All decoration designs available in the RDD +GLOBAL_LIST_INIT(rdd_designs, list( + "Grasses" = list( + list("name" = "Plastic Grass Patch", "path" = /obj/structure/decoration/grass/first), + list("name" = "Plastic Grass Patch (Alt)", "path" = /obj/structure/decoration/grass/second), + list("name" = "Plastic Grass Patch (Alt 2)", "path" = /obj/structure/decoration/grass/third), + list("name" = "Plastic Grass Patch (Random)", "path" = /obj/structure/decoration/grass/style_random), + list("name" = "Plastic Brown Grass", "path" = /obj/structure/decoration/grass/brown/first), + list("name" = "Plastic Brown Grass (Alt)", "path" = /obj/structure/decoration/grass/brown/second), + list("name" = "Plastic Brown Grass (Alt 2)", "path" = /obj/structure/decoration/grass/brown/third), + list("name" = "Plastic Brown Grass (Random)", "path" = /obj/structure/decoration/grass/brown/style_random), + list("name" = "Plastic Jungle Grass", "path" = /obj/structure/decoration/jungle_grass/first), + list("name" = "Plastic Jungle Grass (Alt)", "path" = /obj/structure/decoration/jungle_grass/second), + list("name" = "Plastic Jungle Grass (Alt 2)", "path" = /obj/structure/decoration/jungle_grass/third), + list("name" = "Plastic Jungle Grass (Alt 3)", "path" = /obj/structure/decoration/jungle_grass/fourth), + list("name" = "Plastic Jungle Grass (Alt 4)", "path" = /obj/structure/decoration/jungle_grass/fifth), + list("name" = "Plastic Jungle Grass (Random)", "path" = /obj/structure/decoration/jungle_grass/style_random), + list("name" = "Plastic Jungle Grass B", "path" = /obj/structure/decoration/jungle_grass/b/first), + list("name" = "Plastic Jungle Grass B (Alt)", "path" = /obj/structure/decoration/jungle_grass/b/second), + list("name" = "Plastic Jungle Grass B (Alt 2)", "path" = /obj/structure/decoration/jungle_grass/b/third), + list("name" = "Plastic Jungle Grass B (Alt 3)", "path" = /obj/structure/decoration/jungle_grass/b/fourth), + list("name" = "Plastic Jungle Grass B (Alt 4)", "path" = /obj/structure/decoration/jungle_grass/b/fifth), + list("name" = "Plastic Jungle Grass B (Random)", "path" = /obj/structure/decoration/jungle_grass/b/style_random), + ), + "Bushes" = list( + list("name" = "Plastic Bush", "path" = /obj/structure/decoration/bush/first), + list("name" = "Plastic Bush (Alt)", "path" = /obj/structure/decoration/bush/second), + list("name" = "Plastic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/third), + list("name" = "Plastic Bush (Alt 3)", "path" = /obj/structure/decoration/bush/fourth), + list("name" = "Plastic Bush (Random)", "path" = /obj/structure/decoration/bush/style_random), + list("name" = "Plastic Reeds", "path" = /obj/structure/decoration/bush/reed/first), + list("name" = "Plastic Reeds (Alt)", "path" = /obj/structure/decoration/bush/reed/second), + list("name" = "Plastic Reeds (Alt 2)", "path" = /obj/structure/decoration/bush/reed/third), + list("name" = "Plastic Reeds (Alt 3)", "path" = /obj/structure/decoration/bush/reed/fourth), + list("name" = "Plastic Reeds (Random)", "path" = /obj/structure/decoration/bush/reed/style_random), + list("name" = "Plastic Leafy Bush", "path" = /obj/structure/decoration/bush/leafy/first), + list("name" = "Plastic Leafy Bush (Alt)", "path" = /obj/structure/decoration/bush/leafy/second), + list("name" = "Plastic Leafy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/leafy/third), + list("name" = "Plastic Leafy Bush (Random)", "path" = /obj/structure/decoration/bush/leafy/style_random), + list("name" = "Plastic Pale Bush", "path" = /obj/structure/decoration/bush/pale/first), + list("name" = "Plastic Pale Bush (Alt)", "path" = /obj/structure/decoration/bush/pale/second), + list("name" = "Plastic Pale Bush (Alt 2)", "path" = /obj/structure/decoration/bush/pale/third), + list("name" = "Plastic Pale Bush (Alt 3)", "path" = /obj/structure/decoration/bush/pale/fourth), + list("name" = "Plastic Pale Bush (Random)", "path" = /obj/structure/decoration/bush/pale/style_random), + list("name" = "Plastic Stalky Bush", "path" = /obj/structure/decoration/bush/stalky/first), + list("name" = "Plastic Stalky Bush (Alt)", "path" = /obj/structure/decoration/bush/stalky/second), + list("name" = "Plastic Stalky Bush (Alt 2)", "path" = /obj/structure/decoration/bush/stalky/third), + list("name" = "Plastic Stalky Bush (Random)", "path" = /obj/structure/decoration/bush/stalky/style_random), + list("name" = "Plastic Grassy Bush", "path" = /obj/structure/decoration/bush/grassy/first), + list("name" = "Plastic Grassy Bush (Alt)", "path" = /obj/structure/decoration/bush/grassy/second), + list("name" = "Plastic Grassy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/grassy/third), + list("name" = "Plastic Grassy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/grassy/fourth), + list("name" = "Plastic Grassy Bush (Random)", "path" = /obj/structure/decoration/bush/grassy/style_random), + list("name" = "Plastic Sparse Grass", "path" = /obj/structure/decoration/bush/sparsegrass/first), + list("name" = "Plastic Sparse Grass (Alt)", "path" = /obj/structure/decoration/bush/sparsegrass/second), + list("name" = "Plastic Sparse Grass (Alt 2)", "path" = /obj/structure/decoration/bush/sparsegrass/third), + list("name" = "Plastic Sparse Grass (Random)", "path" = /obj/structure/decoration/bush/sparsegrass/style_random), + list("name" = "Plastic Full Grass", "path" = /obj/structure/decoration/bush/fullgrass/first), + list("name" = "Plastic Full Grass (Alt)", "path" = /obj/structure/decoration/bush/fullgrass/second), + list("name" = "Plastic Full Grass (Alt 2)", "path" = /obj/structure/decoration/bush/fullgrass/third), + list("name" = "Plastic Full Grass (Random)", "path" = /obj/structure/decoration/bush/fullgrass/style_random), + list("name" = "Plastic Ferny Bush", "path" = /obj/structure/decoration/bush/ferny/first), + list("name" = "Plastic Ferny Bush (Alt)", "path" = /obj/structure/decoration/bush/ferny/second), + list("name" = "Plastic Ferny Bush (Alt 2)", "path" = /obj/structure/decoration/bush/ferny/third), + list("name" = "Plastic Ferny Bush (Random)", "path" = /obj/structure/decoration/bush/ferny/style_random), + list("name" = "Plastic Sunny Bush", "path" = /obj/structure/decoration/bush/sunny/first), + list("name" = "Plastic Sunny Bush (Alt)", "path" = /obj/structure/decoration/bush/sunny/second), + list("name" = "Plastic Sunny Bush (Alt 2)", "path" = /obj/structure/decoration/bush/sunny/third), + list("name" = "Plastic Sunny Bush (Random)", "path" = /obj/structure/decoration/bush/sunny/style_random), + list("name" = "Plastic Generic Bush", "path" = /obj/structure/decoration/bush/generic/first), + list("name" = "Plastic Generic Bush (Alt)", "path" = /obj/structure/decoration/bush/generic/second), + list("name" = "Plastic Generic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/generic/third), + list("name" = "Plastic Generic Bush (Alt 3)", "path" = /obj/structure/decoration/bush/generic/fourth), + list("name" = "Plastic Generic Bush (Random)", "path" = /obj/structure/decoration/bush/generic/style_random), + list("name" = "Plastic Pointy Bush", "path" = /obj/structure/decoration/bush/pointy/first), + list("name" = "Plastic Pointy Bush (Alt)", "path" = /obj/structure/decoration/bush/pointy/second), + list("name" = "Plastic Pointy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/pointy/third), + list("name" = "Plastic Pointy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/pointy/fourth), + list("name" = "Plastic Pointy Bush (Random)", "path" = /obj/structure/decoration/bush/pointy/style_random), + list("name" = "Plastic Lavender Grass", "path" = /obj/structure/decoration/bush/lavendergrass/first), + list("name" = "Plastic Lavender Grass (Alt)", "path" = /obj/structure/decoration/bush/lavendergrass/second), + list("name" = "Plastic Lavender Grass (Alt 2)", "path" = /obj/structure/decoration/bush/lavendergrass/third), + list("name" = "Plastic Lavender Grass (Alt 3)", "path" = /obj/structure/decoration/bush/lavendergrass/fourth), + list("name" = "Plastic Lavender Grass (Random)", "path" = /obj/structure/decoration/bush/lavendergrass/style_random), + ), + "Flowers" = list( + list("name" = "Plastic Yellow-White Flowers", "path" = /obj/structure/decoration/bush/flowers_yw/first), + list("name" = "Plastic Yellow-White Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_yw/second), + list("name" = "Plastic Yellow-White Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_yw/third), + list("name" = "Plastic Yellow-White Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_yw/style_random), + list("name" = "Plastic Blue-Red Flowers", "path" = /obj/structure/decoration/bush/flowers_br/first), + list("name" = "Plastic Blue-Red Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_br/second), + list("name" = "Plastic Blue-Red Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_br/third), + list("name" = "Plastic Blue-Red Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_br/style_random), + list("name" = "Plastic Purple Flowers", "path" = /obj/structure/decoration/bush/flowers_pp/first), + list("name" = "Plastic Purple Flowers (Alt)", "path" = /obj/structure/decoration/bush/flowers_pp/second), + list("name" = "Plastic Purple Flowers (Alt 2)", "path" = /obj/structure/decoration/bush/flowers_pp/third), + list("name" = "Plastic Purple Flowers (Random)", "path" = /obj/structure/decoration/bush/flowers_pp/style_random), + ), + "Snow" = list( + list("name" = "Plastic Snowy Bush", "path" = /obj/structure/decoration/bush/snow/first), + list("name" = "Plastic Snowy Bush (Alt)", "path" = /obj/structure/decoration/bush/snow/second), + list("name" = "Plastic Snowy Bush (Alt 2)", "path" = /obj/structure/decoration/bush/snow/third), + list("name" = "Plastic Snowy Bush (Alt 3)", "path" = /obj/structure/decoration/bush/snow/fourth), + list("name" = "Plastic Snowy Bush (Alt 4)", "path" = /obj/structure/decoration/bush/snow/fifth), + list("name" = "Plastic Snowy Bush (Alt 5)", "path" = /obj/structure/decoration/bush/snow/sixth), + list("name" = "Plastic Snowy Bush (Random)", "path" = /obj/structure/decoration/bush/snow/style_random), + ), + "Jungle" = list( + list("name" = "Plastic Jungle Bush", "path" = /obj/structure/decoration/bush/jungle/first), + list("name" = "Plastic Jungle Bush (Alt)", "path" = /obj/structure/decoration/bush/jungle/second), + list("name" = "Plastic Jungle Bush (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/third), + list("name" = "Plastic Jungle Bush (Random)", "path" = /obj/structure/decoration/bush/jungle/style_random), + list("name" = "Plastic Jungle Bush B", "path" = /obj/structure/decoration/bush/jungle/b/first), + list("name" = "Plastic Jungle Bush B (Alt)", "path" = /obj/structure/decoration/bush/jungle/b/second), + list("name" = "Plastic Jungle Bush B (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/b/third), + list("name" = "Plastic Jungle Bush B (Random)", "path" = /obj/structure/decoration/bush/jungle/b/style_random), + list("name" = "Plastic Jungle Bush C", "path" = /obj/structure/decoration/bush/jungle/c/first), + list("name" = "Plastic Jungle Bush C (Alt)", "path" = /obj/structure/decoration/bush/jungle/c/second), + list("name" = "Plastic Jungle Bush C (Alt 2)", "path" = /obj/structure/decoration/bush/jungle/c/third), + list("name" = "Plastic Jungle Bush C (Random)", "path" = /obj/structure/decoration/bush/jungle/c/style_random), + list("name" = "Large Plastic Bush", "path" = /obj/structure/decoration/bush/large/first), + list("name" = "Large Plastic Bush (Alt)", "path" = /obj/structure/decoration/bush/large/second), + list("name" = "Large Plastic Bush (Alt 2)", "path" = /obj/structure/decoration/bush/large/third), + list("name" = "Large Plastic Bush (Random)", "path" = /obj/structure/decoration/bush/large/style_random), + ), + "Rocks" = list( + list("name" = "Plastic Rock", "path" = /obj/structure/decoration/rock/first), + list("name" = "Plastic Rock (Alt)", "path" = /obj/structure/decoration/rock/second), + list("name" = "Plastic Rock (Alt 2)", "path" = /obj/structure/decoration/rock/third), + list("name" = "Plastic Rock (Alt 3)", "path" = /obj/structure/decoration/rock/fourth), + list("name" = "Plastic Rock (Random)", "path" = /obj/structure/decoration/rock/style_random), + list("name" = "Plastic Rock Pile", "path" = /obj/structure/decoration/rock/pile/first), + list("name" = "Plastic Rock Pile (Alt)", "path" = /obj/structure/decoration/rock/pile/second), + list("name" = "Plastic Rock Pile (Alt 2)", "path" = /obj/structure/decoration/rock/pile/third), + list("name" = "Plastic Rock Pile (Random)", "path" = /obj/structure/decoration/rock/pile/style_random), + list("name" = "Plastic Jungle Rocks", "path" = /obj/structure/decoration/rock/pile/jungle/first), + list("name" = "Plastic Jungle Rocks (Alt)", "path" = /obj/structure/decoration/rock/pile/jungle/second), + list("name" = "Plastic Jungle Rocks (Alt 2)", "path" = /obj/structure/decoration/rock/pile/jungle/third), + list("name" = "Plastic Jungle Rocks (Alt 3)", "path" = /obj/structure/decoration/rock/pile/jungle/fourth), + list("name" = "Plastic Jungle Rocks (Alt 4)", "path" = /obj/structure/decoration/rock/pile/jungle/fifth), + list("name" = "Plastic Jungle Rocks (Random)", "path" = /obj/structure/decoration/rock/pile/jungle/style_random), + list("name" = "Large Plastic Rocks", "path" = /obj/structure/decoration/rock/pile/jungle/large/first), + list("name" = "Large Plastic Rocks (Alt)", "path" = /obj/structure/decoration/rock/pile/jungle/large/second), + list("name" = "Large Plastic Rocks (Alt 2)", "path" = /obj/structure/decoration/rock/pile/jungle/large/third), + list("name" = "Large Plastic Rocks (Random)", "path" = /obj/structure/decoration/rock/pile/jungle/large/style_random), + ), +)) + +/obj/item/construction/rdd + name = "rapid-decoration-device (RDD)" + desc = "A device used to rapidly deploy plastic decorative flora. \ + Internally synthesizes cheap plastic replicas of natural scenery." + icon = 'icons/obj/tools.dmi' + icon_state = "rdd" + worn_icon_state = "RCD" + lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi' + custom_premium_price = PAYCHECK_COMMAND * 1 + max_matter = 200 + slot_flags = ITEM_SLOT_BELT + item_flags = NO_MAT_REDEMPTION | NOBLUDGEON + has_ammobar = TRUE + banned_upgrades = RCD_ALL_UPGRADES & ~RCD_UPGRADE_SILO_LINK + charge_icon_state = "rtd" + drop_sound = 'sound/items/handling/tools/rcd_drop.ogg' + pickup_sound = 'sound/items/handling/tools/rcd_pickup.ogg' + sound_vary = TRUE + + /// Currently selected decoration path + var/obj/structure/decoration/selected_decoration + /// Currently selected category name (for UI) + var/selected_category + /// Currently selected design name (for UI) + var/selected_design_name + +/obj/item/construction/rdd/Initialize(mapload) + . = ..() + selected_category = GLOB.rdd_designs[1] + var/list/category_designs = GLOB.rdd_designs[selected_category] + if(length(category_designs)) + var/list/first_design = category_designs[1] + selected_decoration = first_design["path"] + selected_design_name = first_design["name"] + +/obj/item/construction/rdd/examine(mob/user) + . = ..() + . += span_info("Currently set to produce: [span_bold(initial(selected_decoration.name))].") + +/obj/item/construction/rdd/attack_self(mob/user) + . = ..() + ui_interact(user) + +/obj/item/construction/rdd/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "RapidDecorationDevice", name) + ui.open() + +/obj/item/construction/rdd/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet_batched/rdd), + ) + +/obj/item/construction/rdd/ui_static_data(mob/user) + var/list/data = ..() + + data["categories"] = list() + for(var/category in GLOB.rdd_designs) + var/list/cat_entry = list("cat_name" = category, "designs" = list()) + for(var/list/design in GLOB.rdd_designs[category]) + cat_entry["designs"] += list(list( + "name" = design["name"], + "icon" = sanitize_css_class_name(design["name"]), + )) + data["categories"] += list(cat_entry) + + return data + +/obj/item/construction/rdd/ui_data(mob/user) + var/list/data = ..() + + var/total_matter = get_matter(user) + data["matter"] = isnum(total_matter) ? total_matter : 0 + data["max_matter"] = max_matter + data["selected_category"] = selected_category + data["selected_design"] = selected_design_name + + return data + +/obj/item/construction/rdd/handle_ui_act(action, params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("design") + var/category = params["category"] + var/design_name = params["name"] + for(var/list/design as anything in GLOB.rdd_designs[category]) + if(design["name"] == design_name) + selected_decoration = design["path"] + selected_category = category + selected_design_name = design_name + playsound(src, SFX_TOOL_SWITCH, 20, TRUE) + return TRUE + + return TRUE + +/obj/item/construction/rdd/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + . = ..() + if(. & ITEM_INTERACT_ANY_BLOCKER) + return . + + var/turf/target_turf = get_turf(interacting_with) + if(!target_turf) + return ITEM_INTERACT_BLOCKING + + if(target_turf.is_blocked_turf(exclude_mobs = TRUE)) + balloon_alert(user, "tile is blocked!") + return ITEM_INTERACT_BLOCKING + + var/decoration_count = 0 + for(var/obj/structure/decoration/existing in target_turf.contents) + decoration_count++ + if(decoration_count >= 3) + balloon_alert(user, "too many decorations here!") + return ITEM_INTERACT_BLOCKING + + var/cost = RDD_COST_MULTIPLIER + if(!useResource(cost, user, TRUE)) + return ITEM_INTERACT_BLOCKING + + playsound(loc, 'sound/machines/click.ogg', 50, TRUE) + if(!do_after(user, 0.5 SECONDS, target_turf)) + return ITEM_INTERACT_BLOCKING + if(!useResource(cost, user, TRUE)) + return ITEM_INTERACT_BLOCKING + + playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE) + new selected_decoration(target_turf) + useResource(cost, user) + + log_tool("[key_name(user)] used [src] to create [initial(selected_decoration.name)] at [AREACOORD(target_turf)]") + return ITEM_INTERACT_SUCCESS + +/obj/item/construction/rdd/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers) + var/turf/target_turf = get_turf(interacting_with) + if(!target_turf) + return NONE + + var/obj/structure/decoration/found = locate() in target_turf + + if(!found) + return NONE + + playsound(target_turf, 'sound/machines/click.ogg', 50, TRUE) + if(!do_after(user, 0.5 SECONDS, target_turf)) + return ITEM_INTERACT_BLOCKING + + playsound(target_turf, 'sound/items/deconstruct.ogg', 50, TRUE) + qdel(found) + + log_tool("[key_name(user)] used [src] to deconstruct [found] at [AREACOORD(target_turf)]") + return ITEM_INTERACT_SUCCESS + +/obj/item/construction/rdd/borg + desc = "A device used to rapidly deploy plastic decorative flora. Uses the cyborg's internal cell." + /// energy usage per decoration + var/energyfactor = 0.05 * STANDARD_CELL_CHARGE + +/obj/item/construction/rdd/borg/get_matter(mob/user) + if(!iscyborg(user)) + return 0 + var/mob/living/silicon/robot/borgy = user + if(!borgy.cell) + return 0 + max_matter = borgy.cell.maxcharge + return borgy.cell.charge + +/obj/item/construction/rdd/borg/useResource(amount, mob/user, dry_run) + var/mob/living/silicon/robot/borgy = user + if(!iscyborg(borgy)) + return FALSE + if(!borgy.cell) + balloon_alert(user, "no cell found!") + return FALSE + if(borgy.cell.charge < amount * energyfactor) + balloon_alert(user, "insufficient charge!") + return FALSE + if(!dry_run) + playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE) + return borgy.cell.use(amount * energyfactor) + return TRUE + +/obj/item/construction/rdd/loaded + matter = 200 + +#undef RDD_COST_MULTIPLIER diff --git a/code/game/objects/items/rcd/RHD.dm b/code/game/objects/items/rcd/RHD.dm index 40f989ee0882..93663639a9cc 100644 --- a/code/game/objects/items/rcd/RHD.dm +++ b/code/game/objects/items/rcd/RHD.dm @@ -30,6 +30,8 @@ var/has_ammobar = FALSE /// amount of divisions in the ammo indicator overlay/number of ammo indicator states var/ammo_sections = 10 + /// icon_state prefix used for charge overlays — defaults to icon_state if not set + var/charge_icon_state /// bitflags for upgrades var/construction_upgrades = NONE /// bitflags for banned upgrades @@ -179,7 +181,7 @@ if(has_ammobar) var/ratio = ceil((matter / max_matter) * ammo_sections) if(ratio > 0) - . += "[icon_state]_charge[ratio]" + . += "[charge_icon_state || icon_state]_charge[ratio]" /** * Uses resource to do some action. Returns amount of resource used or TRUE/FALSE if only an dry run is required @@ -193,7 +195,7 @@ if(!silo_mats || !silo_link) if(matter < amount) if(has_ammobar) - flick("[icon_state]_empty", src) + flick("[charge_icon_state || icon_state]_empty", src) if(user) balloon_alert(user, "not enough matter!") return FALSE diff --git a/code/game/objects/structures/decorations.dm b/code/game/objects/structures/decorations.dm new file mode 100644 index 000000000000..fb05b6478e2a --- /dev/null +++ b/code/game/objects/structures/decorations.dm @@ -0,0 +1,609 @@ +/obj/structure/decoration + name = "plastic decoration" + desc = "A cheap plastic imitation of nature. At least it doesn't need watering." + icon = 'icons/obj/fluff/flora/ausflora.dmi' + resistance_flags = FLAMMABLE + max_integrity = 20 + anchored = TRUE + alpha = 210 + /// No material refund on deconstruction, it's cheap plastic + custom_materials = null + +/obj/structure/decoration/Initialize(mapload) + . = ..() + +/obj/structure/decoration/atom_deconstruct(disassembled = TRUE) + if(!disassembled) + new /obj/effect/decal/cleanable/plastic(loc) + return + +/obj/structure/decoration/examine(mob/user) + . = ..() + . += span_notice("It's made of cheap, hollow plastic.") + +/obj/structure/decoration/grass + name = "plastic grass patch" + desc = "Fake grass. Feels like a brillo pad." + icon = /obj/structure/flora/grass/green::icon + icon_state = /obj/structure/flora/grass/green::icon_state + +/obj/structure/decoration/grass/first + // inherits from parent + +/obj/structure/decoration/grass/second + icon_state = /obj/structure/flora/grass/green/style_2::icon_state + +/obj/structure/decoration/grass/third + icon_state = /obj/structure/flora/grass/green/style_3::icon_state + +/obj/structure/decoration/grass/style_random/Initialize(mapload) + . = ..() + icon_state = "snowgrass[rand(1, 3)]gb" + update_appearance() + +/obj/structure/decoration/grass/brown + icon = /obj/structure/flora/grass/brown::icon + icon_state = /obj/structure/flora/grass/brown::icon_state + +/obj/structure/decoration/grass/brown/first + +/obj/structure/decoration/grass/brown/second + icon_state = /obj/structure/flora/grass/brown/style_2::icon_state + +/obj/structure/decoration/grass/brown/third + icon_state = /obj/structure/flora/grass/brown/style_3::icon_state + +/obj/structure/decoration/grass/brown/style_random/Initialize(mapload) + . = ..() + icon_state = "snowgrass[rand(1, 3)]bb" + update_appearance() + +/obj/structure/decoration/jungle_grass + name = "plastic jungle grass" + desc = "Plastic alien-looking grass. The jungle vibe without the jungle bugs." + icon = /obj/structure/flora/grass/jungle::icon + icon_state = /obj/structure/flora/grass/jungle::icon_state + +/obj/structure/decoration/jungle_grass/first + +/obj/structure/decoration/jungle_grass/second + icon_state = /obj/structure/flora/grass/jungle/a/style_2::icon_state + +/obj/structure/decoration/jungle_grass/third + icon_state = /obj/structure/flora/grass/jungle/a/style_3::icon_state + +/obj/structure/decoration/jungle_grass/fourth + icon_state = /obj/structure/flora/grass/jungle/a/style_4::icon_state + +/obj/structure/decoration/jungle_grass/fifth + icon_state = /obj/structure/flora/grass/jungle/a/style_5::icon_state + +/obj/structure/decoration/jungle_grass/style_random/Initialize(mapload) + . = ..() + icon_state = "grassa[rand(1, 5)]" + update_appearance() + +/obj/structure/decoration/jungle_grass/b + icon = /obj/structure/flora/grass/jungle/b::icon + icon_state = /obj/structure/flora/grass/jungle/b::icon_state + +/obj/structure/decoration/jungle_grass/b/first + +/obj/structure/decoration/jungle_grass/b/second + icon_state = /obj/structure/flora/grass/jungle/b/style_2::icon_state + +/obj/structure/decoration/jungle_grass/b/third + icon_state = /obj/structure/flora/grass/jungle/b/style_3::icon_state + +/obj/structure/decoration/jungle_grass/b/fourth + icon_state = /obj/structure/flora/grass/jungle/b/style_4::icon_state + +/obj/structure/decoration/jungle_grass/b/fifth + icon_state = /obj/structure/flora/grass/jungle/b/style_5::icon_state + +/obj/structure/decoration/jungle_grass/b/style_random/Initialize(mapload) + . = ..() + icon_state = "grassb[rand(1, 5)]" + update_appearance() + +/obj/structure/decoration/bush + name = "plastic bush" + desc = "A plastic shrub. Bristly to the touch and slightly off-color." + icon = /obj/structure/flora/bush::icon + icon_state = /obj/structure/flora/bush::icon_state + +/obj/structure/decoration/bush/first + +/obj/structure/decoration/bush/second + icon_state = /obj/structure/flora/bush/style_2::icon_state + +/obj/structure/decoration/bush/third + icon_state = /obj/structure/flora/bush/style_3::icon_state + +/obj/structure/decoration/bush/fourth + icon_state = /obj/structure/flora/bush/style_4::icon_state + +/obj/structure/decoration/bush/style_random/Initialize(mapload) + . = ..() + icon_state = "firstbush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/reed + name = "plastic reeds" + icon = /obj/structure/flora/bush/reed::icon + icon_state = /obj/structure/flora/bush/reed::icon_state + +/obj/structure/decoration/bush/reed/first + +/obj/structure/decoration/bush/reed/second + icon_state = /obj/structure/flora/bush/reed/style_2::icon_state + +/obj/structure/decoration/bush/reed/third + icon_state = /obj/structure/flora/bush/reed/style_3::icon_state + +/obj/structure/decoration/bush/reed/fourth + icon_state = /obj/structure/flora/bush/reed/style_4::icon_state + +/obj/structure/decoration/bush/reed/style_random/Initialize(mapload) + . = ..() + icon_state = "reedbush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/leafy + name = "plastic leafy bush" + icon = /obj/structure/flora/bush/leafy::icon + icon_state = /obj/structure/flora/bush/leafy::icon_state + +/obj/structure/decoration/bush/leafy/first + +/obj/structure/decoration/bush/leafy/second + icon_state = /obj/structure/flora/bush/leavy/style_2::icon_state + +/obj/structure/decoration/bush/leafy/third + icon_state = /obj/structure/flora/bush/leavy/style_3::icon_state + +/obj/structure/decoration/bush/leafy/style_random/Initialize(mapload) + . = ..() + icon_state = "leafybush_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/pale + name = "plastic pale bush" + icon = /obj/structure/flora/bush/pale::icon + icon_state = /obj/structure/flora/bush/pale::icon_state + +/obj/structure/decoration/bush/pale/first + +/obj/structure/decoration/bush/pale/second + icon_state = /obj/structure/flora/bush/pale/style_2::icon_state + +/obj/structure/decoration/bush/pale/third + icon_state = /obj/structure/flora/bush/pale/style_3::icon_state + +/obj/structure/decoration/bush/pale/fourth + icon_state = /obj/structure/flora/bush/pale/style_4::icon_state + +/obj/structure/decoration/bush/pale/style_random/Initialize(mapload) + . = ..() + icon_state = "palebush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/stalky + name = "plastic stalky bush" + icon = /obj/structure/flora/bush/stalky::icon + icon_state = /obj/structure/flora/bush/stalky::icon_state + +/obj/structure/decoration/bush/stalky/first + +/obj/structure/decoration/bush/stalky/second + icon_state = /obj/structure/flora/bush/stalky/style_2::icon_state + +/obj/structure/decoration/bush/stalky/third + icon_state = /obj/structure/flora/bush/stalky/style_3::icon_state + +/obj/structure/decoration/bush/stalky/style_random/Initialize(mapload) + . = ..() + icon_state = "stalkybush_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/grassy + name = "plastic grassy bush" + icon = /obj/structure/flora/bush/grassy::icon + icon_state = /obj/structure/flora/bush/grassy::icon_state + +/obj/structure/decoration/bush/grassy/first + +/obj/structure/decoration/bush/grassy/second + icon_state = /obj/structure/flora/bush/grassy/style_2::icon_state + +/obj/structure/decoration/bush/grassy/third + icon_state = /obj/structure/flora/bush/grassy/style_3::icon_state + +/obj/structure/decoration/bush/grassy/fourth + icon_state = /obj/structure/flora/bush/grassy/style_4::icon_state + +/obj/structure/decoration/bush/grassy/style_random/Initialize(mapload) + . = ..() + icon_state = "grassybush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/sparsegrass + name = "plastic sparse grass" + icon = /obj/structure/flora/bush/sparsegrass::icon + icon_state = /obj/structure/flora/bush/sparsegrass::icon_state + +/obj/structure/decoration/bush/sparsegrass/first + +/obj/structure/decoration/bush/sparsegrass/second + icon_state = /obj/structure/flora/bush/sparsegrass/style_2::icon_state + +/obj/structure/decoration/bush/sparsegrass/third + icon_state = /obj/structure/flora/bush/sparsegrass/style_3::icon_state + +/obj/structure/decoration/bush/sparsegrass/style_random/Initialize(mapload) + . = ..() + icon_state = "sparsegrass_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/fullgrass + name = "plastic full grass" + icon = /obj/structure/flora/bush/fullgrass::icon + icon_state = /obj/structure/flora/bush/fullgrass::icon_state + +/obj/structure/decoration/bush/fullgrass/first + +/obj/structure/decoration/bush/fullgrass/second + icon_state = /obj/structure/flora/bush/fullgrass/style_2::icon_state + +/obj/structure/decoration/bush/fullgrass/third + icon_state = /obj/structure/flora/bush/fullgrass/style_3::icon_state + +/obj/structure/decoration/bush/fullgrass/style_random/Initialize(mapload) + . = ..() + icon_state = "fullgrass_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/ferny + name = "plastic ferny bush" + icon = /obj/structure/flora/bush/ferny::icon + icon_state = /obj/structure/flora/bush/ferny::icon_state + +/obj/structure/decoration/bush/ferny/first + +/obj/structure/decoration/bush/ferny/second + icon_state = /obj/structure/flora/bush/ferny/style_2::icon_state + +/obj/structure/decoration/bush/ferny/third + icon_state = /obj/structure/flora/bush/ferny/style_3::icon_state + +/obj/structure/decoration/bush/ferny/style_random/Initialize(mapload) + . = ..() + icon_state = "fernybush_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/sunny + name = "plastic sunny bush" + icon = /obj/structure/flora/bush/sunny::icon + icon_state = /obj/structure/flora/bush/sunny::icon_state + +/obj/structure/decoration/bush/sunny/first + +/obj/structure/decoration/bush/sunny/second + icon_state = /obj/structure/flora/bush/sunny/style_2::icon_state + +/obj/structure/decoration/bush/sunny/third + icon_state = /obj/structure/flora/bush/sunny/style_3::icon_state + +/obj/structure/decoration/bush/sunny/style_random/Initialize(mapload) + . = ..() + icon_state = "sunnybush_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/generic + name = "plastic generic bush" + icon = /obj/structure/flora/bush/generic::icon + icon_state = /obj/structure/flora/bush/generic::icon_state + +/obj/structure/decoration/bush/generic/first + +/obj/structure/decoration/bush/generic/second + icon_state = /obj/structure/flora/bush/generic/style_2::icon_state + +/obj/structure/decoration/bush/generic/third + icon_state = /obj/structure/flora/bush/generic/style_3::icon_state + +/obj/structure/decoration/bush/generic/fourth + icon_state = /obj/structure/flora/bush/generic/style_4::icon_state + +/obj/structure/decoration/bush/generic/style_random/Initialize(mapload) + . = ..() + icon_state = "genericbush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/pointy + name = "plastic pointy bush" + icon = /obj/structure/flora/bush/pointy::icon + icon_state = /obj/structure/flora/bush/pointy::icon_state + +/obj/structure/decoration/bush/pointy/first + +/obj/structure/decoration/bush/pointy/second + icon_state = /obj/structure/flora/bush/pointy/style_2::icon_state + +/obj/structure/decoration/bush/pointy/third + icon_state = /obj/structure/flora/bush/pointy/style_3::icon_state + +/obj/structure/decoration/bush/pointy/fourth + icon_state = /obj/structure/flora/bush/pointy/style_4::icon_state + +/obj/structure/decoration/bush/pointy/style_random/Initialize(mapload) + . = ..() + icon_state = "pointybush_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/lavendergrass + name = "plastic lavender grass" + icon = /obj/structure/flora/bush/lavendergrass::icon + icon_state = /obj/structure/flora/bush/lavendergrass::icon_state + +/obj/structure/decoration/bush/lavendergrass/first + +/obj/structure/decoration/bush/lavendergrass/second + icon_state = /obj/structure/flora/bush/lavendergrass/style_2::icon_state + +/obj/structure/decoration/bush/lavendergrass/third + icon_state = /obj/structure/flora/bush/lavendergrass/style_3::icon_state + +/obj/structure/decoration/bush/lavendergrass/fourth + icon_state = /obj/structure/flora/bush/lavendergrass/style_4::icon_state + +/obj/structure/decoration/bush/lavendergrass/style_random/Initialize(mapload) + . = ..() + icon_state = "lavendergrass_[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/bush/flowers_yw + name = "plastic yellow-white flowers" + icon = /obj/structure/flora/bush/flowers_yw::icon + icon_state = /obj/structure/flora/bush/flowers_yw::icon_state + +/obj/structure/decoration/bush/flowers_yw/first + +/obj/structure/decoration/bush/flowers_yw/second + icon_state = /obj/structure/flora/bush/flowers_yw/style_2::icon_state + +/obj/structure/decoration/bush/flowers_yw/third + icon_state = /obj/structure/flora/bush/flowers_yw/style_3::icon_state + +/obj/structure/decoration/bush/flowers_yw/style_random/Initialize(mapload) + . = ..() + icon_state = "ywflowers_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/flowers_br + name = "plastic blue-red flowers" + icon = /obj/structure/flora/bush/flowers_br::icon + icon_state = /obj/structure/flora/bush/flowers_br::icon_state + +/obj/structure/decoration/bush/flowers_br/first + +/obj/structure/decoration/bush/flowers_br/second + icon_state = /obj/structure/flora/bush/flowers_br/style_2::icon_state + +/obj/structure/decoration/bush/flowers_br/third + icon_state = /obj/structure/flora/bush/flowers_br/style_3::icon_state + +/obj/structure/decoration/bush/flowers_br/style_random/Initialize(mapload) + . = ..() + icon_state = "brflowers_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/flowers_pp + name = "plastic purple flowers" + icon = /obj/structure/flora/bush/flowers_pp::icon + icon_state = /obj/structure/flora/bush/flowers_pp::icon_state + +/obj/structure/decoration/bush/flowers_pp/first + +/obj/structure/decoration/bush/flowers_pp/second + icon_state = /obj/structure/flora/bush/flowers_pp/style_2::icon_state + +/obj/structure/decoration/bush/flowers_pp/third + icon_state = /obj/structure/flora/bush/flowers_pp/style_3::icon_state + +/obj/structure/decoration/bush/flowers_pp/style_random/Initialize(mapload) + . = ..() + icon_state = "ppflowers_[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/snow + name = "plastic snowy bush" + desc = "A plastic bush dusted with fake snow. Year-round winter cheer." + icon = /obj/structure/flora/bush/snow::icon + icon_state = /obj/structure/flora/bush/snow::icon_state + +/obj/structure/decoration/bush/snow/first + +/obj/structure/decoration/bush/snow/second + icon_state = /obj/structure/flora/bush/snow/style_2::icon_state + +/obj/structure/decoration/bush/snow/third + icon_state = /obj/structure/flora/bush/snow/style_3::icon_state + +/obj/structure/decoration/bush/snow/fourth + icon_state = /obj/structure/flora/bush/snow/style_4::icon_state + +/obj/structure/decoration/bush/snow/fifth + icon_state = /obj/structure/flora/bush/snow/style_5::icon_state + +/obj/structure/decoration/bush/snow/sixth + icon_state = /obj/structure/flora/bush/snow/style_6::icon_state + +/obj/structure/decoration/bush/snow/style_random/Initialize(mapload) + . = ..() + icon_state = "snowbush[rand(1, 6)]" + update_appearance() + +/obj/structure/decoration/bush/jungle + name = "plastic jungle bush" + desc = "Plastic jungle foliage. All the looks, none of the allergens." + icon = /obj/structure/flora/bush/jungle::icon + icon_state = /obj/structure/flora/bush/jungle::icon_state + +/obj/structure/decoration/bush/jungle/first + +/obj/structure/decoration/bush/jungle/second + icon_state = /obj/structure/flora/bush/jungle/a/style_2::icon_state + +/obj/structure/decoration/bush/jungle/third + icon_state = /obj/structure/flora/bush/jungle/a/style_3::icon_state + +/obj/structure/decoration/bush/jungle/style_random/Initialize(mapload) + . = ..() + icon_state = "busha[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/jungle/b + icon = /obj/structure/flora/bush/jungle/b::icon + icon_state = /obj/structure/flora/bush/jungle/b::icon_state + +/obj/structure/decoration/bush/jungle/b/first + +/obj/structure/decoration/bush/jungle/b/second + icon_state = /obj/structure/flora/bush/jungle/b/style_2::icon_state + +/obj/structure/decoration/bush/jungle/b/third + icon_state = /obj/structure/flora/bush/jungle/b/style_3::icon_state + +/obj/structure/decoration/bush/jungle/b/style_random/Initialize(mapload) + . = ..() + icon_state = "bushb[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/jungle/c + icon = /obj/structure/flora/bush/jungle/c::icon + icon_state = /obj/structure/flora/bush/jungle/c::icon_state + +/obj/structure/decoration/bush/jungle/c/first + +/obj/structure/decoration/bush/jungle/c/second + icon_state = /obj/structure/flora/bush/jungle/c/style_2::icon_state + +/obj/structure/decoration/bush/jungle/c/third + icon_state = /obj/structure/flora/bush/jungle/c/style_3::icon_state + +/obj/structure/decoration/bush/jungle/c/style_random/Initialize(mapload) + . = ..() + icon_state = "bushc[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/bush/large + name = "large plastic bush" + desc = "A large plastic bush. Dominates the room with its hollow presence." + icon = /obj/structure/flora/bush/large::icon + icon_state = /obj/structure/flora/bush/large::icon_state + pixel_x = /obj/structure/flora/bush/large::pixel_x + pixel_y = /obj/structure/flora/bush/large::pixel_y + layer = /obj/structure/flora/bush/large::layer + plane = /obj/structure/flora/bush/large::plane + density = /obj/structure/flora/bush/large::density + +/obj/structure/decoration/bush/large/first + +/obj/structure/decoration/bush/large/second + icon_state = /obj/structure/flora/bush/large/style_2::icon_state + +/obj/structure/decoration/bush/large/third + icon_state = /obj/structure/flora/bush/large/style_3::icon_state + +/obj/structure/decoration/bush/large/style_random/Initialize(mapload) + . = ..() + icon_state = "bush[rand(1, 3)]" + update_appearance() + +/obj/structure/decoration/rock + name = "plastic rock" + desc = "A hollow plastic boulder. Surprisingly convincing from a distance." + icon = /obj/structure/flora/rock::icon + icon_state = /obj/structure/flora/rock::icon_state + +/obj/structure/decoration/rock/first + +/obj/structure/decoration/rock/second + icon_state = /obj/structure/flora/rock/style_2::icon_state + +/obj/structure/decoration/rock/third + icon_state = /obj/structure/flora/rock/style_3::icon_state + +/obj/structure/decoration/rock/fourth + icon_state = /obj/structure/flora/rock/style_4::icon_state + +/obj/structure/decoration/rock/style_random/Initialize(mapload) + . = ..() + icon_state = "basalt[rand(1, 4)]" + update_appearance() + +/obj/structure/decoration/rock/pile + name = "plastic rock pile" + desc = "A pile of hollow plastic rocks. Light enough to kick over." + icon = /obj/structure/flora/rock/pile::icon + icon_state = /obj/structure/flora/rock/pile::icon_state + +/obj/structure/decoration/rock/pile/first + +/obj/structure/decoration/rock/pile/second + icon_state = /obj/structure/flora/rock/pile/style_2::icon_state + +/obj/structure/decoration/rock/pile/third + icon_state = /obj/structure/flora/rock/pile/style_3::icon_state + +/obj/structure/decoration/rock/pile/style_random/Initialize(mapload) + . = ..() + icon_state = "lavarocks[pick(3;1,3;2,1;3)]" + update_appearance() + +/obj/structure/decoration/rock/pile/jungle + name = "plastic jungle rocks" + desc = "Fake rocks with a jungle theme. No actual geological history." + icon = /obj/structure/flora/rock/pile/jungle::icon + icon_state = /obj/structure/flora/rock/pile/jungle::icon_state + +/obj/structure/decoration/rock/pile/jungle/first + +/obj/structure/decoration/rock/pile/jungle/second + icon_state = /obj/structure/flora/rock/pile/jungle/style_2::icon_state + +/obj/structure/decoration/rock/pile/jungle/third + icon_state = /obj/structure/flora/rock/pile/jungle/style_3::icon_state + +/obj/structure/decoration/rock/pile/jungle/fourth + icon_state = /obj/structure/flora/rock/pile/jungle/style_4::icon_state + +/obj/structure/decoration/rock/pile/jungle/fifth + icon_state = /obj/structure/flora/rock/pile/jungle/style_5::icon_state + +/obj/structure/decoration/rock/pile/jungle/style_random/Initialize(mapload) + . = ..() + icon_state = "rock[rand(1, 5)]" + update_appearance() + +/obj/structure/decoration/rock/pile/jungle/large + name = "plastic large rocks" + desc = "A pile of large fake jungle rocks. Surprisingly light." + icon = /obj/structure/flora/rock/pile/jungle/large::icon + icon_state = /obj/structure/flora/rock/pile/jungle/large::icon_state + pixel_x = /obj/structure/flora/rock/pile/jungle/large::pixel_x + pixel_y = /obj/structure/flora/rock/pile/jungle/large::pixel_y + +/obj/structure/decoration/rock/pile/jungle/large/first + +/obj/structure/decoration/rock/pile/jungle/large/second + icon_state = /obj/structure/flora/rock/pile/jungle/large/style_2::icon_state + +/obj/structure/decoration/rock/pile/jungle/large/third + icon_state = /obj/structure/flora/rock/pile/jungle/large/style_3::icon_state + +/obj/structure/decoration/rock/pile/jungle/large/style_random/Initialize(mapload) + . = ..() + icon_state = "rocks[rand(1, 3)]" + update_appearance() diff --git a/code/modules/asset_cache/assets/rdd.dm b/code/modules/asset_cache/assets/rdd.dm new file mode 100644 index 000000000000..a5f422b420bb --- /dev/null +++ b/code/modules/asset_cache/assets/rdd.dm @@ -0,0 +1,11 @@ +/datum/asset/spritesheet_batched/rdd + name = "rdd" + +/datum/asset/spritesheet_batched/rdd/create_spritesheets() + for(var/category in GLOB.rdd_designs) + for(var/list/design in GLOB.rdd_designs[category]) + var/obj/structure/decoration/dec_path = design["path"] + var/sprite_name = sanitize_css_class_name(design["name"]) + var/datum/universal_icon/icon = uni_icon(initial(dec_path.icon), initial(dec_path.icon_state)) + icon.scale(32, 32) + insert_icon(sprite_name, icon) diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm index 784cc5688b87..316feffcd93d 100644 --- a/code/modules/research/designs/autolathe/service_designs.dm +++ b/code/modules/research/designs/autolathe/service_designs.dm @@ -661,3 +661,20 @@ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE, ) departmental_flags = DEPARTMENT_BITFLAG_SERVICE + +/datum/design/rdd + name = "Rapid Decoration Device (RDD)" + id = "rdd" + build_type = PROTOLATHE | AWAY_LATHE + materials = list( + /datum/material/iron = SHEET_MATERIAL_AMOUNT * 8, + /datum/material/plastic = SHEET_MATERIAL_AMOUNT * 4, + /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2, + ) + build_path = /obj/item/construction/rdd/loaded + category = list( + RND_CATEGORY_INITIAL, + RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE, + ) + departmental_flags = DEPARTMENT_BITFLAG_SERVICE + diff --git a/code/modules/research/techweb/nodes/service_nodes.dm b/code/modules/research/techweb/nodes/service_nodes.dm index c0a07e72c68d..c1e5abb7bd11 100644 --- a/code/modules/research/techweb/nodes/service_nodes.dm +++ b/code/modules/research/techweb/nodes/service_nodes.dm @@ -38,6 +38,7 @@ "water_balloon", "ticket_machine", "radio_entertainment", + "rdd", "photocopier", ) diff --git a/icons/mob/inhands/equipment/tools_lefthand.dmi b/icons/mob/inhands/equipment/tools_lefthand.dmi index adaedb6483f79bbf0225eb8998a67206b846bc03..5c0cf513166bd3c698be488c370340234216cb8e 100644 GIT binary patch literal 22686 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 literal 20999 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 Q&~}2Z8ChPfH9#c(9c|mP9{>OV literal 22885 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

aJnDAI+#HzV(5H@e8S-rKFU%aeTK2+t+k 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 diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi index 0f1f57eb860ab277053fe45aab34ed6bf13da544..4c8209021996cf637cb812d6438049ee13bd6bcd 100644 GIT binary patch literal 32204 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

{8>{`7xGHEKyFpzhe(k&{`pc+{3RE{%Y) zd6*tXeBmO_g{n?d!w^xs$+4hI@Mn_j%E-|)B3A-h8J~Kg-WBb7D|zkx+f)=HkZ2q~ zeCGEKVgIr1e^B!56w{HAUX7)R6t~-5 z`rp@!6))OOle1kk!oFZx8q|9S2fHh-`O!^$?C|B-aQ?oc5=LG`TnunH!lxE5odoIq zrkUQ9-CY^+pDR~j`s&Tf<KMIc<}4JY%5nRCX4rogh#BO4a)ni`(|LR?M74^;YF(P+ zdDeT1{Z%}Z1`;^Gi*bI=)7{%Aj~y{nNc*d&DF5w9xjE{YVwkHn*Q$-nq8FdfI9ukU zk=usI#Q70xDBe4KQfgkS#lSiNR+zoh;Z}hBJC_e*gh#yx1Y{ zIo~9#*!dpsZq~El8x4!a{2_2Z>lGvs)z=>}c{vfuu2?Zz_+uUN!HOuL2y3h{D8?fO0bP#w$ z7-ieoNXNK%fyGdW5vie^X7cvk=C37r4h|sSIcV<)S&*Dt5CHy9REtxMugw557~iNh z`fS}keFx}D)nLOuY9>c7%iqz0=3em6j?fdDTdmSJLu&o2e_a7fnl@awO zG-nYR&Btal?3yNJ4NJesTJoJFm6^PpC_a=ra~kWG$pB`ubh^t1KE3J9!`i&eUaGa6 zjW*5uxoHK^kz^`)d|yGvl$9y7PbOr{zu=b3p>?~646o76Al@jrAK>%9WwIC@t65vk zkCAB?5QRo$hAVnozZHxW{;IAT-$$Nm}7$&(^! zllBQPR-GkwTvaysID16`==$Bh%cnJL_(q~6;w&v>Oz6ct)m^T~X~n>+c3 zu}}9S!0hzg%OM=C`!K)3ld_^Z4wePjs3J^Xnu#`UkSHM`rxfIxW`y~Ox%fcoq~538Vq9bNWc-C~LdJogykouinV{rERW-gh+bdY46O7BIPGC=a-a?_zf6> zKYaTZ$jSEs3Ot1^=2*)x4W|*Wgb;GMGT-hG>JT(D?Lo_vHT%NnCxCDWwT%VZ&vJzN z6lznt7*kc=Q{DT=!xW6bIq^MbMlTAxE!+L-+vMq+=Y9)L7cTGO*;+<)=&*#sXu3FH zEV5}bBu(r?mdkjsQ2mL3Km#x>EVlXqgXvqirL;aK_3>pM9Nczs3-ZSuQoW-7`51p$ zYzp^p-1pn?jX#-n;$wv&@shF>#3C3Loq^iOxdXxT?{?uw&#utynpdkaVVWO;G1!Kc z(-*{=0Wl%IGsdHFNE{}5oS1o#MgKY*TB-0zpp$#w6*F{58By#&Y_kCzFGOt{*f~YW z9+kUx9W!lU+kO)r5L^2E!T(#_2jZrMat=N-HydVw@WFE#*Yx($TEe_2Ff z7EaxNWOAM7%PqX_=s==RQBN@wX_q$vx>7pHR5P9^#cpV`Bj;6AyM>h z2{1#}5Yk6-dXHdkYp^2j^09j|2=oEQSC^QG zkz|?_3d}gJYZx1Xbbw_(W*M6!n@IBufGo$I-N1R#x_wsGJ+AdvSN<+bs9_?hbzPnv zqn4@0{QLIZRR|9w(Lam`ojjQN1?E>Mz=3Z6q^pmXH3Q!rX(~-n3Pw4#xKO%*I96Gb zc}$XcD6y!*!tq^B;zsi9E|kYlpan=Ns^mX1B=jm{g>1({i(Yn-Wl4ndCOnh5y#s#} zt3op+szcni8(PuLyI(;J(kDW@)zFOSm3bp-W3KTntvaQxR^`Rmn8I7EC+sS&7BG%=U}DFj z3%EmoaS)%3{p%I6C?<8$K)D-?xD8j?%_>{3 zuUi^qXZ+!$_cVaEp&~fNt@!J&Jzq_FnNbHtKWrZUZutOTBc_6}sR@yokK$7J!c&4SD>Z zkHy>&-Gn+Q7*O|tKj%T^I*sS3wYqRD?AA!JeThj+*mvA7~sgnmA1>o!pgj* zUczo>|J8X+k7fVbhiySTXJ>>CSA6|ffwt1Q<(XK%oA-G(jJk*ziN7XKQfRRVDE~$A z@C1legLw@_!eHROfr`*Znc6JfoviU^Q7cXL$L#>&X>*9e?Emc$Y~6iI73%6f=83a# z;B;1c8U>H#QV@$EH7>!-`{l~l&TiT*0MiI;HMq`G=* zXhvYv*-Ea$K5|Nz|DP2gM4ksH_LIC=98gxgNonhaW7-KRd1;~TbkRq1@9uSmd>Z3s zN-YJ=|Mv{E_&91!D44J(04AcDqY}AoCQ(E_#Ff6bYJ6n+a2t^FNajx3>6K1d7y^h6 z;rpANp93SGb=`RghM#^fV7gg%>h5&6;ireZ_Qm3MWupl_du5++%&45k5*FX05%Z$bO---l>*AwPq z##jC2r|r)%CxH_(u9ddg(eSzZVT&hd$5xWy{h(c`v{EV5@Z=aKZUM=tEWxU@K6M#l zu%0e0@>iisUl1w2Bv?8B!d6z$>-P31QAgz+w`1wC$W|RbhtqppkET;PIDztaryzfz zr9A%S2*?y)`?c+6k@rj8Bp>DIKYXYc?s>bIO|ig1VJ&U~e&j=o)y183Uus1oi{0${ zsgZ8PX^i$G%7epDBMV(57FrYtmFn@;H z8e6~4=_K0?EqDR@5_k&#nVbdO_4^VA;Ce?|MFB7ty-^hC>3lp!^PCIM)^+%5_~`bi z*)t=gElR}S$18MBSg*xrk-_0oh?R6<%QTC)KWaDCt^{<~j_ZeXSP{pqk*K;or|3yh zb8A=f$u(P|m;RJD_*YU$984y5_U2D%Wh}P_Qw0*RNm_dEzUh(S$4=bReaNU-?)T`v z_)R8*-4#PQL^W&|hvf1nYS~_z-#AYQELs@D2|{`{uM6{RS>oW#dMvf7koVZ;j48-F=U%C9b|pUt z;E0-dqVvp@jI84Ke>TE*kgrR4`At>C$8nk=sC{FO7b+n2lU{LSN5kXOdF_WWUcZGE zF5N5TRCA!A`nY_t+^qSfVv>`5smv6D+=a;wnpv75Jn~~or7)88k zdx_G22+#obw;?B(g{zb4HtiC#bXi0@pzkIh4tzJf$`6W00 z!nRYq2nF@;gOzq%qxmrigsr*GeORu3aoQVNL@cd$(}DXz69g{+|`MvEfK zLL9H0k*7=3p7(U~>FORi(qbLAYdYtpCtp4M-b**!_h3~kwk^8FSagNp>%Qm z?GjjTLCl)Ip)=TP!22rhXHo~zQBmb(pE<4YLSRL{A9ih3|4kFdPTL74%p z?6(9z?%islx|Pkr!O^=4MQELl6^2EcF17rR!$mgw=$$KGX7QW0o9MXj5TRQPR<;@D zFrboWZnpN0T6GJKiq5K+z6&H_WI%e_-koJ1va))n`+pi92LW!&+WE_c*LTJx4@%1H z8guZMXTCS*YnxOfKCNq=XZ9z5?=PVQJhC~K1^<|Vz33>-xB@yx>zvL;_@;BhyR}d7HvF7c2DRfXDE;+no_lYD ze}1wazT6e1JyrP*L#Ob$W}0Hc*B8jBY?5Q>_E(`>0oIx=zCN{= zI}PTY;S?Jpfb8|_W4Xkwuz<^xN3^DwH!%!Sgrcj1*p(XCCk&bs(@ad;mrB3IY38{z zBt4(?W=hoLcE@1;wf#uRxU^EjuPn{_fu&ph`wogQ{M>WUl|owrGoK3H=A(Zw`n*=) zb=@k^%MSc58%)T+RaoBab1Byw4`4-$6cj2R^uF|0I#?Ur)<-s-YCz;1vH)Q^#^lL_ zsl^yuNzKL)M$>7G7=;WqV2z}d!8NS0+YWariH1jmOm}EJ#7jvqSQ^NwaEYB4>W__* zUUN^#)$nOtDu>tl{tuF4)wMa*!cgrUE?VRHr1u*A2O@F_OUWR#V=XoMx5#e0LiCnRuro_@92$q5P~@=%TeamdbJfFd zg)-4g{c1=luOrQj=rW2})o#MUf`M3_B0Z9Uc){i@?t2ls3SPUnb!(3})jML+Uns#& z&fHHmf`SR?mlK` zIB^_Q1+6#w_W500s3|wEajDu!Sen&fnLbjcCsDC&JtzAcK7XT0D{Mu~$oRrtsU&Sr<%M9as4><9 z;x0Z|aJaqRAqSgT;^3xfGq|XpkSeA>z}`^Cl9fSjig68e^rOl`BC}|TsO*F zNz%9J*vP^3`ukp&cV1B7tB>o+&JxY{0PWf{Wb>!w4laCIEi*Q0iw>OWmJp?AdM!$R z6C0ze``TYPjcV+4O!c@C^*#!x0s?!{SYgC?1fUQ%ZTn%et3~xgUv8F<=Z1V zmrI)uw!1B!H`@!ZkW!jx{ns^?!;p0RyKabp%bb0=*AlT36o8f`GODWp*dPIo50Vxp zWi&?}VPqE4^kChR-w1=-OO1_3i9`8|WM)9vt64k5yLS`&sg%$F5z52Fax>9X<{@oc zG~vGHhU6!U13B+_>h@}WWn5{wU%YnEsQhn9kokNT_89ID)LCaE10?>`5Bm2NlJ`0I z*|F=d`L~ubn5#+yK|OTFCIMN$cqmD?4uj7n;t~Ld z-Y&Pv?zkO)_%WOSj?E%XgUL2(g8s3(8R1G*xJ5b2L-#cQnOvJ?I+Kbr@Q4?@Nfd{k zm6uNlKuvx~f>+*&rU!3+IeE040;-rQ)I6cAxSBXYb&Q3{3aEEa=)z6Nvga<}g4to5 zPv7%6RCo}eOZq$OZKtcltbV9~-8gTfa`y`^>n&u#o?%6CNb^((5m(lwG|I*%~tx#c8#I!huJ0D3pL%=0#20K&?u zXL;7RQ&xpb96Jr{X5kcOVAQcn9>VN+wGxyWYA*!p3`7n zr<=f<^5UN7*5hNLK;iBdveC42!@_5>IT(tUYY-x_%7VIOvmHc2E@*=Dwqu-e7|R(C*7+7Ec){i{#yg0BvpT7O2%Lo61n zS>fSP)SVL_ur2j~wOau?wUdNJg=CWSe>3d|cNVU=DWEygToKLV6vQ8J>`!&fGQ!Iy zu9Wth7!kQZ9|C&jVd4LbXU&8OvGlBqw%mC(IU0v}d9ZIUjTj&iRRP={i>k4Y!(;-k zJ1!N{rhZ8Cexl(II5DwTv1Sm395b?&Zs+8Pn<_<}zP3_p1G6PSo%>(lNpW}qz%a7X z;pkS3TnZu0*7;N3p#f2^nAlTYMd;Mw+0}BERfcdxXWdLvQyva>?s6 zD07mpR?qBU#Ha7h=DUF|#m`H3BXq4sz-F;a6HMr3(j}8)r%gqq&g2Kod9gX@6G)|C z^M@fz^8B916N7ji$6^p6dZ4?~x|rLnrA+*lT3JpEC9XY^6!XyJN&OT_A9h_+XZpuF zEjtrQ;L#tAx`puRfcB0vE%^424m* zu|`g&UTsI;P8aInny4r!@}FBT=TU-#4{r?6Dv(FMRgF`(!Q&HQs6Vwpunbu2LVGOQ zNpd)4Uno&z^F00r87q}Pa(W4Hlqtwn07S~z;c@;Tfzpem-gPIzm(y;>L5*^-As)95 z;I@U8_heFZ6cDvzvu0vXmgs3IX$0h7w&56q*(4qFSbWgB-Q6pN?y3AuWck{;pmxt& zo>sa%*%$Snzx+kXf1N;14j+O5_vOq%D$BmS3IQ8u)&+4i^cYHwUp%%`WQ=i#?fW~1 znK(4w|E|+gtqvC;e~0SI#o++vvb!_5`J_*z!fvvAm7ao;P_97C^OHV1*!J9^JU75> zCVcljs3JAK)bU*vsJ)FBkT1(nLPI$|j*jaSOL920qQTKPdw|hX*Xwv=)e>1xXqg|b zUP{eU0+MHX`Zx8LS`hlD&DGG_&UXnKiPJ*tJ}i^I9*l>iE5aT{nlO9Dm~;WMweR6hHX< zXjFqC&a}9LsNSCab(jM4Kja-1;d29-V~k;VNPu=^tFOci*7;r&)p#49T^I0i6Zm_s z#C0d7<5B6bd9XXSh*5mMHHC+HH|15o#aJ`F6W~6-peOQ zY^$(m-+O93?W-YOv)tb(`m(^uWgY*)ZZLp&v&U3~2suYEB@zouEph%Z_Kx|^9kru_ z{h3-;?-h(4!Ev{2)o!i&enAF*bj6u*o_HtfgH%e!mjULpG;Az!V^Y^OzY8v8(Yg`!F%JsBC9ji$A zTvzcwCqS$tHo0buq=7=!XNp*|JpWn-IOfrlQRk0SBe?l5Q5m*AnkYcKX;rIA0FZ z=5%rQqTyH}ETJifkud`YZHaA7%@orYb<+02lnyG9D4O=z;qb7{xmLNIGL-`_(r{z`%5Ta zBDziCt)EDv5N>@xU(7+D$8~l&>*;ivmzn{FpTY9qBYY@VrlF^TKoyqF#CFc=a3zAR za1xph+Ke43Rm>+EspLgsV61$0LoeSgpY!Z9QsLAzJ zb#e(2bGVcO`^(Hmnx%!ACkWzuZS?lnC8}QBolG7he7>|^BSbzCLObt*9keGmvV1pE zN=hC2!^d{{0ENiz`VEG42UoH{doI+=a5VIDVx`eG2~h()ga>uJb0-{#5DU|&yVQNV zOm}~6EyFZckqXimi><%ODG`;D=M1<^M;|1nsc(}}@{JQwq^9`};8|Q@(*y!#a@vdJ zmzC)qOX2D%-H*(87m10aNX5p!Q;;g2MEy0#-tODPojQwXww)fSWv=){o$z3^4$^+@tCNt|IE&V}K zgw4tEO7G03qO?DVoPY4(-P=Qr-pwOoV`E?BE>VPUIhqF(r}HCa9Nk@HPG>)yWpi?l zSU^X^-Nl6mfhN~TT92m6Zd+ustI$~#%( zGha9Zk2!MQ8?!`tL>20TF}du7NYlPD28LoEUtS2b$E>W(s{%$0WdV2q#2@|M zH{O)aQWj!0PY0AU9QZTWarSuNiyo!q~A_Z`g*<7b}{S-lPEZ zBgt2ewu`+O|Ia8v@f1$*VQl>`V7?>&F!kxPuA`x$X^J0x+4)mlhjN{B5lSBYxI!TA ziDSevNkR!o_#M|OHZ?vbwJ`z~Mrgg? zgIAAFC@iQ6*z-lTiu@1k(*C`mH8FSS3BUmx{2`Oo9!!cRPtc!SHi+s%AF$z#Y+jF0 zm5auh1ZGk{40#%5xN_NPT%BXXixP#Nl6B7hnFV^tRZ0sPM6v6`o^Oo0yn`3T4$%`s zd^NN6&(k50GJjIR^FelBQxo09yPo+dIsIT@32cIYSQ;MA2yG{d|_DeM;9+6vm zDeze?_@P6@4h7M1QHz`c2)JC74Z#p*b{igK#F$=r?2tc-Q1KbmsDEsqk_7b+*8iJN z_-_|6bq4bS-so+&??Q|3P3%%0i~c*`MdZ#fMfi8v9Jv3QGT_|=8gjEY<4{IJJT7Tr z(cqNWjXhtB|D(U$wf%;_WK@8d#@*q zX$1V|a7n~Q_0n~~F!4hXTb=C#t$K&pJniJi&{8vh4Oa zb22C(q+Ey&7eOun&SCu)v4(PKn}o%H zFBf7}MJ7!qwY)05uW8E1qSSSOhlseiOP3PZab5|P(Q*_}DtIs-qWNIOS^u>zA7%Oh z+C3QOP3HAzd^_tp1hE8q#wm1^1EqtyuQ2GM=~%>+qJQ8vhjc7QLv-4>zk<%d6z6r_ zBQ5gaXKKZuuSgLwGGsWr?!ghwl^q$UAG2AQC2xa~`YXN$cwbq>r+SIa!sAN71yGB0 zQX3Z(@&geH@ABVug;ht4&I>>Sb{~*CmBH{5IFnHuV%&&7D6F#WQ1+Ukv!E9%UR5(h zoO!+<-Y(j&=IadluYKf#Lq(j|Faf**SPBX`t^l@!_nNivTC*Rk)#~va@I`2W_^i(wWLHm1~K?_SkME1 z!J(Xx9#93!6gkT5d(vXFcC**$7kFF~PQKwR;FmrVlg zvE)g3@5u#Ww_j)Zy_eRMH1KxQ+fWwCsglILMCT5oVhYj@{1EPkDC664^VPmEbv)`@^}$#+#Id{b75U!GpG=I0HcVAGQr~z4e}aF--Coz~<)j3N zO-aFQBiy%O_nAAS?u(+{TG<40A>Sq>B#;ey{3Ww>&GG-8SVQ{WK;Jir(nB+Zo~JR) zHt4%V0Mq|zo*nmxMQ3;3p6J~SBXHOXQmBBm+e6?bvp`{5pU63;dcmHdzicFVNv*KE zMzo_4Y5TQV_-muN?W0?w`Lb8H;Tk2RmXXuE5M6tfhpO-=({t!wuVEDio=J+ON4umE zKjnQsp+y6CSS{!CqGD)j05*I(>2kbLaE-;*qP5)dM|1OFHj!VX9WJ{rvQFhH^gzy+ zRD$zCK~d=^eF4SuEB}J?UIyErt|V)xh^u&*K!B)}EkkQ?O6O@b6@?>k-1Bl_^(`nZ zgEv!`yFHQ@uwv~WL@wcK>EBnzVFOmJqc%I5@&hl3R>zh*Lz%9T5RJ$B@|@5%N0enV zNPc^I--)`i;S9lZXWDw=TD|&;|2m+!BVcgkh^o_sGJuC~L#ugcTC-+>u+2}g&X#)Z zm9OVTk@0wqN#u{H*2Z{~R)7rd@v(sEmX8OeT{}7H2FJ^i>+dlZIy?Z56_kAtQ9n9iAzuSR^84_~Yo}lOd$D@KkA5&m&^N(l(wB;6F zbh|TBkk!Jx6T*gVU!g-aarU5Bn_){$LkCO650RfI7lK{Jv9;e~IdiK)X z@225qDQDM^S}K~j(8TXCFc71F3Z2-HyP6;poMUZlgBzmk?dk3uO?X`H8jyd)sX!tm z^WZy4uA{L|61skzKNfV@>b{6w&--w?1Ox<|nQ5NS*@s@wK-+BVw0{me#FW<;-cYHr z7xx^gM#LQm&E78EF{pqZTg-Q9V@THMSyDkku#Rg}2hvbMcw$40h`X;aK3{5n^qJF^ zl(F453k&!n+u?-9gVMR|UMbvaT&`4r0kj325Jygi^$?JNostn)AgEb5^nAj?&N!fr zOqH0ZBOm5Np#W1YA|>8*hFIY*9xZ=Yk5NmOFRe7Q2)Aq2n`ZMF7d6#Y8y07w8yj0c zfPkv-tM196!A=1H-Ey%-+qWig*V)g8_DZ68^BBcGF8iF3uIeQ3$N?7Etk@0(FTp3z zH3T%uSa}qku^~DEyb@Db*k9ie_^QgqYX?pRG!n@dLgC{&G2h8+tjzp|o562N)M@i; zHQqX2(bbTa{xMfA`kZ!XapKZ}>UEG1|FWaeOZfM;+rtGpavZPUubWhMIt^$ZScd;_46exFjG_Co)|kdWu4fz-LMde@DPT2weGXoQeCBK`mr22 ziQMMfy9woIt@R=2Ebp}#h-CX)lZt1h9eisT^cL$ra3v<{_&jA$^Km*y+aMghIu@Qc z^$I6Z*f+ESg_GC$QqA2~FQF_i<2`5AiT@^r4iXbPXK9RrtS_7B!|n8}$OP47!Z{Mw zyQSC*lumuE);FRj6RFIOHovb{CV545_E;e$JXZUwb;l#Rvs(*3jCaWDt%7O({^kXNAiUN{wcffoyh*FkD!|8PtuxA{E z2b0+my#*0fQ9^Tcs-44fKT-!_RnN}2GWuLi7aaxH&S04Ww!}q04vOE~7K@xJQ5-{z z>ud~h4%Gnqw#ua?S`Lmx6Q#a5p6y^M&?n%+8@;?upv?NPQ}-*$?gfhe@0MsTrKh1; z5t@!a?a#@L`aGTV=7)bDB_4wJpZ$&iw5L}5{oY_XOFO)=R~K1emDC7YU!8q|%s zE~4l^smgyLf;e49MYG25{;qdw@4tyF7lp=$U+B0w0@(cKjiMXwu6JNe@BT9Yq^x}C z`fSH_pS2tLAu@U^pTXCx@gtCte_$A_$ob{)3)F8AljZemfRM#BLx>JMF?;e2w|VSL zGYx_lP5&~5Ly4q^HNVx01>}b$CUwHg?O=_g3%sMVB%Rque@0?+<0Ho=Pm&+l(1|(Q zG8>)tc)}plurl@g%dcM~Q#{mA7G-m1=WiL1*YULb&d@VqbyAkm7DIPPNxRhsSVu0k z!fM6i?q#9x*@$;iIL&P85E88-e^>q7#sW?q2r1XDUce(D7{1NOFK4>F<{XBwDH750 zgQ4RYNUUcbPc4Gq{kD!%1cR;LJHDFf)i>HnDl>1hy4$J9N3Kj;2?cL7Ui$@t`uE&( z?7%uc+?o2hnEWli(cGn07hjXCBIwB6uU?K?qlFYGlr}k1q5Tt@>aET&P*K~PTAQ6} zx9oI>hmzS`L%`59w^=jQ>9!GoJP`6KAlLslC}A}XRn%9>^PWd%+tI_ylXq$>F)*Xc zloR2+h{#s8#$)uP!_xeeVl(D@{FU9ZQA?$oWSr4^RW1be7-Sn%Whk+pX7DWfEfKDJ zEWbGTxbN6CECK+w)u-I{fx;KB2H-yuzWr4?!m*T}5A6SKA_O3ku{wL|h6ybdoGapk z(mV71PwoxxZ|||NNGb}KuF@*pF77E1td}r4n|Jm8>i=TsO3D8I_UXI)_|SAXA`%Q) z2qF^WgtDE?*zP}=k8e#Aw9wB2>96-k+Vl)3_$%v2NP0g-373E#f=n47?e6KPHeGr? z^Lbg38EW)RYwCG2NT)Oi!wE#<(1xO>Lzui*Gj-POb1zP{I>e z7Y0gFY)nObiZ}J9Qa>f%Fd?kMUr;3FrpcqRb8-TsGS}Ym#IeWb={UX{KI5kDqkC|f zH)G?^1~Eo=-j=&B&A;VFXGb713`!z}ebOrQ&PNK4y+7RN*$>FwhhizhN7-qN>~PCf zq&+@UMRs2V)K9g68U|vKAQbukfHm7761jR;NlCJY8?vCzYy{kU0du0Q9rOq}wyK%+ z9iEN7mA5$Ki#5ZA)zgR%+YW1*k9|!}<%uSPljN)jtLWPjr%s?=XjuNh#ltgn#uX5< z{9$`*yoPT9z3tN*yc;x^>K_`RZ+z=F=Y}u1svIKVlDB-x!`s1sM(~m9fP9Q)l%g%C zzvnrya-VSk`FikiWSfU|5M_zwk3eVB*=jc&L$ic6 z&uqd-a{TyemdUW&)z#s_j0{E02$8r&1vuatugZ?9r}TmM(HMHR=lW3Kq&=72^bs&^ zn;FuVe|YSWcBr0Xisv;s6fQIjo@}*S;HX@Se1A8pIcDCsrA;~1_DWY*8+Gub(VN?Z zZ`%sG`)=-+($`C$X%cs`-ndObX(sue71fHh) zE7j?e;<~t=h2-U7=`IMJEZ#dI-nA`VlU^+q63^Qm=XCZQJ-y8|O4ZMa6sb+n!w_XD>t|JspOF&PV!;dvkVd?Vqo1ZjH za6ap4C*GfEFXRGl#^Nd>A2kvCBNt>P*Xu`lr|)e)`PI;KLLSyYwQ#_tG2JBmQ%Zwb z`WPT(*AjY;*jBI3E9MNj`#Jie9S`>|mDOPQV74v_E&xR9(ZnNLCEo$uRvE!`J()q3$-OA?HJ8tVx^_%R~%y$FVc*%|8u_f)(a8?Vmwe!0;H0dB?1A* z?RHiH*N`dmyKP!!jD*2K4rISCr*9rr{q9rB4;%Oz2SZ@?*qFSF#V91ar}h5m3) zmhkSl*NOx+jxWK73=}pTV9c`s0|*)TNopcqzkAT#CBOHi#y5C4Gs!aV-JJ6N3#!ol zQ67FX@gQLL*&uI70dhJ4IfN`Nb^o}|K~*QSX6{Z{+N=OFQ9$L58sW`f)S7<|usZ&i<(TpK{`H8}Bq9MHuC9Lg+;CxJL<$!8@cjjjYYn6QLSv8(L#YQx34)kU7HZ!x7&VI6vqCN+9FgF| z1W0u!h*XTLsM?Ak-3ewn=E2Bf*xwT{fFYm!pl>W`<+ac{Lx}kUfwMz*cfpTAY(S)P zt9E;Aj;FAzn&P`(R2|eedzo}OcXh`*p1Q819AXZx1 zqLRZ?n-YM%-y(ck1(3Up-}AqT;L!c+-(S6g)N|XEIS&(XP$Hhm!JSFUZjS!(ZC+2< zb|4U-MZxl2i}vftG<#R$^kWBMc>Se|i2AJ_I*6e>X+6LM*#0_74~$^nO>y#tZG9A{ zMEypFTN8Z{P5tn0xLwXnF!VKLMpf9NAG>|-4jOcgs0`X>8`&u|-g+F1%s^~Xca~o5 zo$;2;;Nph0#kpJ%10vN9g!k2J&Oc^9&jE)ID8piZ(!iB=e?gV??jvW0rADu0Hn$q0 zYY}4Arzdxf1parbw)YW#Q0Y}E<^%2%$7q@{A~KKAYy>+1@8jhay8d z&7>Qc_ACczDG(460&hRK`5OpeH|e7m5ST4Le?_OZOp+FPKFZly53WI1=6z4p(={^S zc(BX;?5?`MEa4DoyV_^o>YJTx9vz>Z;nZ7LY_%T@n~~?_(J2sq_%l`cN#)N^e9I9y zf5ZJylXR~Qy!)r0DrYF_QU(SCJ8|lugTMb_hGy>D_-^L%uQZyifJ7#ddd3myGCY7t zXKCZ>Eol!&c}8=VbXtq0ScfEBaM%*wz ztn6-niXSZj4=DMu3zV8CX}bNT4nyz?v=j}DxXE{CX)l^E0*2y-6kI7RyC*Z!%Azl@fSbcTMl`yn4uE=WK*aEiw?h z8@S4#Q0R)KsydYl8#vV1RsQneh!sGCdUjUrCYOC|OQ%NQ?)NBO6WbA4nglbDaWjeJ zE22x!*Lx^i4MJueng*Eon}nOU%9}{}8wi8Z3PY(_^{@43eUj(492HK+gC^&pH6zl zLcF~Ti*P;`l)8x*|!B%aehwQK{vH{ zD#cRHe#be9z<+uHs^oH+WSqA9N3JhGKWM8=roEKg#%)OR7=a{`HV(+{&b@|97Tlc- zr#L)9y^XO(0ul+mPnWg^x2L$LbgV~AWjaP-(w;Ia5 zEGR>F0;LV5bHkVJ;xnX39O)oiJmMvQzHEd-GcUKTuJ~Gu%|8`n=)ydPlo&8XSbUob z^`*dMjo}%sCU+rR+s&!{`dxLzs)G;mUGZ*3cpwl@DPOFB3i*43skh;5igJi-7OhSm zXVXPluw>8x3{Gd7)(1`6{xCVg>fb-%0Jw|2n+DJFx?v_9SJC?luoOK$i2ov*p#CL{Ct{YK#wA~LX_m#Di5sz)o$)|mlVF(6~)LEXDe zrrtQ$ibb!!?0>NMz4&>H70Z=FBq$z@$HFA5PaAHTzTTg#7(eN(wL9-}C&o z?yktY!6}B`f`$=9$rMN}vYN1YaJ7jhDQ)Tpk|MBRB@L|crhyf#e~w_~m`Oob? zR!oWok4k>D>XvxpO44t_V3D^e9)^eVIot+*9=626h5_C!DAzqI z(<}xUTd=Iynu&ef!~JKJWg4mdp;uxdYUz35I0qEe)bpZS>51hM!520fcW8UhFoty` zswxHLSA_yNA7*R202MlEXH2;)D&a?&fqq+L&TP;@H2(CYZk*#3xTEoLqh|X<3>;si zX#;8#!nAa1!0SpTKOt1NoCu?zVQP67V8uRHFUo}Pvd_)T%N-C=J$}!%S1XoyFT80W z!`Fap0Ep~~-}@iVJ>Xx{ZmX5;PR{~psAyvYlaq#b{RwM)zzozEm)L~-Q%+L|E~)%& zX`x5U!dmBTUD-7H@QR$Ydn+5HN;~I$Qi*e=eJbQr#%X8sQ&ldpX?wyZ2VllG0$CV;orI=c@1;0IIdQVEIngzE6*?TFGcejo zWes1q34tlGk+->Z4R(gygy7a5Yn?}v!sO4gEP8#C4*$O<;5+#;)qqHWQIXk6*HADJ zqVaImmgs{7R(l@C3YQ4ZpQgSl&OkjUu-ra7plPc0W<>V^z#};>j(|CBYOktfIJXjE zEt%^P5nyC@bj7{Q8T@h70Uxv0a_mU&KGga12s#^lpKkb79(Y z_+WkI>b^A%;!Re$$v}(803tMAeaK{~fwJ3~i%nPRi-Xb)LJ502p~Oe`kbEcrgxv8V z$H;fXQ;!*v0zZ%=L?o`=f)Dn8PFD)kMEU7mTW%{JauX5)B5CHotEnIqKy`y^=HK1_ ztYmY(>*e@hzpgwH;?EEH_ipl9KE71to9kcMQD=|W8 z9-7~?SBx>t+(}4?RM%c#+-luoE{9`M5hdb0UX`CxRyKAeImhqAn+35SShuvlAn`1@ z+CJWt)(wx<2Lcd?@9T-o;OSFPcK5Qzr{cbY(IoyOm-r05zQx>&Ds@V>Kclvq@TnQv zYV?FeyV0QPgDSf%VF-fvnbs3tz=yr$&#h~q8T5(a%_=V*{*DA?eHh5rF4dl~ZEYSz z`NWU_@l{HD5{4K`{;Tt5*;@g^9&e2}k$_ydTg^%MhkGA3Z~9RP10mqpb78uXt8wCP z`;7PBEqq^C&(jTH-N?9%_1>u2%*xNBFqqGH$RT-|k_MSi9}(Xy5R{7H3$yD&S`8*g zVzVA)Pntf7cpIL@m|HG1^#di-ECBSK0gU&j%q8BPmm5kxH6@RC*O*1eD2J%7Q^a_9 zAFj-}dWsw%s{O<;yL+S)EJGvW;~jbE!)C>=d5U+jN#)J0+5eN9Y%Z1Y$LSfgLFL~( z%u){fK;~O@*qx`sb(Nf`v;~?AzBXdB42QY?`Lg7AE*Ir*fo z^vV9f0;4-=FMJZ&wAg;_%2rFk>4Md%o*+N*=O_*YR2FoY{Lylcfx`KOApgnhWwTQ) z!vnAl6fUMKKXH1_0{==Zh8Y=CzaEjK_-us$09F#p3-N z44eS1B4H;dpGBIf>1+JYOf}e1$K(Xv^1Zx_Q|)Vxn31d-?Nl09_wvh8QmmNPfmm*2 z*yuRGDek05{AcqD^WYg_8zPMfb}&`$|Iikdkati{h#1I#9KEPj&=faSJL5^fT6 zk=rBNyrzfa^(o#kzjdm`D|7TEqK+aC)Ek~h#xnn>cVKndT=<5ByWaj`pz_>6VP?n^ z{+9Y^87JoeROe;$Vv;4vxQg1`Mk+GkbJdY&$!nJ#V)Q4$qczZ{HLlK*vSR4QB;zfc zci@>?Y#HuUX9@KFCvP|>b$L0wplUvi5P3n~?9~YxG|fQQXpse5ucuw8^gcg4&n5e) z!^zL8*b~>=BKVt6)=)JoJ8BV==zH4|9d?gzT7hmM=@V3 z;OuoF1@SJa@@$T`NaBj8)$X^0avNB01(l_)!MKRWMEqvga}H+}wiYK77qGC^QB;(E z+liL#CnJ8+=BP5ANhTRy+)SY^=@yN!XZ-+@^>XI+gp@`z4C6$w*si5~uF` zf)g)O;)=9mLWm zLe<0cF)^RpZ_>ooXa}aWg3;U3im7fZ5;`A49O|djQJA%AvXZn7e4g$u4}#Ou?AcZ9 z?JEt%Qnw9k#^LSl?K>au@9#S_qe z(r|8<1ZMZC1Hq7ORs~GA$NyYad-be=fF=;6Vpr8wq`P~17fM*y$trHXac&(3qa_Nj>*1?HI4?vjD|Q^)-Sv ze`l>dKHL%PJ?Xf_HeF$fsQWtS`X`w$I(~HSO7AzCalQ_zez1pf9;Vrhe#vNfujlJ_ z?-rSL_e+`9r&iChow^9x`<&1VF7734 z4$eBFz;_Z64k|4|ajXmjQr(-%i*Qq(1(J;2MqyU*fX3$JMI4A~`_%U<5~lz<()C#pdK6pN zxBMgZk$1_bt0wvh^m^N~b&T?P!Wiv+BFBDMO&4-+{Rw=ztu`@60)TZhvyQeL-owqUHNPfAVZE8gTKmSVJS=)% zUU;>$dv{QLq>EKqt72X(VOM*_>7;^q?TW#x3OYMFK_HIM636q^JXzPNNm{wE5Wu9* z>)y}d*Nn_cCx>9lm|QQ_XMHG&2;Q-*R=RQ{xAw zN4o_ODEBu0kA0=NGF9SUB@4Q;3hijyT)?5oxfAV1*p$-P$y+ew}2#75TTxogSPwBG4E=%u7Gv zP|dc}8k&uFTYUHgDpI4<8lvtS!3@r`(ilId8haVNlb@w`{wtw`eEb(N{}?2wS;0}> zK;#eYXG#6~jgeMmy}Z5|)_fx}oFQ&GKBPkr%ei*8rJ1k}`pTxJ8gcxLCI-YMh|ac- z*xfG!l%AZ{%2WSDdoS;`&r-pcNo}wNecC>9&LD*%y0o~!&glk|TA@j6s~|hTJ&QH- z7pE=~mU(hSR`pEDkokg{r~RNd%xO&!5Mh7~$^r8AW3I(iB@IA`rFj!4)#;|=Ci6Aw zF%I(h)ng+?gAY)AlL7z^4i36)UT9s2Si#>%COT^bFJT+AG4sEr9}p@ypub_ppMqIX zdIz?{78f=+AIXBPh4N%8t)S5g0lnn4sl34olkSE}Z%EH28$wCsw2K|c6o-$ZB z$;!gg3(D?$@4`Wa7jdN_$Z|}GK zzT<)RpU%H0F8{_?#^dW)Iuq=q1F^N7@lvE}`L+35UZZ>Dp=iATlr?E!IZ9lszzF<&U*NF&NW8k*sl8YD zX7@#wLfsW>0~BGT&1{6Zzj=Yg{>ilmjrAPwoSXk|{~#<$6EFMCeK=?@n%|8DHB@yY zz=%*tc@*;HHnns2_B=|&E@mbKv=)iGPE!e0%#R(nut(ZtG(P^)$T6bM&(Wvo$x-Si zVHT^nqAKr(5(&yaB1--wDDbcv<#Sy7u6m8U`P6J1e>rCSs&(tQ17QL4grEU-l2;&r z^>paNmS9J%FCNZp>TV?Eg^|f6bIf1Wl1KNC+X`L|p987TtD@Th;c><5v$dq3e9f!! zvra$ISDtDJ@d<;Pb-7)1>=4aUb)G^A_1Q+_a3+hmUBEd0^tt|<=-fTO6(F>9oGEf*Wint5VvK6~r6kxnvP<}_?n4qY>zpvAOC*~EZTjr#;i5;< zExrPmq;9|Io=Y(0`c?^JjH9KfGp@IsodX}(z4nJSImy<>k-NsnF}mm0zw8DZY+?_x z^iagvrim`fk*mkk|H~yiKWR5u3D*U) zLcs8UD1)-!TdKI1l@w=Hud;PQSb_WtD!=~UAZLJhrj-D-)4c4Vt5lg*1Vr*Cz}~J{ zmj?oIB)4Q;NX-QfAkuvxoVbG<^44%1py?#X29s!Dxxx<)2IzPR1(}jNh6Cn~#ft@M zpOasX`d5A$eu;f_kNnq$eTX1kQ0@L8uP1&o*76Gg`UbH80Ov93WM!2*8;_25iefZ6 z??$jiNQ}c*#gz8c8S47uWZu45v3Pz3I*A#qYo$nM?&#=%)-;|k+telMP5!YcaG_1t z3VW%Id))9N+x{s{4O!OW<5si=4B&s%2m?~>)m-Mdnl7rib=+v)(@)Eth=;5B1VBI& z#1$T3B2Eajlk=vc^TPPwk1Y9824|hmf@>$8Gz8i)HuQg7owSEXO{e?j%x0!&wi(od zNz&4Q#fM@8a=W?@EDVzRxGmef=pTzjckqT+j#>Is5H}9$$$SLGD!E?gbn<`Yq|_h@ zxoZ$3L8oqiMz(;az|i@7kRC<`Lt9ip1PKCDO!~f7o)w%()U=$%u{{p7k?$))J@nUQ6?JjoO1x94est#s2>)-lQW5N z^iCUCKer&PXv)a8>tAH>0AMDP-x;;^a%M())Iw<#Nf0wY@?G!ozlk(nEO#ILzMete z*HSco;QB`rDqw)bw^R3746m1(%cT5a#6HKw#zaiE?LE2yJc(0t+?QMUu~Bxm_MM3m z=bj?9JupwDd2>xmav{?WrfbM_&(v42v4}ZxMrjW+Ij1rk3OEtvhbi(y0I)Z(W{84v z9~l^A?JONuwFgyJq20z!nu9dN( zUvlDp{HE(bjve9eSA%`@TQj2MXXp;uAo@-#G1|#Qy+_9vYYwWW=dLq$Z6rQluSwY@ zny%se?W*AjWOr=Yd|x9D5J1N-jZ@goCBp!fOO~-kUG}@_fR6cvIY{#H?jc!L`fZ7_ zcpU!K0rlg_16^F(IdONZ;e48Zgy8Dv*cf|&1s_b@G}r=%koaFili{w+g#||1diJ0K zbnU5Hty=d_zw5p|R>S;u&GpyrJ51bd<~ZhgSz%eYB16vL&HT?X>Y z%h3-jawR_8!tm)o5&*}miee8WhpS}8Eb7Oy*zj%&ttkLUubSNx|GKRB38r)4Fz&(JBONcWPMevrvX%0z_9|KH2DTda#W%mo;hb`7R=;K*c6m)65bo=Es zynM^sU^qZpTAEKlpuqENbCe((Y(mQ7n=|FJf2bjO^1Oiumh%jlx|_!|R_1Pz{pMS7 zcFSd3cuhW6V=c#6c7IJLIr>ZHdo+YC+kb=4a3ZfhO^@u|ZAn7}fl!!gR1$7(?d(I& zECgOIZgAefecC!6AKd}~Le}iAA5<2OW}#@2hTlQ` z2zw-bB*z{fijI9fU^J#=3b6pI(!PWoawX6C6M-ejMCJRoiJcegUB@@gambz5H#*fg zCLwDwP?bV1$VmD2pz;GnL+Y;3lhg-|_ZJs={&q(OYM6$Luz*N&uTO%h6@vkRj(V^3 zjLlOmXqQ{(3_yTDnVA{;`fEOKxGHq=xWJewHtS7~9&b(pR2Y9i?DsN1|=0MR;kfexZn42#3XV1Xps{ZH$ym+l0{zo z;k!7iQANt3f!c6e-e6lWLe})_`j*p3L+YNMo=zu53V8(J8u7R%7JQ$6ci%!I=Qyvo zV>0B=Hh-@i9%7oCPmTKDPjB9fiR7z+cKVY2HLR-}m(Rax8-6DvxJJOBMI$jK0(+@g zv)Zr-7WmGs%_n#|{RZUi+}tql?|ob{8#Tg93I8NzIo2);>e&t}7($XUSAS1J~z5V*n&ZM+&P_>4vTgY@czCtU$AhE@x zY~RXy64>3)F>b^QlF(m7Y2w}|-GrfcwjJ-2m1~;o(;$!Y8i1bRK7!#kM(Ev=&x521 zy)yuRRvP>4Y3(}rxf~YBVq;q-tn0Mzuh-%PwxG(y9W`sO&OQr(dvnc#{{x9b1eeb!B2OOSEkONE7h(n9?vcWBGvXgJHmmWf@e!YRII!69b4IeLjmzALD=n^Umv=YXKXzDgPiNSg0%#ghFSvIX*|{jbV}S@< zO;AWvFARa^;Y~MPclz-r{UoLw;;|laxseE16Os`BtPWPXDCBa6La%uy|E3m0ksj=f zsXNKbdGc~w_uBr&n}RPsfV|4K1MV8ZicQ_k`M#b%NC1gBl})cLtdL@B9-Jt+qIE4+ zWQ7o9#r+fpcMXDdBZ-H#h1L@~QhrsTF+3-#B)iO;zqP`owY37aR>I)e-G#{iscxk& zpe8?WDJFUGUyZ;2KVHUc$OB3cr8guzX#u@w^w{eF91nxX+>3!@XA)6G-*1kxk`#t#asSL@FGpYh~Ex-!x7+{Ivcq z*zb|vl_YxO$@RT>#U`ApK&Z2#1ppY?Po@q;S+pPAv6RV1EWBbhB9UEe z!=J$Z&(=tw#bs9rFf?cw-mdZ?h$3V~jFPu5sL$XATfhy-WgvRJ3qM^|)Ny^-o$$fk z(c=u+T`PL=j2N9q2E|<4>;4>HIw+sY#7Qj0bj8B|fq9KN?k|fumR4>jw~Toe zCtiC~6}7Mb+1r59nVT;l(g}cZ31}aKynq2PQ7r%;sTQpE?oYTi_6q`J${En!F^HsBM*&j{^mJigfxc8URMhyaEHqYDRw`Wi)yOf(CNAJ&FDNgd=hjBXSpBtZZ!sPvz!( zRx*IiCyim!Zy_2HD*N%%@9NNgrs2-r=lij{%h4fS)8qe*GZOdac5lECCnA|2f?30v z<_Lacf2A(#(8$E!Jg^8(!YiC4C-XLTe?MYiEq1M=6?TZ3wKH)*mI%=Hc9U8wt~lOV z4Kq+n<>U;PLuiT3K%xpE_F=W?w}h--YbW(Bj}^tSnr*`Jr*{pPXP{pVLXRKq_&es9m6nz!Ul{cm z=(CUnzPxbN_OYQsu?OXPDYPw?xWdNdk)hg-2J2|Bc&k%mYagS8{I5p9gBtN1&&FgE z@qhmxLB=vC2QKDT2&7Gsij;qqs=f(*rsLq)tUjuVV~+c~oIh)!6kCWEh}`lWjtd)(q3~&%I&C1zKVu#-h6VewI7@Q;!AM-$0yiB_t2_muj%B zytV>(AK=!Xo}JYgY$-jITu4g+_bgOX0CjvPb3X+5md71Ec3eqFg{08E zGYOVSb~%{yj)H+zEy+*@>5x^;<{RH-0u5Cs(M5+TP8BcNMQExH!xTj z5KhM@N+nQ4nX)IKrY=034@-ELu^U;xIZg^~G`iySQvf)>XQeRR=8NRR8$fISw5v6K z-t9m<2R&Ur#znvKK^v268%Xmx`jX)D_lShH?T5&mJ%L#d1}TTo4?{kX@25S#Ur|mf zd1+WJXacdQz>Bs%?ulE6gh_!B=0VO&vJ>sP*@_?|Lc-w4;rlCfT|`{Azf_XIi+XXN zBgo&s-_Gvqsct_m?&EVGZBNO^>^(#DmfqeWS}#;n4H93aL7Y8bgYHF`3;lpT@kmv% zu0|nYJM&CkV(a0(aHz9zUo<5@2vlvSiw81W9uog(=Q6i-_4cU?_a$s0Q*Ma~1fq7# zk(4>*fBjm*X}0-cIv8#=ugZPuz^H;%NbN%*ov)xAC@;;!IG^%RgCLI2W&8!LQtRn| zY>wXj)lTyVCIR|JIl~UW5j$s?A$1t~ZW^ChH`(ZzZ~GwQcY8o{GT$YENY$7<##H2k zIF5&DI7Q;Lt*QDs-usXjJ$>_F1V;dYJ(u5i1W zdH7aapo{BqqXHb5+yt1Z_8tp>Tj9Er_|=#(8XCSM<;+}7(C5FJEr=>Ou=&D4W7%R3 zAAt)^NRp42n}btK{TBY1mgMKt>7iq4%^-w?g&B8f45{2EI05C^#VddHdwhYT6cTqB zzyCEScpA?Bt2=-GDwm$cu5cRuTf-NfdG0E%uOw&z52D=#_SI zX}V@$Ml=#giZNcU&K&=H@-eZi%-=)?4Id(mTirjw(m)-)$~pEQLLjq+LELaZhl1bxFd&vSamX}kf(m-2 z|1-;yQ4h$?IxA_=&msB68xa+iXylK8!!906;(wa%r+qC8s7-Cv`otyH_4N%M(=88Z z6;p-e3kBJq$27jVTEI!AkY?y(n+&PXMS~!kOh=APw39x&C@VHWvX#*&og5&&N&|3T0c!l0k6VG$ zLJW}LkZ`M#Wg4G^2^tV39Ewo+xYiT#i(Bn@Ibgjz3QD>Jm~3IsTM9uh5O6Zi?lk8# zT%dJkaHQpOn4pM#yI<;Xi+T!jGC@88cH7g)H_`QbI7e5Z6Q9X}&z*nZ^T z?b+xq)oXQ0HjC#^GqZNWCWQQjau#gU@(N;ra_x*TkLfDM`JRykQhy>DHSZ^O)3XUzi})iSw-ZL& z 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 le{;hwha{0mBoaw^_ void; +}) => { + const { design, selected, onClick } = props; + + return ( + + ); +}; + +export const RapidDecorationDevice = () => { + const { act, data } = useBackend(); + const { + categories = [], + selected_category, + selected_design, + matter, + max_matter, + } = data; + + return ( + + + + +

+ + + {matter}/{max_matter} Units + + + {selected_design ? capitalizeAll(selected_design) : 'None'} + + +
+ + +
+ {categories.map((category) => ( +
+ + {category.designs.map((design) => ( + + + act('design', { + category: category.cat_name, + name: design.name, + }) + } + /> + + ))} + +
+ ))} +
+
+ + + + ); +}; From c510838f573499b2d92c245eb422315d66bda067 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:37:01 +0000 Subject: [PATCH 030/101] Automatic changelog for PR #96350 [ci skip] --- html/changelogs/AutoChangeLog-pr-96350.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96350.yml diff --git a/html/changelogs/AutoChangeLog-pr-96350.yml b/html/changelogs/AutoChangeLog-pr-96350.yml new file mode 100644 index 000000000000..125e6c87f023 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96350.yml @@ -0,0 +1,5 @@ +author: "mcbalaam" +delete-after: True +changes: + - rscadd: "Added fake plastic plants to be used for decoration" + - rscadd: "RDD: Rapid Decoration Device is now available for the service department" \ No newline at end of file From a2007d007bfa469c4027badd320e2698f73fd148 Mon Sep 17 00:00:00 2001 From: Lucy Date: Mon, 8 Jun 2026 01:37:09 -0400 Subject: [PATCH 031/101] use the chat spritesheet for byond member ooc icons (#96384) ## About The Pull Request partial port of my fixups from https://github.com/Monkestation/Monkestation2.0/pull/11644 this refactors the byond member icon in OOC use the chat spritesheet, instead of `icon2html`. 2026-06-06 (1780794864) ~
dreamseeker also as a result, this makes the byond member icon align better with the text and be slightly less crunchy. here's how it was before, for reference: image ## Why It's Good For The Game why did this use icon2html that's dumb. better performance i guess. and looks better on the eyes. ## Changelog :cl: fix: Made the BYOND member OOC icon better aligned to the text. /:cl: --- code/modules/asset_cache/assets/chat.dm | 4 ++++ code/modules/client/verbs/ooc.dm | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/code/modules/asset_cache/assets/chat.dm b/code/modules/asset_cache/assets/chat.dm index 471e8ce85dc6..890f05afe39c 100644 --- a/code/modules/asset_cache/assets/chat.dm +++ b/code/modules/asset_cache/assets/chat.dm @@ -11,3 +11,7 @@ if (icon != 'icons/ui/chat/language.dmi') var/icon_state = initial(L.icon_state) insert_icon("language-[icon_state]", uni_icon(icon, icon_state)) + + var/datum/universal_icon/byond_member = uni_icon('icons/ui/chat/member_content.dmi', "blag") + byond_member.scale(16, 16) + insert_icon("byond_member", byond_member) diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index 7aa9ffd8fb85..cb02dc6d248d 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -77,12 +77,27 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") mob.log_talk(raw_msg, LOG_OOC) var/keyname = key - if(prefs.unlock_content) - if(prefs.toggles & MEMBER_PUBLIC) - keyname = "[icon2html('icons/ui/chat/member_content.dmi', world, "blag")][keyname]" + var/list/key_tags + var/key_prefix = "" + var/visible_unlock = prefs.unlock_content && (prefs.toggles & MEMBER_PUBLIC) + + // heart first lol if(prefs.hearted) - var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat) - keyname = "[sheet.icon_tag("emoji-heart")][keyname]" + LAZYADD(key_tags, "emoji-heart") + if(visible_unlock) + LAZYADD(key_tags, "byond_member") + + if(LAZYLEN(key_tags)) + var/datum/asset/spritesheet_batched/chat/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat) + for(var/icon_name in key_tags) + key_prefix = "[key_prefix][sheet.icon_tag(icon_name)]" + key_prefix = "[key_prefix]" + + keyname = "[key_prefix][keyname]" + + if(visible_unlock) + keyname = "[keyname]" + //The linkify span classes and linkify=TRUE below make ooc text get clickable chat href links if you pass in something resembling a url for(var/client/receiver as anything in GLOB.clients) if(!receiver.prefs) // Client being created or deleted. Despite all, this can be null. From c8f0f080a0436931bd18baf42c243a7882ef0ef3 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:37:26 +0000 Subject: [PATCH 032/101] Automatic changelog for PR #96384 [ci skip] --- html/changelogs/AutoChangeLog-pr-96384.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96384.yml diff --git a/html/changelogs/AutoChangeLog-pr-96384.yml b/html/changelogs/AutoChangeLog-pr-96384.yml new file mode 100644 index 000000000000..ccec9d094535 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96384.yml @@ -0,0 +1,4 @@ +author: "Absolucy" +delete-after: True +changes: + - bugfix: "Made the BYOND member OOC icon better aligned to the text." \ No newline at end of file From fac7cbae253c8e662c52ce62e325d4bcd6590333 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:08:10 +0530 Subject: [PATCH 033/101] Use new `tgstation/code-reviewers` github action (#96346) ## About The Pull Request Replaces the existing code reviewer's workflow with the new [one](https://github.com/tgstation/code-reviewers) See [here](https://github.com/tgstation/code-reviewers/pull/1) on why this is better. ## Changelog N/A --- .github/workflows/codeowner_reviews.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/codeowner_reviews.yml b/.github/workflows/codeowner_reviews.yml index f1cb174de37b..f81647e76eb6 100644 --- a/.github/workflows/codeowner_reviews.yml +++ b/.github/workflows/codeowner_reviews.yml @@ -14,16 +14,9 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it - uses: actions/checkout@v6 - #Parse the Codeowner file on non draft PRs - - name: CodeOwnersParser - if: github.event.pull_request.draft == false - id: CodeOwnersParser - uses: tgstation/CodeOwnersParser@v1 - - #Request reviews + #Request reviews - name: Request reviews - if: steps.CodeOwnersParser.outputs.owners != '' - uses: tgstation/RequestReviewFromUser@v1 + if: github.event.pull_request.draft == false + uses: tgstation/code-reviewers@main with: - separator: " " - users: ${{ steps.CodeOwnersParser.outputs.owners }} + token: ${{ secrets.GITHUB_TOKEN }} From 6f4914aa7653638fcd4f1a6271c7ac487ac69cd3 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:09:18 +0530 Subject: [PATCH 034/101] Labeller action can remove labels from PR upon sync (#96131) ## About The Pull Request #92553 made it possible for the PR author to add labels to the PR body whenever it was updated however as discussed in the comments it was not possible to remove labels. Maintainers can still add/remove labels and that works as expected but the PR author could not. This obviously is inconvenient and should not be the case but now it is possible. This will not conflict with maintainer added/removed labels which means if the author says like removes the balance label from their changelog, but the maintainer adds the balance label then the balance label will always stay and cannot be removed by the author so labelling still works as intended. It will only remove labels added by the contributor should they decide to change their changelog/modified files/PR title at any point ## Changelog N/A --------- Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> --- tools/pull_request_hooks/autoLabel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/pull_request_hooks/autoLabel.js b/tools/pull_request_hooks/autoLabel.js index b995bdc19d57..02ed25a2b6c6 100644 --- a/tools/pull_request_hooks/autoLabel.js +++ b/tools/pull_request_hooks/autoLabel.js @@ -177,12 +177,11 @@ export async function get_updated_label_set({ github, context }) { const { body = "", diff_url, - labels = [], mergeable, title = "", } = pull_request; - const updated_labels = new Set(labels.map((l) => l.name)); + const updated_labels = new Set(); // Always check file diffs if (diff_url) { @@ -224,6 +223,9 @@ export async function get_updated_label_set({ github, context }) { } } catch (error) { console.error("Error fetching paginated events:", error); + for(const label of pull_request.labels){ + updated_labels.add(label.name); + } } // Always remove Test Merge Candidate From d27010a01e7eb08616b7d6db6a3acf612e1dc53f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 06:00:23 +0000 Subject: [PATCH 035/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96350.yml | 5 ----- html/changelogs/AutoChangeLog-pr-96384.yml | 4 ---- html/changelogs/archive/2026-06.yml | 5 +++++ 3 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96350.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96384.yml diff --git a/html/changelogs/AutoChangeLog-pr-96350.yml b/html/changelogs/AutoChangeLog-pr-96350.yml deleted file mode 100644 index 125e6c87f023..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96350.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "mcbalaam" -delete-after: True -changes: - - rscadd: "Added fake plastic plants to be used for decoration" - - rscadd: "RDD: Rapid Decoration Device is now available for the service department" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96384.yml b/html/changelogs/AutoChangeLog-pr-96384.yml deleted file mode 100644 index ccec9d094535..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96384.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Absolucy" -delete-after: True -changes: - - bugfix: "Made the BYOND member OOC icon better aligned to the text." \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 3c5c8d141b6e..59bf5b114dd2 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -195,10 +195,15 @@ soulware1: - bugfix: Parthenogenesis no longer makes your material fish have null weights 2026-06-08: + Absolucy: + - bugfix: Made the BYOND member OOC icon better aligned to the text. Melbert: - bugfix: Fixed a bug where diseases wouldn't trigger their regular effects when applied in certain contexts - bugfix: Fixed a bug where airborne diseases wouldn't spread via breathing when applied in certain contexts + mcbalaam: + - rscadd: Added fake plastic plants to be used for decoration + - rscadd: 'RDD: Rapid Decoration Device is now available for the service department' soulware1: - balance: Carving blocks can be made out of any material now! From 68dd51e7eebdbcd7b3f8557b89190c6677877a1e Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 8 Jun 2026 08:26:49 -0500 Subject: [PATCH 036/101] Blood worms are no longer fully immune to space/cold (#96385) ## About The Pull Request Blood worms take 2 damage/second from being in extreme pressure, and 1 damage/second from being in extreme cold Note that it doesn't stop their regen (up to 0.5 damage/secon) so the number is closer to 1.5 damage/second and 0.5 damage/second (for the elders) ## Why It's Good For The Game Blood Worm's space immunity is kind of at odds with their intended design/gameplay loop of stealing people's identities, as more often than not, the identities they steal are not immune to space. It is not an uncommon sight to see bworms completely ditch the disguise mechanic and just go around busting windows to "expand their domain" of places that they can't be chased Thus I am opting to remove the full immunity to pressure and cold from bworms. They are still quite resilient to pressure and cold (if my math is correct it would take at least a minute to die to pressure damage as an elder) but lacking full immunity should disincentivize them from spacing themselves unless in complete necessity. ## Changelog :cl: Melbert balance: Blood worms are no longer fully immune to extreme pressures and cold, such as the vacuum of space /:cl: --- code/modules/antagonists/blood_worm/blood_worm_mob.dm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/modules/antagonists/blood_worm/blood_worm_mob.dm b/code/modules/antagonists/blood_worm/blood_worm_mob.dm index 7ac058417869..44b5f662f84a 100644 --- a/code/modules/antagonists/blood_worm/blood_worm_mob.dm +++ b/code/modules/antagonists/blood_worm/blood_worm_mob.dm @@ -24,11 +24,12 @@ attack_verb_continuous = "bites" attack_verb_simple = "bite" - minimum_survivable_temperature = 0 + minimum_survivable_temperature = ICEBOX_MIN_TEMPERATURE maximum_survivable_temperature = T0C + 100 - unsuitable_cold_damage = 0 + unsuitable_cold_damage = 1 + unsuitable_atmos_damage = 2 - habitable_atmos = null + habitable_atmos = list("min_oxy" = 5, "max_oxy" = 0) // A vivid red. lighting_cutoff_red = 40 From 7fd03e1d56a7694ef45de11de6e52a987f01793d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:45:47 +0000 Subject: [PATCH 037/101] Automatic changelog for PR #96385 [ci skip] --- html/changelogs/AutoChangeLog-pr-96385.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96385.yml diff --git a/html/changelogs/AutoChangeLog-pr-96385.yml b/html/changelogs/AutoChangeLog-pr-96385.yml new file mode 100644 index 000000000000..b64b5dc9dff8 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96385.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - balance: "Blood worms are no longer fully immune to extreme pressures and cold, such as the vacuum of space" \ No newline at end of file From 5c16d8d75ef72b096232bc38add45f9c05b8bf49 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 8 Jun 2026 09:13:05 -0500 Subject: [PATCH 038/101] Walking Aid Component (#96294) ## About The Pull Request So I saw someone was adding a limping quirk in https://github.com/tgstation/tgstation/pull/96200 and I had tried to do the same back in https://github.com/tgstation/tgstation/pull/71470 a few years ago. The codebase has evolved enough to support this behavior that was not present initially during my first attempt. One of the big features of my old PR was to add cane-like behavior to a bunch of other pole objects. This PR introduces a new modular `/datum/component/walking_aid` component, moving canes/crutches away from hardcoded object behavior. This component is now attached to the following items: - Brooms - Mops - Staves - Scythes - Spears - Canes - Crutches There are also some notable changes I made to how things work, detailed below: - Walking aid objects must be held on the same side as the affected leg (if your right leg is missing, you need to be holding crutches on your right arm) - Objects lose their ability as a walking aid while being wielded with two hands (ex. spears) - Limping/Limbless are now both affected by a walking aid, where before only crutches helped assist limbless mobs - "Walking Aid" examine tag when the walking aid object is examined All walking aids (except crutches) reduce limbless slowdown by 40%, and completely ignore any limping penalties. Crutches reduce the limbless slowdown by 60% and are much faster. ## Why It's Good For The Game 1. Mah Immersion 2. More modular code 3. Examine tags are peak 4. Assistants can now justify carrying spears as medical equipment 5. Cane/Poles should help with limblessness, but not as helpful as crutches While I wanted pole items to offer mobility support, they needed to remain less effective than dedicated medical crutches. During testing, I found that a 40% reduction is the point where the benefit becomes noticeable. ## Changelog :cl: add: Pole objects like mops, brooms, staves, scythes, and spears can now be held in a hand to act as walking aids, mitigating limping from fractures and reducing missing-leg slowdowns. balance: Walking aids must be held in the hand on the same side as the injured or missing leg to provide support, and lose their effectiveness if actively wielded with two hands. balance: Objects with the walking aid reduce limbless slowdown by 40%, while dedicated medical crutches reduce it by 60%. refactor: Refactor cane and crutch code logic into a modular walking aid component /:cl: --- .../signals/signals_mob/signals_mob_carbon.dm | 2 +- code/datums/components/walking_aid.dm | 161 ++++++++++++++++++ code/datums/status_effects/wound_effects.dm | 5 +- code/game/objects/items/mop.dm | 1 + code/game/objects/items/pitchfork.dm | 1 + code/game/objects/items/religion.dm | 4 + .../objects/items/tools/janitorial/broom.dm | 1 + code/game/objects/items/tools/medical/cane.dm | 72 ++------ .../game/objects/items/weaponry/melee/misc.dm | 5 + .../items/weaponry/melee/soulscythe.dm | 1 + .../objects/items/weaponry/melee/spear.dm | 1 + code/modules/hydroponics/hydroitemdefines.dm | 1 + .../job_types/chaplain/chaplain_nullrod.dm | 22 +++ .../chaplain/chaplain_vorpal_scythe.dm | 1 + code/modules/projectiles/guns/magic/staff.dm | 4 + tgstation.dme | 1 + 16 files changed, 224 insertions(+), 59 deletions(-) create mode 100644 code/datums/components/walking_aid.dm diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index a8af2cc5c667..70642a4d6b47 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -167,7 +167,7 @@ /// Return to skip oxyloss and similar effects from blood level #define HANDLE_BLOOD_NO_OXYLOSS (1<<2) -/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced) iodk where it should go +/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced): (var/obj/item/bodypart/limping_leg) #define COMSIG_CARBON_LIMPING "mob_limp_check" #define COMPONENT_CANCEL_LIMP (1<<0) diff --git a/code/datums/components/walking_aid.dm b/code/datums/components/walking_aid.dm new file mode 100644 index 000000000000..7a869661f2d5 --- /dev/null +++ b/code/datums/components/walking_aid.dm @@ -0,0 +1,161 @@ +/** + * Walking Aid Component + * + * Add this to an item to allow it to act as a cane (or crutch) while held. + * Items with this component will help mobs avoid limping from broken + * leg bones, and lessen the slowdown caused by missing legs. + * + * Used by canes, crutches, and pole-like items such as spears and staffs. + */ +/datum/component/walking_aid + dupe_mode = COMPONENT_DUPE_UNIQUE + /// Causes a mob to waddle (wiggle) while walking when holding this object + var/waddling = FALSE + /// If set, the parent item must have this trait for the support to function + var/required_trait + /// Weakref to the mob currently being supported + var/datum/weakref/current_user_ref + /// The amount of slowdown to reduce for a limbless leg + var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40% + +/datum/component/walking_aid/Initialize(limbless_slowdown_modifier = 0.6, required_trait = null, waddling = FALSE) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + + src.waddling = waddling + src.required_trait = required_trait + src.limbless_slowdown_modifier = limbless_slowdown_modifier + +/datum/component/walking_aid/Destroy(force) + remove_support() + return ..() + +/datum/component/walking_aid/RegisterWithParent() + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop)) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(get_examine_tags)) + RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_WIELDED), PROC_REF(update_legs)) + RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_WIELDED), PROC_REF(update_legs)) + RegisterSignal(parent, SIGNAL_ADDTRAIT(required_trait), PROC_REF(update_legs)) + RegisterSignal(parent, SIGNAL_REMOVETRAIT(required_trait), PROC_REF(update_legs)) + +/datum/component/walking_aid/UnregisterFromParent() + UnregisterSignal(parent, list( + COMSIG_ITEM_EQUIPPED, + COMSIG_ITEM_DROPPED, + COMSIG_ATOM_EXAMINE_TAGS, + SIGNAL_ADDTRAIT(TRAIT_WIELDED), + SIGNAL_REMOVETRAIT(TRAIT_WIELDED), + SIGNAL_ADDTRAIT(required_trait), + SIGNAL_REMOVETRAIT(required_trait), + )) + remove_support() + +/datum/component/walking_aid/proc/on_equip(datum/source, mob/equipper, slot) + SIGNAL_HANDLER + + remove_support() + if(!(slot & ITEM_SLOT_HANDS)) + return + if(!isliving(equipper)) + return + + apply_support(equipper) + +/datum/component/walking_aid/proc/on_drop(datum/source, mob/user) + SIGNAL_HANDLER + remove_support() + +/datum/component/walking_aid/proc/get_examine_tags(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + examine_list["walking-aid"] = "It can help lessen the slowdown caused from a missing or injured leg, when held on the same side as the injury." + +// Updates our leg status when wielded/unwielded a two handed walking aid like a spear +/datum/component/walking_aid/proc/update_legs(atom/source) + SIGNAL_HANDLER + + var/mob/living/user = current_user_ref?.resolve() + user?.update_usable_leg_status() + +/datum/component/walking_aid/proc/apply_support(mob/living/user) + if(current_user_ref) + remove_support() + + current_user_ref = WEAKREF(user) + RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping)) + RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown)) + user.update_usable_leg_status() + + if(waddling) + user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling) + +/datum/component/walking_aid/proc/remove_support() + var/mob/living/user = current_user_ref?.resolve() + current_user_ref = null + if(isnull(user)) + return + UnregisterSignal(user, list(COMSIG_CARBON_LIMPING, COMSIG_LIVING_LIMBLESS_SLOWDOWN)) + user.update_usable_leg_status() + + if(waddling) + REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src)) + +/datum/component/walking_aid/proc/is_active() + // if both hands are holding it, then it is not being used for support + if(HAS_TRAIT(parent, TRAIT_WIELDED)) + return FALSE + + if(isnull(required_trait)) + return TRUE + + return HAS_TRAIT(parent, required_trait) + +/datum/component/walking_aid/proc/handle_limping(mob/living/user, obj/item/bodypart/limping_leg) + SIGNAL_HANDLER + + if(!is_active()) + return NONE + if(isnull(limping_leg)) + return NONE + + var/supported_zone = get_supported_leg_zone(user) + if(isnull(supported_zone)) + return NONE + if(limping_leg.body_zone != supported_zone) + return NONE + + return COMPONENT_CANCEL_LIMP + +/datum/component/walking_aid/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods) + SIGNAL_HANDLER + + if(!is_active()) + return + if(!iscarbon(user)) + return + var/mob/living/carbon/carbon_user = user + var/leg_amount = carbon_user.usable_legs + if(leg_amount >= carbon_user.default_num_legs) + return + if(!leg_amount) // someday support dual-wielding crutches but for now they are destined to waddle + return + + var/supported_zone = get_supported_leg_zone(user) + if(isnull(supported_zone)) + return + if(carbon_user.get_bodypart(supported_zone)) // make sure their leg is actually missing + return + + slowdown_mods += limbless_slowdown_modifier + +/datum/component/walking_aid/proc/get_supported_leg_zone(mob/living/user) + var/held_hand_zone = user.get_hand_zone_of_item(parent) + + switch(held_hand_zone) + if(BODY_ZONE_R_ARM) + return BODY_ZONE_R_LEG + if(BODY_ZONE_L_ARM) + return BODY_ZONE_L_LEG + else + return null diff --git a/code/datums/status_effects/wound_effects.dm b/code/datums/status_effects/wound_effects.dm index 5ea0be0b256d..6b10d609778b 100644 --- a/code/datums/status_effects/wound_effects.dm +++ b/code/datums/status_effects/wound_effects.dm @@ -79,18 +79,21 @@ // less limping while we have determination still var/determined_mod = owner.has_status_effect(/datum/status_effect/determined) ? 0.5 : 1 + var/obj/item/bodypart/leg_about_to_limp var/limp_chance var/limp_slowdown if(next_leg == left) + leg_about_to_limp = left limp_chance = limp_chance_left limp_slowdown = slowdown_left next_leg = right else + leg_about_to_limp = right limp_chance = limp_chance_right limp_slowdown = slowdown_right next_leg = left - if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING) & COMPONENT_CANCEL_LIMP) + if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING, leg_about_to_limp) & COMPONENT_CANCEL_LIMP) return if(prob(limp_chance * determined_mod)) diff --git a/code/game/objects/items/mop.dm b/code/game/objects/items/mop.dm index 591dd127411c..35f4e2675301 100644 --- a/code/game/objects/items/mop.dm +++ b/code/game/objects/items/mop.dm @@ -36,6 +36,7 @@ /obj/item/mop/Initialize(mapload) . = ..() AddComponent(/datum/component/cleaner, mopspeed, pre_clean_callback=CALLBACK(src, PROC_REF(should_clean)), on_cleaned_callback=CALLBACK(src, PROC_REF(apply_reagents))) + AddComponent(/datum/component/walking_aid) create_reagents(max_reagent_volume) GLOB.janitor_devices += src diff --git a/code/game/objects/items/pitchfork.dm b/code/game/objects/items/pitchfork.dm index 99f714f09f3b..497e213acba0 100644 --- a/code/game/objects/items/pitchfork.dm +++ b/code/game/objects/items/pitchfork.dm @@ -32,6 +32,7 @@ . = ..() AddComponent(/datum/component/jousting) AddComponent(/datum/component/two_handed, force_unwielded=7, force_wielded=15, icon_wielded="[base_icon_state]1") + AddComponent(/datum/component/walking_aid) /obj/item/pitchfork/update_icon_state() icon_state = "[base_icon_state]0" diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm index 0e96116ed3d1..a087dfeb897e 100644 --- a/code/game/objects/items/religion.dm +++ b/code/game/objects/items/religion.dm @@ -337,6 +337,10 @@ var/staffcooldown = 0 var/staffwait = 30 +/obj/item/godstaff/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + /obj/item/godstaff/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) if(SHOULD_SKIP_INTERACTION(interacting_with, src, user)) return NONE diff --git a/code/game/objects/items/tools/janitorial/broom.dm b/code/game/objects/items/tools/janitorial/broom.dm index 970bb5a55261..e34e0044b08f 100644 --- a/code/game/objects/items/tools/janitorial/broom.dm +++ b/code/game/objects/items/tools/janitorial/broom.dm @@ -29,6 +29,7 @@ wield_callback = CALLBACK(src, PROC_REF(on_wield)), \ unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \ ) + AddComponent(/datum/component/walking_aid) /obj/item/pushbroom/update_icon_state() icon_state = "[base_icon_state]0" diff --git a/code/game/objects/items/tools/medical/cane.dm b/code/game/objects/items/tools/medical/cane.dm index 132f03a9b378..ca9de8325b7e 100644 --- a/code/game/objects/items/tools/medical/cane.dm +++ b/code/game/objects/items/tools/medical/cane.dm @@ -13,32 +13,18 @@ custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT * 0.5) attack_verb_continuous = list("bludgeons", "whacks", "disciplines", "thrashes") attack_verb_simple = list("bludgeon", "whack", "discipline", "thrash") + /// The amount of slowdown to reduce for a limbless leg + var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40% + /// Does this cause waddling when held + var/causes_waddling = FALSE -/obj/item/cane/examine(mob/user, thats) +/obj/item/cane/Initialize(mapload) . = ..() - . += span_notice("This item can be used to support your weight, preventing limping from any broken bones on your legs you may have.") + AddComponent(/datum/component/walking_aid, limbless_slowdown_modifier, get_walking_aid_required_trait(), causes_waddling) -/obj/item/cane/equipped(mob/living/user, slot, initial) - ..() - if(!(slot & ITEM_SLOT_HANDS)) - return - movement_support_add(user) - -/obj/item/cane/dropped(mob/living/user, silent = FALSE) - . = ..() - movement_support_del(user) - -/obj/item/cane/proc/movement_support_add(mob/living/user) - RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping)) - return TRUE - -/obj/item/cane/proc/movement_support_del(mob/living/user) - UnregisterSignal(user, list(COMSIG_CARBON_LIMPING)) - return TRUE - -/obj/item/cane/proc/handle_limping(mob/living/user) - SIGNAL_HANDLER - return COMPONENT_CANCEL_LIMP +/// Determines if a trait is required to be used as a walking aid (ex. foldable canes) +/obj/item/cane/proc/get_walking_aid_required_trait() + return null /obj/item/cane/crutch name = "medical crutch" @@ -55,42 +41,13 @@ custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 0.5) attack_verb_continuous = list("bludgeons", "whacks", "thrashes") attack_verb_simple = list("bludgeon", "whack", "thrash") + limbless_slowdown_modifier = 0.4 // reduces slowdown by 60% + causes_waddling = TRUE /obj/item/cane/crutch/Initialize(mapload) . = ..() AddElement(/datum/element/cuffable_item) -/obj/item/cane/crutch/examine(mob/user, thats) - . = ..() - // tacked on after the cane string - . += span_notice("As a crutch, it can also help lessen the slowdown incurred by missing a leg.") - -/obj/item/cane/crutch/movement_support_add(mob/living/user) - . = ..() - if(!.) - return - RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown)) - user.update_usable_leg_status() - user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling) - -/obj/item/cane/crutch/movement_support_del(mob/living/user) - . = ..() - if(!.) - return - UnregisterSignal(user, list(COMSIG_LIVING_LIMBLESS_SLOWDOWN, COMSIG_CARBON_LIMPING)) - user.update_usable_leg_status() - REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src)) - -/obj/item/cane/crutch/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods) - SIGNAL_HANDLER - var/leg_amount = user.usable_legs - // Don't do anything if the number is equal (or higher) to the usual. - if(leg_amount >= user.default_num_legs) - return - // If we have at least one leg and it's less than the default, reduce slowdown by 60%. - if(leg_amount && (leg_amount < user.default_num_legs)) - slowdown_mods += 0.4 - /obj/item/cane/crutch/wood name = "wooden crutch" desc = "A handmade crutch. Also makes a decent bludgeon if you need it." @@ -124,15 +81,16 @@ ) RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform)) -/obj/item/cane/white/handle_limping(mob/living/user) - return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? COMPONENT_CANCEL_LIMP : NONE +// White canes only provide support while extended +/obj/item/cane/white/get_walking_aid_required_trait() + return TRAIT_TRANSFORM_ACTIVE /* * Signal proc for [COMSIG_TRANSFORMING_ON_TRANSFORM]. * * Gives feedback to the user and makes it show up inhand. */ -/obj/item/cane/white/proc/on_transform(obj/item/source, mob/user, active) +/obj/item/cane/white/proc/on_transform(obj/item/source, mob/living/user, active) SIGNAL_HANDLER if(user) diff --git a/code/game/objects/items/weaponry/melee/misc.dm b/code/game/objects/items/weaponry/melee/misc.dm index b79ff035f20d..35f3151987ed 100644 --- a/code/game/objects/items/weaponry/melee/misc.dm +++ b/code/game/objects/items/weaponry/melee/misc.dm @@ -386,6 +386,7 @@ force_unwielded = 10, \ force_wielded = 14, \ ) + AddComponent(/datum/component/walking_aid) /obj/item/bambostaff/update_icon_state() icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]" @@ -414,6 +415,10 @@ attack_verb_simple = list("bludgeon", "whack", "discipline") resistance_flags = FLAMMABLE +/obj/item/staff/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + /obj/item/staff/broom name = "broom" desc = "Used for sweeping, and flying into the night while cackling. Black cat not included." diff --git a/code/game/objects/items/weaponry/melee/soulscythe.dm b/code/game/objects/items/weaponry/melee/soulscythe.dm index 596166d14e02..c7dabc9d44df 100644 --- a/code/game/objects/items/weaponry/melee/soulscythe.dm +++ b/code/game/objects/items/weaponry/melee/soulscythe.dm @@ -39,6 +39,7 @@ RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED_SECONDARY, PROC_REF(on_secondary_attack)) RegisterSignal(src, COMSIG_ATOM_INTEGRITY_CHANGED, PROC_REF(on_integrity_change)) AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) + AddComponent(/datum/component/walking_aid) /obj/item/soulscythe/examine(mob/user) . = ..() diff --git a/code/game/objects/items/weaponry/melee/spear.dm b/code/game/objects/items/weaponry/melee/spear.dm index 39ca04c50173..6fc5634c7c0a 100644 --- a/code/game/objects/items/weaponry/melee/spear.dm +++ b/code/game/objects/items/weaponry/melee/spear.dm @@ -70,6 +70,7 @@ wield_callback = CALLBACK(src, PROC_REF(on_wield)), \ unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \ ) + AddComponent(/datum/component/walking_aid) add_headpike_component() update_appearance() diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm index a35b938fcf66..1eb888dc1cd7 100644 --- a/code/modules/hydroponics/hydroitemdefines.dm +++ b/code/modules/hydroponics/hydroitemdefines.dm @@ -174,6 +174,7 @@ . = ..() AddComponent(/datum/component/butchering, speed = 9 SECONDS, effectiveness = 105) AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) + AddComponent(/datum/component/walking_aid) /obj/item/scythe/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is beheading [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!")) diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm index 5e67c8de35bf..f73ab5f588c6 100644 --- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm +++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm @@ -340,6 +340,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) /// The icon which appears over the mob holding the item var/shield_icon = "shield-red" +/obj/item/nullrod/staff/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + /obj/item/nullrod/staff/worn_overlays(mutable_appearance/standing, isinhands) . = ..() if(isinhands) @@ -629,6 +633,7 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) force_unwielded = 14, \ force_wielded = 18, \ ) + AddComponent(/datum/component/walking_aid) /obj/item/nullrod/bostaff/update_icon_state() icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]" @@ -703,6 +708,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) sharpness = SHARP_EDGED menu_description = "A sharp pitchfork. Can be worn on the back." +/obj/item/nullrod/pitchfork/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + // Egyptian Staff - Used as a tool for making mummy wraps. /obj/item/nullrod/egyptian @@ -720,6 +729,11 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) attack_verb_simple = list("bash", "smack", "whack") menu_description = "A staff. Can be used as a tool to craft exclusive egyptian items. Easily stored. Can be worn on the back." + +/obj/item/nullrod/egyptian/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + // Hypertool - It does brain damage rather than normal damage. /obj/item/nullrod/hypertool @@ -760,6 +774,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) hitsound = 'sound/items/weapons/bladeslice.ogg' menu_description = "A pointy spear which penetrates armor a little. Can be worn only on the belt." +/obj/item/nullrod/spear/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + // Unholy version of above, since the gamemode is dead in the water /obj/item/brass_spear @@ -783,6 +801,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants()) attack_verb_simple = list("stab", "poke", "slash", "clock") hitsound = 'sound/items/weapons/bladeslice.ogg' +/obj/item/brass_spear/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + // Nullblade - For when you really want to feel like rolling dice during combat /obj/item/nullrod/nullblade diff --git a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm index c06f8993b53f..e94ad0d9762f 100644 --- a/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm +++ b/code/modules/jobs/job_types/chaplain/chaplain_vorpal_scythe.dm @@ -81,6 +81,7 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and AddElement(/datum/element/nullrod_core, chaplain_spawnable = FALSE, rune_remove_line = "TO DUST WITH YE!! AWAY!!") // The implant is the actual item the chappie can select AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) //also good at killing plants AddComponent(/datum/component/butchering, speed = 3 SECONDS, effectiveness = 125) + AddComponent(/datum/component/walking_aid) /obj/item/vorpalscythe/attack(mob/living/target, mob/living/user, list/modifiers, list/attack_modifiers) if(ismonkey(target) && !target.mind) //Don't empower from hitting monkeys. Hit a corgi or something, I don't know. diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm index 3615616f4f39..ccaed1a6996d 100644 --- a/code/modules/projectiles/guns/magic/staff.dm +++ b/code/modules/projectiles/guns/magic/staff.dm @@ -11,6 +11,10 @@ /// If FALSE, only wizards or survivalists can use the staff to its full potential - If TRUE, anyone can var/allow_intruder_use = FALSE +/obj/item/gun/magic/staff/Initialize(mapload) + . = ..() + AddComponent(/datum/component/walking_aid) + /obj/item/gun/magic/staff/proc/is_wizard_or_friend(mob/user) if(!HAS_MIND_TRAIT(user, TRAIT_MAGICALLY_GIFTED) && !allow_intruder_use) return FALSE diff --git a/tgstation.dme b/tgstation.dme index 7575b8fdc36c..00cc6b2f93ba 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1396,6 +1396,7 @@ #include "code\datums\components\usb_port.dm" #include "code\datums\components\vacuum.dm" #include "code\datums\components\vision_hurting.dm" +#include "code\datums\components\walking_aid.dm" #include "code\datums\components\wearertargeting.dm" #include "code\datums\components\weatherannouncer.dm" #include "code\datums\components\wet_floor.dm" From 368b9f3d6abd5e59d6010bc3f37a6bd5768e3bac Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:13:28 +0000 Subject: [PATCH 039/101] Automatic changelog for PR #96294 [ci skip] --- html/changelogs/AutoChangeLog-pr-96294.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96294.yml diff --git a/html/changelogs/AutoChangeLog-pr-96294.yml b/html/changelogs/AutoChangeLog-pr-96294.yml new file mode 100644 index 000000000000..f3af7d45951a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96294.yml @@ -0,0 +1,7 @@ +author: "timothymtorres" +delete-after: True +changes: + - rscadd: "Pole objects like mops, brooms, staves, scythes, and spears can now be held in a hand to act as walking aids, mitigating limping from fractures and reducing missing-leg slowdowns." + - balance: "Walking aids must be held in the hand on the same side as the injured or missing leg to provide support, and lose their effectiveness if actively wielded with two hands." + - balance: "Objects with the walking aid reduce limbless slowdown by 40%, while dedicated medical crutches reduce it by 60%." + - refactor: "Refactor cane and crutch code logic into a modular walking aid component" \ No newline at end of file From 2f2436b9a31f918913c1ca44cd393f13d0efee55 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:02:59 -0400 Subject: [PATCH 040/101] Remove slavery joke from cotton seed description (#96386) ## About The Pull Request Remove a joke about enslaving assistants to farm cotton from the description for cotton seeds ## Why It's Good For The Game It's a pretty tasteless and pointlessly edgy thing to include ## Changelog Probably not --- code/modules/hydroponics/grown/cotton.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/hydroponics/grown/cotton.dm b/code/modules/hydroponics/grown/cotton.dm index c2149b7a3300..ea160511b973 100644 --- a/code/modules/hydroponics/grown/cotton.dm +++ b/code/modules/hydroponics/grown/cotton.dm @@ -1,6 +1,6 @@ /obj/item/seeds/cotton name = "cotton seed pack" - desc = "A pack of seeds that'll grow into a cotton plant. Assistants make good free labor if neccesary." + desc = "A pack of seeds that'll grow into a cotton plant." icon_state = "seed-cotton" species = "cotton" plantname = "Cotton" From 3a772e55068f6feb6d23bda828d4d54a760e31f5 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:00:29 +0000 Subject: [PATCH 041/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96294.yml | 7 ------- html/changelogs/AutoChangeLog-pr-96385.yml | 4 ---- html/changelogs/archive/2026-06.yml | 12 ++++++++++++ 3 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96294.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-96385.yml diff --git a/html/changelogs/AutoChangeLog-pr-96294.yml b/html/changelogs/AutoChangeLog-pr-96294.yml deleted file mode 100644 index f3af7d45951a..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96294.yml +++ /dev/null @@ -1,7 +0,0 @@ -author: "timothymtorres" -delete-after: True -changes: - - rscadd: "Pole objects like mops, brooms, staves, scythes, and spears can now be held in a hand to act as walking aids, mitigating limping from fractures and reducing missing-leg slowdowns." - - balance: "Walking aids must be held in the hand on the same side as the injured or missing leg to provide support, and lose their effectiveness if actively wielded with two hands." - - balance: "Objects with the walking aid reduce limbless slowdown by 40%, while dedicated medical crutches reduce it by 60%." - - refactor: "Refactor cane and crutch code logic into a modular walking aid component" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-96385.yml b/html/changelogs/AutoChangeLog-pr-96385.yml deleted file mode 100644 index b64b5dc9dff8..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96385.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - balance: "Blood worms are no longer fully immune to extreme pressures and cold, such as the vacuum of space" \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 59bf5b114dd2..8205762fdd27 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -202,8 +202,20 @@ applied in certain contexts - bugfix: Fixed a bug where airborne diseases wouldn't spread via breathing when applied in certain contexts + - balance: Blood worms are no longer fully immune to extreme pressures and cold, + such as the vacuum of space mcbalaam: - rscadd: Added fake plastic plants to be used for decoration - rscadd: 'RDD: Rapid Decoration Device is now available for the service department' soulware1: - balance: Carving blocks can be made out of any material now! + timothymtorres: + - rscadd: Pole objects like mops, brooms, staves, scythes, and spears can now be + held in a hand to act as walking aids, mitigating limping from fractures and + reducing missing-leg slowdowns. + - balance: Walking aids must be held in the hand on the same side as the injured + or missing leg to provide support, and lose their effectiveness if actively + wielded with two hands. + - balance: Objects with the walking aid reduce limbless slowdown by 40%, while dedicated + medical crutches reduce it by 60%. + - refactor: Refactor cane and crutch code logic into a modular walking aid component From c6b96b067507165b148237949d41c6f5b98d8a31 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:59:17 -0400 Subject: [PATCH 042/101] Split `sprite_accessories.dm` into separate files (#96389) ## About The Pull Request This file was nearly 3000 lines long and contained every single `sprite_accessory` subtype, breaks it into separate files based on subtype instead ## Why It's Good For The Game Giant unorganized files aren't fun to look through ## Changelog N/A --- code/datums/sprite_accessories.dm | 2702 ----------------- .../sprite_accessories/_sprite_accessory.dm | 66 + code/datums/sprite_accessories/clothing.dm | 718 +++++ code/datums/sprite_accessories/ears.dm | 40 + code/datums/sprite_accessories/facial_hair.dm | 161 + code/datums/sprite_accessories/frills.dm | 14 + code/datums/sprite_accessories/hair.dm | 1022 +++++++ code/datums/sprite_accessories/horns.dm | 23 + code/datums/sprite_accessories/markings.dm | 85 + .../sprite_accessories/moth_antennae.dm | 94 + code/datums/sprite_accessories/pod_hair.dm | 43 + code/datums/sprite_accessories/snouts.dm | 19 + code/datums/sprite_accessories/spines.dm | 47 + code/datums/sprite_accessories/tails.dm | 97 + code/datums/sprite_accessories/wings.dm | 249 ++ tgstation.dme | 15 +- 16 files changed, 2692 insertions(+), 2703 deletions(-) delete mode 100644 code/datums/sprite_accessories.dm create mode 100644 code/datums/sprite_accessories/_sprite_accessory.dm create mode 100644 code/datums/sprite_accessories/clothing.dm create mode 100644 code/datums/sprite_accessories/ears.dm create mode 100644 code/datums/sprite_accessories/facial_hair.dm create mode 100644 code/datums/sprite_accessories/frills.dm create mode 100644 code/datums/sprite_accessories/hair.dm create mode 100644 code/datums/sprite_accessories/horns.dm create mode 100644 code/datums/sprite_accessories/markings.dm create mode 100644 code/datums/sprite_accessories/moth_antennae.dm create mode 100644 code/datums/sprite_accessories/pod_hair.dm create mode 100644 code/datums/sprite_accessories/snouts.dm create mode 100644 code/datums/sprite_accessories/spines.dm create mode 100644 code/datums/sprite_accessories/tails.dm create mode 100644 code/datums/sprite_accessories/wings.dm diff --git a/code/datums/sprite_accessories.dm b/code/datums/sprite_accessories.dm deleted file mode 100644 index b89da65d5ebf..000000000000 --- a/code/datums/sprite_accessories.dm +++ /dev/null @@ -1,2702 +0,0 @@ -/* - * Hello and welcome to sprite_accessories: For sprite accessories, such as hair, - * facial hair, and possibly tattoos and stuff somewhere along the line. This file is - * intended to be friendly for people with little to no actual coding experience. - * The process of adding in new hairstyles has been made pain-free and easy to do. - * Enjoy! - Doohl - * - * - * Notice: This all gets automatically compiled in a list in dna.dm, so you do not - * have to define any UI values for sprite accessories manually for hair and facial - * hair. Just add in new hair types and the game will naturally adapt. - * - * !!WARNING!!: changing existing hair information can be VERY hazardous to savefiles, - * to the point where you may completely corrupt a server's savefiles. Please refrain - * from doing this unless you absolutely know what you are doing, and have defined a - * conversion in savefile.dm - */ - -/datum/sprite_accessory - /// The icon file the accessory is located in. - var/icon - /// The icon_state of the accessory. - var/icon_state - /// The preview name of the accessory. - var/name - /// Determines if the accessory will be skipped or included in random hair generations. - var/gender = NEUTER - /// Something that can be worn by either gender, but looks different on each. - var/gender_specific = FALSE - /// Determines if the accessory will be skipped by color preferences. - var/use_static - /** - * Currently only used by mutantparts so don't worry about hair and stuff. - * This is the source that this accessory will get its color from. Default is MUTCOLOR, but can also be HAIR, FACEHAIR, EYECOLOR and 0 if none. - */ - var/color_src = MUTANT_COLOR - /// Is this part locked from roundstart selection? Used for parts that apply effects. - var/locked = FALSE - /// Should we center the sprite? - var/center = FALSE - /// The width of the sprite in pixels. Used to center it if necessary. - var/dimension_x = 32 - /// The height of the sprite in pixels. Used to center it if necessary. - var/dimension_y = 32 - /// Should this sprite block emissives? - var/em_block = FALSE - /// Determines if this is considered "sane" for the purpose of [/proc/randomize_human_normie] - /// Basically this is to blacklist the extremely wacky stuff from being picked in random human generation. - var/natural_spawn = TRUE - -/datum/sprite_accessory/blank - name = SPRITE_ACCESSORY_NONE - icon_state = SPRITE_ACCESSORY_NONE - -//////////////// -// Hair Masks // -//////////////// - -/datum/hair_mask - var/icon/icon = 'icons/mob/human/hair_masks.dmi' - var/icon_state = "" - /// Strict coverage zones will always have the hair mask applied to them, even if a piece of hair at that location would normally resist being masked. - /// If a piece of headware only covers the top of the head, it should only strictly cover the top zone. But a mostly-enclosed helmet might strictly cover almost all zones. - var/strict_coverage_zones = NONE - -/datum/hair_mask/standard_hat_middle - icon_state = "hide_above_45deg" - strict_coverage_zones = HAIR_APPENDAGE_TOP - -/datum/hair_mask/standard_hat_low - icon_state = "hide_above_45deg_low" - strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR - -/datum/hair_mask/winterhood - icon_state = "hide_winterhood" - strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR - -/datum/hair_mask/hoodie - icon_state = "hide_hoodie" - strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR - -////////////////////// -// Hair Definitions // -////////////////////// -// Cache of each hairstyle's icon after being blended with the given masks -// "joined mask types" is each mask's type as a string joined by commas (for no masks, it is the empty string) -// /datum/sprite_accessory/hair path -> list(joined mask types -> icon) -GLOBAL_LIST_EMPTY(blended_hair_icons_cache) - -/datum/sprite_accessory/hair - icon = 'icons/mob/human/human_face.dmi' // default icon for all hairs - var/y_offset = 0 // Y offset to apply so we can have hair that reaches above the player sprite's visual bounding box - - // Some hair will have "appendages", such as pony tails, that stick out from certain parts of the head. These can be layered above or below headwear and resist being masked away by hair masks. - // Lists should be icon_state strings associated with the HAIR_APPENDAGE defines specifying the part of the head they stick out from. - // hair_appendages_inner contains icon_states that go in the normal hair layer, hair_appendages_outer contains icon_states that go above the layer for headwear. - // hair_appendages_inner will be masked normally if their HAIR_APPENDAGE zone is strictly masked by a piece of clothing (a fully enclosed helmet with a transparent visor will strictly mask all zones, a small hat will only strictly mask the top, etc.). - // hair_appendages_outer will never be masked at all and will just not be shown if their zone has strict masking. These should generally not have visible sprites for every dir. - var/list/hair_appendages_inner = null - var/list/hair_appendages_outer = null - -/// Retrieve the base hair icon with all hair appendeges blended in, with hair masks applied, from the cache, or generate it if it doesn't exist -/datum/sprite_accessory/hair/proc/getCachedIcon(list/hair_masks) - var/icon/cachedIcon - var/joinedMasks = LAZYLEN(hair_masks) ? jointext(hair_masks, ",") : "" - var/list/masks_to_icons = GLOB.blended_hair_icons_cache[type] - if(!masks_to_icons) - GLOB.blended_hair_icons_cache[type] = list() - else - cachedIcon = masks_to_icons[joinedMasks] - - if(!cachedIcon) - if(LAZYLEN(hair_masks)) - if(LAZYLEN(hair_appendages_inner)) - // Check if there are any hair appendages in a zone that is not strictly masked - var/found_mask_dodger = FALSE - for(var/datum/hair_mask/mask as anything in hair_masks) - for(var/appendage in hair_appendages_inner) - var/zone = hair_appendages_inner[appendage] - if(!(zone & mask.strict_coverage_zones)) - found_mask_dodger = TRUE - - if(found_mask_dodger) - // We have to process each icon individually - cachedIcon = icon(icon, icon_state) - // mask the base icon - for(var/datum/hair_mask/mask as anything in hair_masks) - var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) - mask_icon.Shift(SOUTH, y_offset) - cachedIcon.Blend(mask_icon, ICON_ADD) - - // mask the appendages if required and add them to the base icon - for(var/appendage_icon_state in hair_appendages_inner) - var/icon/appendage_icon = icon(icon, appendage_icon_state) - var/zone = hair_appendages_inner[appendage_icon_state] - for(var/datum/hair_mask/mask as anything in hair_masks) - if(zone & mask.strict_coverage_zones) - var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) - mask_icon.Shift(SOUTH, y_offset) - appendage_icon.Blend(mask_icon, ICON_ADD) - cachedIcon.Blend(appendage_icon, ICON_OVERLAY) - else - // No mask dodgers, so we can just mask the full (hopefully cached) icon - cachedIcon = icon(getCachedIcon()) - for(var/datum/hair_mask/mask as anything in hair_masks) - var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) - mask_icon.Shift(SOUTH, y_offset) - cachedIcon.Blend(mask_icon, ICON_ADD) - else - // No hair appendages, so just apply all hair masks to the base icon - cachedIcon = icon(icon, icon_state) - for(var/datum/hair_mask/mask as anything in hair_masks) - var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) - mask_icon.Shift(SOUTH, y_offset) - cachedIcon.Blend(mask_icon, ICON_ADD) - else - // no hair masks - cachedIcon = icon(icon, icon_state) - if(LAZYLEN(hair_appendages_inner)) - for(var/appendage_icon_state in hair_appendages_inner) - var/icon/appendage_icon = icon(icon, appendage_icon_state) - cachedIcon.Blend(appendage_icon, ICON_OVERLAY) - // set cache - GLOB.blended_hair_icons_cache[type][joinedMasks] = cachedIcon - return cachedIcon - - -// please make sure they're sorted alphabetically and, where needed, categorized -// try to capitalize the names please~ -// try to spell -// you do not need to define _s or _l sub-states, game automatically does this for you - -/datum/sprite_accessory/hair/afro - name = "Afro" - icon_state = "hair_afro" - -/datum/sprite_accessory/hair/afro2 - name = "Afro 2" - icon_state = "hair_afro2" - -/datum/sprite_accessory/hair/afro_large - name = "Afro (Large)" - icon_state = "hair_bigafro" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/afro_huge - name = "Afro (Huge)" - icon_state = "hair_hugeafro" - y_offset = 6 - natural_spawn = FALSE - -/datum/sprite_accessory/hair/allthefuzz - name = "All The Fuzz" - icon_state = "hair_allthefuzz" - -/datum/sprite_accessory/hair/antenna - name = "Ahoge" - icon_state = "hair_antenna" - hair_appendages_inner = list("hair_antenna_a1" = HAIR_APPENDAGE_TOP) - -/datum/sprite_accessory/hair/bald - name = "Bald" - icon_state = null - -/datum/sprite_accessory/hair/balding - name = "Balding Hair" - icon_state = "hair_e" - -/datum/sprite_accessory/hair/bedhead - name = "Bedhead" - icon_state = "hair_bedhead" - -/datum/sprite_accessory/hair/bedhead2 - name = "Bedhead 2" - icon_state = "hair_bedheadv2" - -/datum/sprite_accessory/hair/bedhead3 - name = "Bedhead 3" - icon_state = "hair_bedheadv3" - -/datum/sprite_accessory/hair/bedheadv4 - name = "Bedhead 4x" - icon_state = "hair_bedheadv4" - -/datum/sprite_accessory/hair/bedheadlong - name = "Long Bedhead" - icon_state = "hair_long_bedhead" - -/datum/sprite_accessory/hair/bedheadfloorlength - name = "Floorlength Bedhead" - icon_state = "hair_floorlength_bedhead" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/badlycut - name = "Shorter Long Bedhead" - icon_state = "hair_verybadlycut" - -/datum/sprite_accessory/hair/beehive - name = "Beehive" - icon_state = "hair_beehive" - -/datum/sprite_accessory/hair/beehive2 - name = "Beehive 2" - icon_state = "hair_beehivev2" - -/datum/sprite_accessory/hair/bob - name = "Bob Hair" - icon_state = "hair_bob" - -/datum/sprite_accessory/hair/bob2 - name = "Bob Hair 2" - icon_state = "hair_bob2" - -/datum/sprite_accessory/hair/bob3 - name = "Bob Hair 3" - icon_state = "hair_bobcut" - -/datum/sprite_accessory/hair/bob4 - name = "Bob Hair 4" - icon_state = "hair_bob4" - -/datum/sprite_accessory/hair/bobcurl - name = "Bobcurl" - icon_state = "hair_bobcurl" - -/datum/sprite_accessory/hair/boddicker - name = "Boddicker" - icon_state = "hair_boddicker" - -/datum/sprite_accessory/hair/bowlcut - name = "Bowlcut" - icon_state = "hair_bowlcut" - -/datum/sprite_accessory/hair/bowlcut2 - name = "Bowlcut 2" - icon_state = "hair_bowlcut2" - -/datum/sprite_accessory/hair/braid - name = "Braid (Floorlength)" - icon_state = "hair_braid" - hair_appendages_inner = list("hair_braid_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_braid_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/braided - name = "Braided" - icon_state = "hair_braided" - -/datum/sprite_accessory/hair/front_braid - name = "Braided Front" - icon_state = "hair_braidfront" - hair_appendages_inner = list("hair_braidfront_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_braidfront_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/not_floorlength_braid - name = "Braid (High)" - icon_state = "hair_braid2" - hair_appendages_inner = list("hair_braid2_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_braid2_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/lowbraid - name = "Braid (Low)" - icon_state = "hair_hbraid" - -/datum/sprite_accessory/hair/shortbraid - name = "Braid (Short)" - icon_state = "hair_shortbraid" - hair_appendages_inner = list("hair_shortbraid_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_shortbraid_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/braidtail - name = "Braided Tail" - icon_state = "hair_braidtail" - hair_appendages_inner = list("hair_braidtail_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_braidtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/bun - name = "Bun Head" - icon_state = "hair_bun" - -/datum/sprite_accessory/hair/bun2 - name = "Bun Head 2" - icon_state = "hair_bunhead2" - hair_appendages_inner = list("hair_bunhead2_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_bunhead2_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/bun3 - name = "Bun Head 3" - icon_state = "hair_bun3" - -/datum/sprite_accessory/hair/largebun - name = "Bun (Large)" - icon_state = "hair_largebun" - -/datum/sprite_accessory/hair/manbun - name = "Bun (Manbun)" - icon_state = "hair_manbun" - hair_appendages_inner = list("hair_manbun_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_manbun_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/tightbun - name = "Bun (Tight)" - icon_state = "hair_tightbun" - -/datum/sprite_accessory/hair/business - name = "Business Hair" - icon_state = "hair_business" - -/datum/sprite_accessory/hair/business2 - name = "Business Hair 2" - icon_state = "hair_business2" - -/datum/sprite_accessory/hair/business3 - name = "Business Hair 3" - icon_state = "hair_business3" - -/datum/sprite_accessory/hair/business4 - name = "Business Hair 4" - icon_state = "hair_business4" - -/datum/sprite_accessory/hair/buzz - name = "Buzzcut" - icon_state = "hair_buzzcut" - -/datum/sprite_accessory/hair/chinbob - name = "Chin-Length Bob Cut" - icon_state = "hair_chinbob" - -/datum/sprite_accessory/hair/comet - name = "Comet" - icon_state = "hair_comet" - -/datum/sprite_accessory/hair/cia - name = "CIA" - icon_state = "hair_cia" - -/datum/sprite_accessory/hair/coffeehouse - name = "Coffee House" - icon_state = "hair_coffeehouse" - -/datum/sprite_accessory/hair/combover - name = "Combover" - icon_state = "hair_combover" - -/datum/sprite_accessory/hair/cornrows1 - name = "Cornrows" - icon_state = "hair_cornrows" - -/datum/sprite_accessory/hair/cornrows2 - name = "Cornrows 2" - icon_state = "hair_cornrows2" - -/datum/sprite_accessory/hair/cornrowbun - name = "Cornrow Bun" - icon_state = "hair_cornrowbun" - -/datum/sprite_accessory/hair/cornrowbraid - name = "Cornrow Braid" - icon_state = "hair_cornrowbraid" - -/datum/sprite_accessory/hair/cornrowdualtail - name = "Cornrow Tail" - icon_state = "hair_cornrowtail" - hair_appendages_inner = list("hair_cornrowtail_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_cornrowtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/crew - name = "Crewcut" - icon_state = "hair_crewcut" - -/datum/sprite_accessory/hair/curls - name = "Curls" - icon_state = "hair_curls" - -/datum/sprite_accessory/hair/cut - name = "Cut Hair" - icon_state = "hair_c" - -/datum/sprite_accessory/hair/dandpompadour - name = "Dandy Pompadour" - icon_state = "hair_dandypompadour" - -/datum/sprite_accessory/hair/devillock - name = "Devil Lock" - icon_state = "hair_devilock" - -/datum/sprite_accessory/hair/doublebun - name = "Double Bun" - icon_state = "hair_doublebun" - hair_appendages_inner = list("hair_doublebun_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_doublebun_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/dreadlocks - name = "Dreadlocks" - icon_state = "hair_dreads" - -/datum/sprite_accessory/hair/drillhair - name = "Drillruru" - icon_state = "hair_drillruru" - hair_appendages_inner = list("hair_drillruru_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_drillruru_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/drillhairextended - name = "Drill Hair (Extended)" - icon_state = "hair_drillhairextended" - hair_appendages_inner = list("hair_drillhairextended_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_drillhairextended_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/emo - name = "Emo" - icon_state = "hair_emo" - -/datum/sprite_accessory/hair/emofrine - name = "Emo Fringe" - icon_state = "hair_emofringe" - -/datum/sprite_accessory/hair/nofade - name = "Fade (None)" - icon_state = "hair_nofade" - -/datum/sprite_accessory/hair/highfade - name = "Fade (High)" - icon_state = "hair_highfade" - -/datum/sprite_accessory/hair/medfade - name = "Fade (Medium)" - icon_state = "hair_medfade" - -/datum/sprite_accessory/hair/lowfade - name = "Fade (Low)" - icon_state = "hair_lowfade" - -/datum/sprite_accessory/hair/baldfade - name = "Fade (Bald)" - icon_state = "hair_baldfade" - -/datum/sprite_accessory/hair/feather - name = "Feather" - icon_state = "hair_feather" - -/datum/sprite_accessory/hair/father - name = "Father" - icon_state = "hair_father" - -/datum/sprite_accessory/hair/sargeant - name = "Flat Top" - icon_state = "hair_sargeant" - -/datum/sprite_accessory/hair/flair - name = "Flair" - icon_state = "hair_flair" - -/datum/sprite_accessory/hair/bigflattop - name = "Flat Top (Big)" - icon_state = "hair_bigflattop" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/flow_hair - name = "Flow Hair" - icon_state = "hair_f" - -/datum/sprite_accessory/hair/gelled - name = "Gelled Back" - icon_state = "hair_gelled" - -/datum/sprite_accessory/hair/gentle - name = "Gentle" - icon_state = "hair_gentle" - -/datum/sprite_accessory/hair/halfbang - name = "Half-banged Hair" - icon_state = "hair_halfbang" - -/datum/sprite_accessory/hair/halfbang2 - name = "Half-banged Hair 2" - icon_state = "hair_halfbang2" - -/datum/sprite_accessory/hair/halfshaved - name = "Half-shaved" - icon_state = "hair_halfshaved" - -/datum/sprite_accessory/hair/hedgehog - name = "Hedgehog Hair" - icon_state = "hair_hedgehog" - -/datum/sprite_accessory/hair/himecut - name = "Hime Cut" - icon_state = "hair_himecut" - -/datum/sprite_accessory/hair/himecut2 - name = "Hime Cut 2" - icon_state = "hair_himecut2" - -/datum/sprite_accessory/hair/shorthime - name = "Hime Cut (Short)" - icon_state = "hair_shorthime" - -/datum/sprite_accessory/hair/himeup - name = "Hime Updo" - icon_state = "hair_himeup" - -/datum/sprite_accessory/hair/hitop - name = "Hitop" - icon_state = "hair_hitop" - -/datum/sprite_accessory/hair/jade - name = "Jade" - icon_state = "hair_jade" - -/datum/sprite_accessory/hair/jensen - name = "Jensen Hair" - icon_state = "hair_jensen" - -/datum/sprite_accessory/hair/joestar - name = "Joestar" - icon_state = "hair_joestar" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/keanu - name = "Keanu Hair" - icon_state = "hair_keanu" - -/datum/sprite_accessory/hair/kusangi - name = "Kusanagi Hair" - icon_state = "hair_kusanagi" - -/datum/sprite_accessory/hair/long - name = "Long Hair 1" - icon_state = "hair_long" - hair_appendages_inner = list("hair_long_a1" = HAIR_APPENDAGE_HANGING_REAR) - -/datum/sprite_accessory/hair/long2 - name = "Long Hair 2" - icon_state = "hair_long2" - hair_appendages_inner = list("hair_long2_a1" = HAIR_APPENDAGE_HANGING_REAR) - -/datum/sprite_accessory/hair/long3 - name = "Long Hair 3" - icon_state = "hair_long3" - hair_appendages_inner = list("hair_long3_a1" = HAIR_APPENDAGE_HANGING_REAR) - -/datum/sprite_accessory/hair/long_over_eye - name = "Long Over Eye" - icon_state = "hair_longovereye" - -/datum/sprite_accessory/hair/longbangs - name = "Long Bangs" - icon_state = "hair_lbangs" - -/datum/sprite_accessory/hair/longemo - name = "Long Emo" - icon_state = "hair_longemo" - -/datum/sprite_accessory/hair/longfringe - name = "Long Fringe" - icon_state = "hair_longfringe" - -/datum/sprite_accessory/hair/sidepartlongalt - name = "Long Side Part" - icon_state = "hair_longsidepart" - hair_appendages_inner = list("hair_longsidepart_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_longsidepart_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/megaeyebrows - name = "Mega Eyebrows" - icon_state = "hair_megaeyebrows" - -/datum/sprite_accessory/hair/messy - name = "Messy" - icon_state = "hair_messy" - -/datum/sprite_accessory/hair/modern - name = "Modern" - icon_state = "hair_modern" - -/datum/sprite_accessory/hair/mohawk - name = "Mohawk" - icon_state = "hair_d" - natural_spawn = FALSE // sorry little one - -/datum/sprite_accessory/hair/nitori - name = "Nitori" - icon_state = "hair_nitori" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/reversemohawk - name = "Mohawk (Reverse)" - icon_state = "hair_reversemohawk" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/shavedmohawk - name = "Mohawk (Shaved)" - icon_state = "hair_shavedmohawk" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/unshavenmohawk - name = "Mohawk (Unshaven)" - icon_state = "hair_unshaven_mohawk" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/mulder - name = "Mulder" - icon_state = "hair_mulder" - -/datum/sprite_accessory/hair/odango - name = "Odango" - icon_state = "hair_odango" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/ombre - name = "Ombre" - icon_state = "hair_ombre" - -/datum/sprite_accessory/hair/oneshoulder - name = "One Shoulder" - icon_state = "hair_oneshoulder" - -/datum/sprite_accessory/hair/over_eye - name = "Over Eye" - icon_state = "hair_shortovereye" - -/datum/sprite_accessory/hair/hair_overeyetwo - name = "Over Eye 2" - icon_state = "hair_overeyetwo" - -/datum/sprite_accessory/hair/oxton - name = "Oxton" - icon_state = "hair_oxton" - -/datum/sprite_accessory/hair/parted - name = "Parted" - icon_state = "hair_parted" - -/datum/sprite_accessory/hair/partedside - name = "Parted (Side)" - icon_state = "hair_part" - -/datum/sprite_accessory/hair/kagami - name = "Pigtails" - icon_state = "hair_kagami" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/pigtail - name = "Pigtails 2" - icon_state = "hair_pigtails" - natural_spawn = FALSE - -/datum/sprite_accessory/hair/pigtail2 - name = "Pigtails 3" - icon_state = "hair_pigtails2" - natural_spawn = FALSE - hair_appendages_inner = list("hair_pigtails2_a1" = HAIR_APPENDAGE_LEFT, "hair_pigtails2_a2" = HAIR_APPENDAGE_RIGHT) - -/datum/sprite_accessory/hair/pixie - name = "Pixie Cut" - icon_state = "hair_pixie" - -/datum/sprite_accessory/hair/pompadour - name = "Pompadour" - icon_state = "hair_pompadour" - -/datum/sprite_accessory/hair/bigpompadour - name = "Pompadour (Big)" - icon_state = "hair_bigpompadour" - -/datum/sprite_accessory/hair/ponytail1 - name = "Ponytail" - icon_state = "hair_ponytail" - -/datum/sprite_accessory/hair/ponytail2 - name = "Ponytail 2" - icon_state = "hair_ponytail2" - -/datum/sprite_accessory/hair/ponytail3 - name = "Ponytail 3" - icon_state = "hair_ponytail3" - -/datum/sprite_accessory/hair/ponytail4 - name = "Ponytail 4" - icon_state = "hair_ponytail4" - hair_appendages_inner = list("hair_ponytail4_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_ponytail4_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/ponytail5 - name = "Ponytail 5" - icon_state = "hair_ponytail5" - hair_appendages_inner = list("hair_ponytail5_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_ponytail5_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/ponytail6 - name = "Ponytail 6" - icon_state = "hair_ponytail6" - hair_appendages_inner = list("hair_ponytail6_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_ponytail6_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/ponytail7 - name = "Ponytail 7" - icon_state = "hair_ponytail7" - hair_appendages_inner = list("hair_ponytail7_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_ponytail7_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/highponytail - name = "Ponytail (High)" - icon_state = "hair_highponytail" - hair_appendages_inner = list("hair_highponytail_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_highponytail_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/stail - name = "Ponytail (Short)" - icon_state = "hair_stail" - hair_appendages_inner = list("hair_stail_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_stail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/longponytail - name = "Ponytail (Long)" - icon_state = "hair_longstraightponytail" - hair_appendages_inner = list("hair_longstraightponytail_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_longstraightponytail_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/countryponytail - name = "Ponytail (Country)" - icon_state = "hair_country" - hair_appendages_inner = list("hair_country_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_country_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/fringetail - name = "Ponytail (Fringe)" - icon_state = "hair_fringetail" - -/datum/sprite_accessory/hair/sidetail - name = "Ponytail (Side)" - icon_state = "hair_sidetail" - -/datum/sprite_accessory/hair/sidetail2 - name = "Ponytail (Side) 2" - icon_state = "hair_sidetail2" - -/datum/sprite_accessory/hair/sidetail3 - name = "Ponytail (Side) 3" - icon_state = "hair_sidetail3" - hair_appendages_inner = list("hair_sidetail3_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_sidetail3_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/sidetail4 - name = "Ponytail (Side) 4" - icon_state = "hair_sidetail4" - hair_appendages_inner = list("hair_sidetail4_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_sidetail4_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/spikyponytail - name = "Ponytail (Spiky)" - icon_state = "hair_spikyponytail" - hair_appendages_inner = list("hair_spikyponytail_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_spikyponytail_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/poofy - name = "Poofy" - icon_state = "hair_poofy" - -/datum/sprite_accessory/hair/quiff - name = "Quiff" - icon_state = "hair_quiff" - -/datum/sprite_accessory/hair/ronin - name = "Ronin" - icon_state = "hair_ronin" - -/datum/sprite_accessory/hair/shaved - name = "Shaved" - icon_state = "hair_shaved" - -/datum/sprite_accessory/hair/shavedpart - name = "Shaved Part" - icon_state = "hair_shavedpart" - -/datum/sprite_accessory/hair/shortbangs - name = "Short Bangs" - icon_state = "hair_shortbangs" - -/datum/sprite_accessory/hair/shortbangs2 - name = "Short Bangs 2" - icon_state = "hair_shortbangs2" - -/datum/sprite_accessory/hair/short - name = "Short Hair" - icon_state = "hair_a" - -/datum/sprite_accessory/hair/shorthair2 - name = "Short Hair 2" - icon_state = "hair_shorthair2" - -/datum/sprite_accessory/hair/shorthair3 - name = "Short Hair 3" - icon_state = "hair_shorthair3" - -/datum/sprite_accessory/hair/shorthair4 - name = "Short Hair 4" - icon_state = "hair_d" - -/datum/sprite_accessory/hair/shorthair5 - name = "Short Hair 5" - icon_state = "hair_e" - -/datum/sprite_accessory/hair/shorthair6 - name = "Short Hair 6" - icon_state = "hair_f" - -/datum/sprite_accessory/hair/shorthair7 - name = "Short Hair 7" - icon_state = "hair_shorthairg" - -/datum/sprite_accessory/hair/shorthaireighties - name = "Short Hair 80s" - icon_state = "hair_80s" - -/datum/sprite_accessory/hair/rosa - name = "Short Hair Rosa" - icon_state = "hair_rosa" - -/datum/sprite_accessory/hair/shoulderlength - name = "Shoulder-length Hair" - icon_state = "hair_b" - -/datum/sprite_accessory/hair/sidecut - name = "Sidecut" - icon_state = "hair_sidecut" - -/datum/sprite_accessory/hair/skinhead - name = "Skinhead" - icon_state = "hair_skinhead" - -/datum/sprite_accessory/hair/protagonist - name = "Slightly Long Hair" - icon_state = "hair_protagonist" - -/datum/sprite_accessory/hair/spiky - name = "Spiky" - icon_state = "hair_spikey" - -/datum/sprite_accessory/hair/spiky2 - name = "Spiky 2" - icon_state = "hair_spiky" - -/datum/sprite_accessory/hair/spiky3 - name = "Spiky 3" - icon_state = "hair_spiky2" - -/datum/sprite_accessory/hair/swept - name = "Swept Back Hair" - icon_state = "hair_swept" - -/datum/sprite_accessory/hair/swept2 - name = "Swept Back Hair 2" - icon_state = "hair_swept2" - -/datum/sprite_accessory/hair/thinning - name = "Thinning" - icon_state = "hair_thinning" - -/datum/sprite_accessory/hair/thinningfront - name = "Thinning (Front)" - icon_state = "hair_thinningfront" - -/datum/sprite_accessory/hair/thinningrear - name = "Thinning (Rear)" - icon_state = "hair_thinningrear" - -/datum/sprite_accessory/hair/topknot - name = "Topknot" - icon_state = "hair_topknot" - -/datum/sprite_accessory/hair/tressshoulder - name = "Tress Shoulder" - icon_state = "hair_tressshoulder" - hair_appendages_inner = list("hair_tressshoulder_a1" = HAIR_APPENDAGE_HANGING_FRONT) - hair_appendages_outer = list("hair_tressshoulder_a1o" = HAIR_APPENDAGE_HANGING_FRONT) - -/datum/sprite_accessory/hair/trimmed - name = "Trimmed" - icon_state = "hair_trimmed" - -/datum/sprite_accessory/hair/trimflat - name = "Trim Flat" - icon_state = "hair_trimflat" - -/datum/sprite_accessory/hair/twintails - name = "Twintails" - icon_state = "hair_twintail" - -/datum/sprite_accessory/hair/undercut - name = "Undercut" - icon_state = "hair_undercut" - -/datum/sprite_accessory/hair/undercutleft - name = "Undercut Left" - icon_state = "hair_undercutleft" - -/datum/sprite_accessory/hair/undercutright - name = "Undercut Right" - icon_state = "hair_undercutright" - -/datum/sprite_accessory/hair/unkept - name = "Unkept" - icon_state = "hair_unkept" - -/datum/sprite_accessory/hair/updo - name = "Updo" - icon_state = "hair_updo" - hair_appendages_inner = list("hair_updo_a1" = HAIR_APPENDAGE_TOP) - -/datum/sprite_accessory/hair/longer - name = "Very Long Hair" - icon_state = "hair_vlong" - -/datum/sprite_accessory/hair/longest - name = "Very Long Hair 2" - icon_state = "hair_longest" - -/datum/sprite_accessory/hair/longest2 - name = "Very Long Over Eye" - icon_state = "hair_longest2" - -/datum/sprite_accessory/hair/veryshortovereye - name = "Very Short Over Eye" - icon_state = "hair_veryshortovereyealternate" - -/datum/sprite_accessory/hair/longestalt - name = "Very Long with Fringe" - icon_state = "hair_vlongfringe" - -/datum/sprite_accessory/hair/volaju - name = "Volaju" - icon_state = "hair_volaju" - -/datum/sprite_accessory/hair/wisp - name = "Wisp" - icon_state = "hair_wisp" - hair_appendages_inner = list("hair_wisp_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_wisp_a1o" = HAIR_APPENDAGE_REAR) - -/datum/sprite_accessory/hair/ziegler - name = "Ziegler" - icon_state = "hair_ziegler" - hair_appendages_inner = list("hair_ziegler_a1" = HAIR_APPENDAGE_REAR) - hair_appendages_outer = list("hair_ziegler_a1o" = HAIR_APPENDAGE_REAR) - -/* -///////////////////////////////////// -/ =---------------------------= / -/ == Gradient Hair Definitions == / -/ =---------------------------= / -///////////////////////////////////// -*/ - -/datum/sprite_accessory/gradient - icon = 'icons/mob/human/species/hair_gradients.dmi' - ///whether this gradient applies to hair and/or beards. Some gradients do not work well on beards. - var/gradient_category = GRADIENT_APPLIES_TO_HAIR|GRADIENT_APPLIES_TO_FACIAL_HAIR - -/datum/sprite_accessory/gradient/none - name = SPRITE_ACCESSORY_NONE - icon_state = SPRITE_ACCESSORY_NONE - -/datum/sprite_accessory/gradient/full - name = "Full" - icon_state = "full" - -/datum/sprite_accessory/gradient/fadeup - name = "Fade Up" - icon_state = "fadeup" - -/datum/sprite_accessory/gradient/fadedown - name = "Fade Down" - icon_state = "fadedown" - -/datum/sprite_accessory/gradient/vertical_split - name = "Vertical Split" - icon_state = "vsplit" - -/datum/sprite_accessory/gradient/horizontal_split - name = "Horizontal Split" - icon_state = "bottomflat" - -/datum/sprite_accessory/gradient/reflected - name = "Reflected" - icon_state = "reflected_high" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/reflected/beard - icon_state = "reflected_high_beard" - gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR - -/datum/sprite_accessory/gradient/reflected_inverse - name = "Reflected Inverse" - icon_state = "reflected_inverse_high" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/reflected_inverse/beard - icon_state = "reflected_inverse_high_beard" - gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR - -/datum/sprite_accessory/gradient/wavy - name = "Wavy" - icon_state = "wavy" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/long_fade_up - name = "Long Fade Up" - icon_state = "long_fade_up" - -/datum/sprite_accessory/gradient/long_fade_down - name = "Long Fade Down" - icon_state = "long_fade_down" - -/datum/sprite_accessory/gradient/short_fade_up - name = "Short Fade Up" - icon_state = "short_fade_up" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/short_fade_up/beard - icon_state = "short_fade_down" - gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR - -/datum/sprite_accessory/gradient/short_fade_down - name = "Short Fade Down" - icon_state = "short_fade_down_beard" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/short_fade_down/beard - icon_state = "short_fade_down_beard" - gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR - -/datum/sprite_accessory/gradient/wavy_spike - name = "Spiked Wavy" - icon_state = "wavy_spiked" - gradient_category = GRADIENT_APPLIES_TO_HAIR - -/datum/sprite_accessory/gradient/striped - name = "striped" - icon_state = "striped" - -/datum/sprite_accessory/gradient/striped_vertical - name = "Striped Vertical" - icon_state = "striped_vertical" - -///////////////////////////// -// Facial Hair Definitions // -///////////////////////////// - -/datum/sprite_accessory/facial_hair - icon = 'icons/mob/human/human_face.dmi' - gender = MALE // barf (unless you're a dorf, dorfs dig chix w/ beards :P) - em_block = TRUE - -// please make sure they're sorted alphabetically and categorized - -/datum/sprite_accessory/facial_hair/abe - name = "Beard (Abraham Lincoln)" - icon_state = "facial_abe" - -/datum/sprite_accessory/facial_hair/brokenman - name = "Beard (Broken Man)" - icon_state = "facial_brokenman" - natural_spawn = FALSE - -/datum/sprite_accessory/facial_hair/chinstrap - name = "Beard (Chinstrap)" - icon_state = "facial_chin" - -/datum/sprite_accessory/facial_hair/dwarf - name = "Beard (Dwarf)" - icon_state = "facial_dwarf" - -/datum/sprite_accessory/facial_hair/fullbeard - name = "Beard (Full)" - icon_state = "facial_fullbeard" - -/datum/sprite_accessory/facial_hair/croppedfullbeard - name = "Beard (Cropped Fullbeard)" - icon_state = "facial_croppedfullbeard" - -/datum/sprite_accessory/facial_hair/gt - name = "Beard (Goatee)" - icon_state = "facial_gt" - -/datum/sprite_accessory/facial_hair/hip - name = "Beard (Hipster)" - icon_state = "facial_hip" - -/datum/sprite_accessory/facial_hair/jensen - name = "Beard (Jensen)" - icon_state = "facial_jensen" - -/datum/sprite_accessory/facial_hair/neckbeard - name = "Beard (Neckbeard)" - icon_state = "facial_neckbeard" - -/datum/sprite_accessory/facial_hair/vlongbeard - name = "Beard (Very Long)" - icon_state = "facial_wise" - -/datum/sprite_accessory/facial_hair/muttonmus - name = "Beard (Muttonmus)" - icon_state = "facial_muttonmus" - -/datum/sprite_accessory/facial_hair/martialartist - name = "Beard (Martial Artist)" - icon_state = "facial_martialartist" - natural_spawn = FALSE - -/datum/sprite_accessory/facial_hair/chinlessbeard - name = "Beard (Chinless Beard)" - icon_state = "facial_chinlessbeard" - -/datum/sprite_accessory/facial_hair/moonshiner - name = "Beard (Moonshiner)" - icon_state = "facial_moonshiner" - -/datum/sprite_accessory/facial_hair/longbeard - name = "Beard (Long)" - icon_state = "facial_longbeard" - -/datum/sprite_accessory/facial_hair/volaju - name = "Beard (Volaju)" - icon_state = "facial_volaju" - -/datum/sprite_accessory/facial_hair/threeoclock - name = "Beard (Three o Clock Shadow)" - icon_state = "facial_3oclock" - -/datum/sprite_accessory/facial_hair/fiveoclock - name = "Beard (Five o Clock Shadow)" - icon_state = "facial_fiveoclock" - -/datum/sprite_accessory/facial_hair/fiveoclockm - name = "Beard (Five o Clock Moustache)" - icon_state = "facial_5oclockmoustache" - -/datum/sprite_accessory/facial_hair/sevenoclock - name = "Beard (Seven o Clock Shadow)" - icon_state = "facial_7oclock" - -/datum/sprite_accessory/facial_hair/sevenoclockm - name = "Beard (Seven o Clock Moustache)" - icon_state = "facial_7oclockmoustache" - -/datum/sprite_accessory/facial_hair/moustache - name = "Moustache" - icon_state = "facial_moustache" - -/datum/sprite_accessory/facial_hair/pencilstache - name = "Moustache (Pencilstache)" - icon_state = "facial_pencilstache" - -/datum/sprite_accessory/facial_hair/smallstache - name = "Moustache (Smallstache)" - icon_state = "facial_smallstache" - -/datum/sprite_accessory/facial_hair/walrus - name = "Moustache (Walrus)" - icon_state = "facial_walrus" - -/datum/sprite_accessory/facial_hair/fu - name = "Moustache (Fu Manchu)" - icon_state = "facial_fumanchu" - -/datum/sprite_accessory/facial_hair/hogan - name = "Moustache (Hulk Hogan)" - icon_state = "facial_hogan" //-Neek - -/datum/sprite_accessory/facial_hair/selleck - name = "Moustache (Selleck)" - icon_state = "facial_selleck" - -/datum/sprite_accessory/facial_hair/chaplin - name = "Moustache (Square)" - icon_state = "facial_chaplin" - -/datum/sprite_accessory/facial_hair/vandyke - name = "Moustache (Van Dyke)" - icon_state = "facial_vandyke" - -/datum/sprite_accessory/facial_hair/watson - name = "Moustache (Watson)" - icon_state = "facial_watson" - -/datum/sprite_accessory/facial_hair/handlebar - name = "Moustache (Handlebar)" - icon_state = "facial_handlebar" - -/datum/sprite_accessory/facial_hair/handlebar2 - name = "Moustache (Handlebar 2)" - icon_state = "facial_handlebar2" - -/datum/sprite_accessory/facial_hair/elvis - name = "Sideburns (Elvis)" - icon_state = "facial_elvis" - -/datum/sprite_accessory/facial_hair/mutton - name = "Sideburns (Mutton Chops)" - icon_state = "facial_mutton" - -/datum/sprite_accessory/facial_hair/sideburn - name = "Sideburns" - icon_state = "facial_sideburn" - -/datum/sprite_accessory/facial_hair/shaved - name = "Shaved" - icon_state = SPRITE_ACCESSORY_NONE - gender = NEUTER - -/datum/sprite_accessory/clothing - abstract_type = /datum/sprite_accessory/clothing - /// Allows you to specify a greyscale config - var/greyscale_config - /// Icon state in the digitigrade template file to use if the wearer is digitigrade. - /// If null, no special digitigrade handling is done. - var/digi_icon_state - /// Color pallete for static colored underwear, like hearts. - /// Used so greyscale copies can have the same palette. - var/greyscale_colors = "#FFFFFF#FFFFFF#FFFFFF" - /// The layer this sprite accessory should render on - var/layer = BODY_LAYER - /// What kind of gender shaping this sprite accessory should use (in case your sprite gets a weird missing pixel in the center) - var/female_sprite_flags = FEMALE_UNIFORM_FULL - -/// Override to return a different icon state given a bodytype or physique -/datum/sprite_accessory/clothing/proc/get_icon_state(physique, bodyshape) - return icon_state - -/** - * Generate an appearance from this clothing datum - * - * * color - if this is NOT a statically colored clothing article and NOT gags, uses this color. - * * physique - physique of the wearer (male or female) - * * bodyshape - bodyshape of the wearer (humanoid, digitigrade, etc) - */ -/datum/sprite_accessory/clothing/proc/make_appearance(color = COLOR_WHITE, physique = MALE, bodyshape = BODYSHAPE_HUMANOID) - var/static/list/cached_icons = list() - var/use_female = physique == FEMALE && female_sprite_flags - var/use_digi = digi_icon_state && (bodyshape & BODYSHAPE_DIGITIGRADE) - var/female_sprite_flags_to_use = female_sprite_flags - var/icon_state_to_use = get_icon_state(physique, bodyshape) - if(use_digi && female_sprite_flags_to_use) - female_sprite_flags_to_use = FEMALE_UNIFORM_TOP_ONLY // No bottom gender shaping for the digi legs - - var/key = "[icon_state_to_use]-[greyscale_config || "ng"]-[use_female]-[use_digi]-[greyscale_colors]" - var/mutable_appearance/result - if(cached_icons[key]) // it's already cached - result = mutable_appearance(icon(cached_icons[key])) - - else if(greyscale_config || use_female || use_digi) // icon ops ahead - var/icon/created = icon(greyscale_config ? SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) : icon, icon_state_to_use) - if(use_female) - created = wear_female_version(icon_state_to_use, icon, female_sprite_flags_to_use) - if(use_digi) - var/icon/replacement = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade_underwear, greyscale_colors), digi_icon_state) - created = replace_icon_legs(created, replacement) - - cached_icons[key] = fcopy_rsc(created) - result = mutable_appearance(created) - - else // no caching necessary - result = mutable_appearance(icon, icon_state) - - result.layer = -layer - result.color = use_static ? null : color - - return result - - -/////////////////////////// -// Underwear Definitions // -/////////////////////////// - -/datum/sprite_accessory/clothing/underwear - icon = 'icons/mob/clothing/underwear.dmi' - use_static = FALSE - em_block = TRUE - abstract_type = /datum/sprite_accessory/clothing/underwear - -//MALE UNDERWEAR -/datum/sprite_accessory/clothing/underwear/nude - name = "Nude" - icon_state = null - gender = NEUTER - -/datum/sprite_accessory/clothing/underwear/nude/make_appearance(mob/living/carbon/human/for_who) - return - -/datum/sprite_accessory/clothing/underwear/male_briefs - name = "Briefs" - icon_state = "male_briefs" - gender = MALE - -/datum/sprite_accessory/clothing/underwear/male_boxers - name = "Boxers" - icon_state = "male_boxers" - gender = MALE - digi_icon_state = "boxers" - -/datum/sprite_accessory/clothing/underwear/male_stripe - name = "Striped Boxers" - icon_state = "male_stripe" - gender = MALE - digi_icon_state = "boxers_stripe" - -/datum/sprite_accessory/clothing/underwear/male_midway - name = "Midway Boxers" - icon_state = "male_midway" - gender = MALE - digi_icon_state = "midway" - -/datum/sprite_accessory/clothing/underwear/male_longjohns - name = "Long Johns" - icon_state = "male_longjohns" - gender = MALE - digi_icon_state = "longjohns" - -/datum/sprite_accessory/clothing/underwear/male_kinky - name = "Jockstrap" - icon_state = "male_kinky" - gender = MALE - -/datum/sprite_accessory/clothing/underwear/male_mankini - name = "Mankini" - icon_state = "male_mankini" - gender = MALE - -/datum/sprite_accessory/clothing/underwear/male_hearts - name = "Hearts Boxers" - icon_state = "male_hearts" - gender = MALE - use_static = TRUE - digi_icon_state = "boxers_stripe_threecolor" - greyscale_colors = "#D62626#EEEEEE#D62626#" - -/datum/sprite_accessory/clothing/underwear/male_commie - name = "Commie Boxers" - icon_state = "male_commie" - gender = MALE - use_static = TRUE - digi_icon_state = "boxers_stripe_twocolor" - greyscale_colors = "#D62626#D1B62C#D62626" - -/datum/sprite_accessory/clothing/underwear/male_usastripe - name = "Freedom Boxers" - icon_state = "male_assblastusa" - gender = MALE - use_static = TRUE - digi_icon_state = "boxers_stripe_threecolor" - greyscale_colors = "#D62626#EEEEEE#2E26D6" - -/datum/sprite_accessory/clothing/underwear/male_uk - name = "UK Boxers" - icon_state = "male_uk" - gender = MALE - use_static = TRUE - digi_icon_state = "boxers_stripe_threecolor" - greyscale_colors = "#D62626#EEEEEE#2E26D6" - -//FEMALE UNDERWEAR -/datum/sprite_accessory/clothing/underwear/female_bikini - name = "Bikini" - icon_state = "female_bikini" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/female_lace - name = "Lace Bikini" - icon_state = "female_lace" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/female_bralette - name = "Bralette w/ Boyshorts" - icon_state = "female_bralette" - gender = FEMALE - digi_icon_state = "short_short" - -/datum/sprite_accessory/clothing/underwear/female_sport - name = "Sports Bra w/ Boyshorts" - icon_state = "female_sport" - gender = FEMALE - digi_icon_state = "short" - -/datum/sprite_accessory/clothing/underwear/female_thong - name = "Thong" - icon_state = "female_thong" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/female_strapless - name = "Strapless Bikini" - icon_state = "female_strapless" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/female_babydoll - name = "Babydoll" - icon_state = "female_babydoll" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/swimsuit_onepiece - name = "One-Piece Swimsuit" - icon_state = "swim_onepiece" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_onepiece - name = "Strapless One-Piece Swimsuit" - icon_state = "swim_strapless_onepiece" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/swimsuit_twopiece - name = "Two-Piece Swimsuit" - icon_state = "swim_twopiece" - gender = FEMALE - digi_icon_state = "short_short" - -/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_twopiece - name = "Strapless Two-Piece Swimsuit" - icon_state = "swim_strapless_twopiece" - gender = FEMALE - digi_icon_state = "short_short" - -/datum/sprite_accessory/clothing/underwear/swimsuit_stripe - name = "Strapless Striped Swimsuit" - icon_state = "swim_stripe" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/swimsuit_halter - name = "Halter Swimsuit" - icon_state = "swim_halter" - gender = FEMALE - -/datum/sprite_accessory/clothing/underwear/female_white_neko - name = "Neko Bikini (White)" - icon_state = "female_neko_white" - gender = FEMALE - use_static = TRUE - -/datum/sprite_accessory/clothing/underwear/female_black_neko - name = "Neko Bikini (Black)" - icon_state = "female_neko_black" - gender = FEMALE - use_static = TRUE - -/datum/sprite_accessory/clothing/underwear/female_commie - name = "Commie Bikini" - icon_state = "female_commie" - gender = FEMALE - use_static = TRUE - -/datum/sprite_accessory/clothing/underwear/female_usastripe - name = "Freedom Bikini" - icon_state = "female_assblastusa" - gender = FEMALE - use_static = TRUE - -/datum/sprite_accessory/clothing/underwear/female_uk - name = "UK Bikini" - icon_state = "female_uk" - gender = FEMALE - use_static = TRUE - -/datum/sprite_accessory/clothing/underwear/female_kinky - name = "Lingerie" - icon_state = "female_kinky" - gender = FEMALE - use_static = TRUE - -//////////////////////////// -// Undershirt Definitions // -//////////////////////////// - -/datum/sprite_accessory/clothing/undershirt - icon = 'icons/mob/clothing/underwear.dmi' - em_block = TRUE - abstract_type = /datum/sprite_accessory/clothing/undershirt - -/datum/sprite_accessory/clothing/undershirt/nude - name = "Nude" - icon_state = null - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/nude/make_appearance(mob/living/carbon/human/for_who) - return - -// please make sure they're sorted alphabetically and categorized - -/datum/sprite_accessory/clothing/undershirt/bluejersey - name = "Jersey (Blue)" - icon_state = "shirt_bluejersey" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/redjersey - name = "Jersey (Red)" - icon_state = "shirt_redjersey" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/bluepolo - name = "Polo Shirt (Blue)" - icon_state = "bluepolo" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/grayyellowpolo - name = "Polo Shirt (Gray-Yellow)" - icon_state = "grayyellowpolo" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/redpolo - name = "Polo Shirt (Red)" - icon_state = "redpolo" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/whitepolo - name = "Polo Shirt (White)" - icon_state = "whitepolo" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/alienshirt - name = "Shirt (Alien)" - icon_state = "shirt_alien" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/mondmondjaja - name = "Shirt (Band)" - icon_state = "band" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/shirt_black - name = "Shirt (Black)" - icon_state = "shirt_black" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/blueshirt - name = "Shirt (Blue)" - icon_state = "shirt_blue" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/clownshirt - name = "Shirt (Clown)" - icon_state = "shirt_clown" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/commie - name = "Shirt (Commie)" - icon_state = "shirt_commie" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/greenshirt - name = "Shirt (Green)" - icon_state = "shirt_green" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/shirt_grey - name = "Shirt (Grey)" - icon_state = "shirt_grey" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/ian - name = "Shirt (Ian)" - icon_state = "ian" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/ilovent - name = "Shirt (I Love NT)" - icon_state = "ilovent" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/lover - name = "Shirt (Lover)" - icon_state = "lover" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/matroska - name = "Shirt (Matroska)" - icon_state = "matroska" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/meat - name = "Shirt (Meat)" - icon_state = "shirt_meat" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/nano - name = "Shirt (Nanotrasen)" - icon_state = "shirt_nano" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/peace - name = "Shirt (Peace)" - icon_state = "peace" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/pacman - name = "Shirt (Pogoman)" - icon_state = "pogoman" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/question - name = "Shirt (Question)" - icon_state = "shirt_question" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/redshirt - name = "Shirt (Red)" - icon_state = "shirt_red" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/skull - name = "Shirt (Skull)" - icon_state = "shirt_skull" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/ss13 - name = "Shirt (SS13)" - icon_state = "shirt_ss13" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/stripe - name = "Shirt (Striped)" - icon_state = "shirt_stripes" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tiedye - name = "Shirt (Tie-dye)" - icon_state = "shirt_tiedye" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/uk - name = "Shirt (UK)" - icon_state = "uk" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/usa - name = "Shirt (USA)" - icon_state = "shirt_assblastusa" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/shirt_white - name = "Shirt (White)" - icon_state = "shirt_white" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/blackshortsleeve - name = "Short-sleeved Shirt (Black)" - icon_state = "blackshortsleeve" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/blueshortsleeve - name = "Short-sleeved Shirt (Blue)" - icon_state = "blueshortsleeve" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/greenshortsleeve - name = "Short-sleeved Shirt (Green)" - icon_state = "greenshortsleeve" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/purpleshortsleeve - name = "Short-sleeved Shirt (Purple)" - icon_state = "purpleshortsleeve" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/whiteshortsleeve - name = "Short-sleeved Shirt (White)" - icon_state = "whiteshortsleeve" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/sports_bra - name = "Sports Bra" - icon_state = "sports_bra" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/sports_bra2 - name = "Sports Bra (Alt)" - icon_state = "sports_bra_alt" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/blueshirtsport - name = "Sports Shirt (Blue)" - icon_state = "blueshirtsport" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/greenshirtsport - name = "Sports Shirt (Green)" - icon_state = "greenshirtsport" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/redshirtsport - name = "Sports Shirt (Red)" - icon_state = "redshirtsport" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tank_black - name = "Tank Top (Black)" - icon_state = "tank_black" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tankfire - name = "Tank Top (Fire)" - icon_state = "tank_fire" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tank_grey - name = "Tank Top (Grey)" - icon_state = "tank_grey" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/female_midriff - name = "Tank Top (Midriff)" - icon_state = "tank_midriff" - gender = FEMALE - -/datum/sprite_accessory/clothing/undershirt/tank_red - name = "Tank Top (Red)" - icon_state = "tank_red" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tankstripe - name = "Tank Top (Striped)" - icon_state = "tank_stripes" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tank_white - name = "Tank Top (White)" - icon_state = "tank_white" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/redtop - name = "Top (Red)" - icon_state = "redtop" - gender = FEMALE - -/datum/sprite_accessory/clothing/undershirt/whitetop - name = "Top (White)" - icon_state = "whitetop" - gender = FEMALE - -/datum/sprite_accessory/clothing/undershirt/tshirt_blue - name = "T-Shirt (Blue)" - icon_state = "blueshirt" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tshirt_green - name = "T-Shirt (Green)" - icon_state = "greenshirt" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/tshirt_red - name = "T-Shirt (Red)" - icon_state = "redshirt" - gender = NEUTER - -/datum/sprite_accessory/clothing/undershirt/yellowshirt - name = "T-Shirt (Yellow)" - icon_state = "yellowshirt" - gender = NEUTER - -/////////////////////// -// Socks Definitions // -/////////////////////// - -/datum/sprite_accessory/clothing/socks - icon = 'icons/mob/clothing/underwear.dmi' - em_block = TRUE - abstract_type = /datum/sprite_accessory/clothing/socks - -/datum/sprite_accessory/clothing/socks/nude - name = "Nude" - icon_state = null - -/datum/sprite_accessory/clothing/socks/nude/make_appearance(mob/living/carbon/human/for_who) - return - -// please make sure they're sorted alphabetically and categorized - -/datum/sprite_accessory/clothing/socks/ace_knee - name = "Knee-high (Ace)" - icon_state = "ace_knee" - -/datum/sprite_accessory/clothing/socks/bee_knee - name = "Knee-high (Bee)" - icon_state = "bee_knee" - -/datum/sprite_accessory/clothing/socks/black_knee - name = "Knee-high (Black)" - icon_state = "black_knee" - -/datum/sprite_accessory/clothing/socks/commie_knee - name = "Knee-High (Commie)" - icon_state = "commie_knee" - -/datum/sprite_accessory/clothing/socks/usa_knee - name = "Knee-High (Freedom)" - icon_state = "assblastusa_knee" - -/datum/sprite_accessory/clothing/socks/rainbow_knee - name = "Knee-high (Rainbow)" - icon_state = "rainbow_knee" - -/datum/sprite_accessory/clothing/socks/striped_knee - name = "Knee-high (Striped)" - icon_state = "striped_knee" - -/datum/sprite_accessory/clothing/socks/thin_knee - name = "Knee-high (Thin)" - icon_state = "thin_knee" - -/datum/sprite_accessory/clothing/socks/trans_knee - name = "Knee-high (Trans)" - icon_state = "trans_knee" - -/datum/sprite_accessory/clothing/socks/uk_knee - name = "Knee-High (UK)" - icon_state = "uk_knee" - -/datum/sprite_accessory/clothing/socks/white_knee - name = "Knee-high (White)" - icon_state = "white_knee" - -/datum/sprite_accessory/clothing/socks/fishnet_knee - name = "Knee-high (Fishnet)" - icon_state = "fishnet_knee" - -/datum/sprite_accessory/clothing/socks/black_norm - name = "Normal (Black)" - icon_state = "black_norm" - -/datum/sprite_accessory/clothing/socks/white_norm - name = "Normal (White)" - icon_state = "white_norm" - -/datum/sprite_accessory/clothing/socks/pantyhose - name = "Pantyhose" - icon_state = "pantyhose" - -/datum/sprite_accessory/clothing/socks/black_short - name = "Short (Black)" - icon_state = "black_short" - -/datum/sprite_accessory/clothing/socks/white_short - name = "Short (White)" - icon_state = "white_short" - -/datum/sprite_accessory/clothing/socks/stockings_blue - name = "Stockings (Blue)" - icon_state = "stockings_blue" - -/datum/sprite_accessory/clothing/socks/stockings_cyan - name = "Stockings (Cyan)" - icon_state = "stockings_cyan" - -/datum/sprite_accessory/clothing/socks/stockings_dpink - name = "Stockings (Dark Pink)" - icon_state = "stockings_dpink" - -/datum/sprite_accessory/clothing/socks/stockings_green - name = "Stockings (Green)" - icon_state = "stockings_green" - -/datum/sprite_accessory/clothing/socks/stockings_orange - name = "Stockings (Orange)" - icon_state = "stockings_orange" - -/datum/sprite_accessory/clothing/socks/stockings_programmer - name = "Stockings (Programmer)" - icon_state = "stockings_lpink" - -/datum/sprite_accessory/clothing/socks/stockings_purple - name = "Stockings (Purple)" - icon_state = "stockings_purple" - -/datum/sprite_accessory/clothing/socks/stockings_yellow - name = "Stockings (Yellow)" - icon_state = "stockings_yellow" - -/datum/sprite_accessory/clothing/socks/stockings_fishnet - name = "Stockings (Fishnet)" - icon_state = "fishnet_full" - -/datum/sprite_accessory/clothing/socks/ace_thigh - name = "Thigh-high (Ace)" - icon_state = "ace_thigh" - -/datum/sprite_accessory/clothing/socks/bee_thigh - name = "Thigh-high (Bee)" - icon_state = "bee_thigh" - -/datum/sprite_accessory/clothing/socks/black_thigh - name = "Thigh-high (Black)" - icon_state = "black_thigh" - -/datum/sprite_accessory/clothing/socks/commie_thigh - name = "Thigh-high (Commie)" - icon_state = "commie_thigh" - -/datum/sprite_accessory/clothing/socks/usa_thigh - name = "Thigh-high (Freedom)" - icon_state = "assblastusa_thigh" - -/datum/sprite_accessory/clothing/socks/rainbow_thigh - name = "Thigh-high (Rainbow)" - icon_state = "rainbow_thigh" - -/datum/sprite_accessory/clothing/socks/striped_thigh - name = "Thigh-high (Striped)" - icon_state = "striped_thigh" - -/datum/sprite_accessory/clothing/socks/thin_thigh - name = "Thigh-high (Thin)" - icon_state = "thin_thigh" - -/datum/sprite_accessory/clothing/socks/trans_thigh - name = "Thigh-high (Trans)" - icon_state = "trans_thigh" - -/datum/sprite_accessory/clothing/socks/uk_thigh - name = "Thigh-high (UK)" - icon_state = "uk_thigh" - -/datum/sprite_accessory/clothing/socks/white_thigh - name = "Thigh-high (White)" - icon_state = "white_thigh" - -/datum/sprite_accessory/clothing/socks/fishnet_thigh - name = "Thigh-high (Fishnet)" - icon_state = "fishnet_thigh" - -/datum/sprite_accessory/clothing/socks/thocks - name = "Thocks" - icon_state = "thocks" - -//////////.////////////////// -// MutantParts Definitions // -///////////////////////////// - -/datum/sprite_accessory/lizard_markings - icon = 'icons/mob/human/species/lizard/lizard_markings.dmi' - -/datum/sprite_accessory/lizard_markings/dtiger - name = "Dark Tiger Body" - icon_state = "dtiger" - gender_specific = TRUE - -/datum/sprite_accessory/lizard_markings/ltiger - name = "Light Tiger Body" - icon_state = "ltiger" - gender_specific = TRUE - -/datum/sprite_accessory/lizard_markings/lbelly - name = "Light Belly" - icon_state = "lbelly" - gender_specific = TRUE - -/datum/sprite_accessory/tails - em_block = TRUE - /// Describes which tail spine sprites to use, if any. - var/spine_key = NONE - -///Used for fish-infused tails, which come in different flavors. -/datum/sprite_accessory/tails/fish - icon = 'icons/mob/human/fish_features.dmi' - color_src = TRUE - -/datum/sprite_accessory/tails/fish/simple - name = "Simple" - icon_state = "simple" - -/datum/sprite_accessory/tails/fish/crescent - name = "Crescent" - icon_state = "crescent" - -/datum/sprite_accessory/tails/fish/long - name = "Long" - icon_state = "long" - center = TRUE - dimension_x = 38 - -/datum/sprite_accessory/tails/fish/shark - name = "Shark" - icon_state = "shark" - -/datum/sprite_accessory/tails/fish/chonky - name = "Chonky" - icon_state = "chonky" - center = TRUE - dimension_x = 36 - -/datum/sprite_accessory/tails/lizard - icon = 'icons/mob/human/species/lizard/lizard_tails.dmi' - spine_key = SPINE_KEY_LIZARD - -/datum/sprite_accessory/tails/lizard/none - name = SPRITE_ACCESSORY_NONE - icon_state = "none" - natural_spawn = FALSE - -/datum/sprite_accessory/tails/lizard/smooth - name = "Smooth" - icon_state = "smooth" - -/datum/sprite_accessory/tails/lizard/dtiger - name = "Dark Tiger" - icon_state = "dtiger" - -/datum/sprite_accessory/tails/lizard/ltiger - name = "Light Tiger" - icon_state = "ltiger" - -/datum/sprite_accessory/tails/lizard/spikes - name = "Spikes" - icon_state = "spikes" - -/datum/sprite_accessory/tails/lizard/short - name = "Short" - icon_state = "short" - spine_key = NONE - -/datum/sprite_accessory/tails/felinid/cat - name = "Cat" - icon = 'icons/mob/human/cat_features.dmi' - icon_state = "default" - color_src = HAIR_COLOR - -/datum/sprite_accessory/tails/monkey - -/datum/sprite_accessory/tails/monkey/none - name = SPRITE_ACCESSORY_NONE - icon_state = "none" - natural_spawn = FALSE - -/datum/sprite_accessory/tails/monkey/default - name = "Monkey" - icon = 'icons/mob/human/species/monkey/monkey_tail.dmi' - icon_state = "default" - color_src = FALSE - -/datum/sprite_accessory/tails/xeno - icon_state = "default" - color_src = FALSE - center = TRUE - -/datum/sprite_accessory/tails/xeno/default - name = "Xeno" - icon = 'icons/mob/human/species/alien/tail_xenomorph.dmi' - dimension_x = 40 - -/datum/sprite_accessory/tails/xeno/queen - name = "Xeno Queen" - icon = 'icons/mob/human/species/alien/tail_xenomorph_queen.dmi' - dimension_x = 64 - -/datum/sprite_accessory/pod_hair - icon = 'icons/mob/human/species/podperson_hair.dmi' - em_block = TRUE - -/datum/sprite_accessory/pod_hair/ivy - name = "Ivy" - icon_state = "ivy" - -/datum/sprite_accessory/pod_hair/cabbage - name = "Cabbage" - icon_state = "cabbage" - -/datum/sprite_accessory/pod_hair/spinach - name = "Spinach" - icon_state = "spinach" - -/datum/sprite_accessory/pod_hair/prayer - name = "Prayer" - icon_state = "prayer" - -/datum/sprite_accessory/pod_hair/vine - name = "Vine" - icon_state = "vine" - -/datum/sprite_accessory/pod_hair/shrub - name = "Shrub" - icon_state = "shrub" - -/datum/sprite_accessory/pod_hair/rose - name = "Rose" - icon_state = "rose" - -/datum/sprite_accessory/pod_hair/orchid - name = "Orchid" - icon_state = "orchid" - -/datum/sprite_accessory/pod_hair/fig - name = "Fig" - icon_state = "fig" - -/datum/sprite_accessory/pod_hair/hibiscus - name = "Hibiscus" - icon_state = "hibiscus" - -/datum/sprite_accessory/snouts - icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' - em_block = TRUE - -/datum/sprite_accessory/snouts/sharp - name = "Sharp" - icon_state = "sharp" - -/datum/sprite_accessory/snouts/round - name = "Round" - icon_state = "round" - -/datum/sprite_accessory/snouts/sharplight - name = "Sharp + Light" - icon_state = "sharplight" - -/datum/sprite_accessory/snouts/roundlight - name = "Round + Light" - icon_state = "roundlight" - -/datum/sprite_accessory/horns - icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' - em_block = TRUE - -/datum/sprite_accessory/horns/simple - name = "Simple" - icon_state = "simple" - -/datum/sprite_accessory/horns/short - name = "Short" - icon_state = "short" - -/datum/sprite_accessory/horns/curled - name = "Curled" - icon_state = "curled" - -/datum/sprite_accessory/horns/ram - name = "Ram" - icon_state = "ram" - -/datum/sprite_accessory/horns/angler - name = "Angeler" - icon_state = "angler" - -/datum/sprite_accessory/ears - icon = 'icons/mob/human/cat_features.dmi' - em_block = TRUE - -/datum/sprite_accessory/ears/cat - name = "Cat" - icon_state = "cat" - color_src = HAIR_COLOR - -/datum/sprite_accessory/ears/cat/big - name = "Big" - icon_state = "big" - -/datum/sprite_accessory/ears/cat/miqo - name = "Coeurl" - icon_state = "miqo" - -/datum/sprite_accessory/ears/cat/fold - name = "Fold" - icon_state = "fold" - -/datum/sprite_accessory/ears/cat/lynx - name = "Lynx" - icon_state = "lynx" - -/datum/sprite_accessory/ears/cat/round - name = "Round" - icon_state = "round" - -/datum/sprite_accessory/ears/cat/cybernetic - name = "Cybernetic" - icon_state = "cyber" - locked = TRUE - -/datum/sprite_accessory/ears/fox - icon = 'icons/mob/human/fox_features.dmi' - name = "Fox" - icon_state = "fox" - color_src = HAIR_COLOR - locked = TRUE - -/datum/sprite_accessory/wings - icon = 'icons/mob/human/species/wings.dmi' - em_block = TRUE - -/datum/sprite_accessory/wings_open - icon = 'icons/mob/human/species/wings.dmi' - em_block = TRUE - -/datum/sprite_accessory/wings/angel - name = "Angel" - icon_state = "angel" - color_src = FALSE - dimension_x = 46 - center = TRUE - dimension_y = 34 - locked = TRUE - -/datum/sprite_accessory/wings_open/angel - name = "Angel" - icon_state = "angel" - color_src = FALSE - dimension_x = 46 - center = TRUE - dimension_y = 34 - -/datum/sprite_accessory/wings/dragon - name = "Dragon" - icon_state = "dragon" - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/dragon - name = "Dragon" - icon_state = "dragon" - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/megamoth - name = "Megamoth" - icon_state = "megamoth" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/megamoth - name = "Megamoth" - icon_state = "megamoth" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/mothra - name = "Mothra" - icon_state = "mothra" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/mothra - name = "Mothra" - icon_state = "mothra" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/skeleton - name = "Skeleton" - icon_state = "skele" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/skeleton - name = "Skeleton" - icon_state = "skele" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/robotic - name = "Robotic" - icon_state = "robotic" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/robotic - name = "Robotic" - icon_state = "robotic" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/fly - name = "Fly" - icon_state = "fly" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/fly - name = "Fly" - icon_state = "fly" - color_src = FALSE - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/wings/slime - name = "Slime" - icon_state = "slime" - dimension_x = 96 - center = TRUE - dimension_y = 32 - locked = TRUE - -/datum/sprite_accessory/wings_open/slime - name = "Slime" - icon_state = "slime" - dimension_x = 96 - center = TRUE - dimension_y = 32 - -/datum/sprite_accessory/frills - icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' - -/datum/sprite_accessory/frills/simple - name = "Simple" - icon_state = "simple" - -/datum/sprite_accessory/frills/short - name = "Short" - icon_state = "short" - -/datum/sprite_accessory/frills/aquatic - name = "Aquatic" - icon_state = "aqua" - -/datum/sprite_accessory/spines - icon = 'icons/mob/human/species/lizard/lizard_spines.dmi' - em_block = TRUE - -/datum/sprite_accessory/tail_spines - icon = 'icons/mob/human/species/lizard/lizard_spines.dmi' - em_block = TRUE - -/datum/sprite_accessory/spines/short - name = "Short" - icon_state = "short" - -/datum/sprite_accessory/tail_spines/short - name = "Short" - icon_state = "short" - -/datum/sprite_accessory/spines/shortmeme - name = "Short + Membrane" - icon_state = "shortmeme" - -/datum/sprite_accessory/tail_spines/shortmeme - name = "Short + Membrane" - icon_state = "shortmeme" - -/datum/sprite_accessory/spines/long - name = "Long" - icon_state = "long" - -/datum/sprite_accessory/tail_spines/long - name = "Long" - icon_state = "long" - -/datum/sprite_accessory/spines/longmeme - name = "Long + Membrane" - icon_state = "longmeme" - -/datum/sprite_accessory/tail_spines/longmeme - name = "Long + Membrane" - icon_state = "longmeme" - -/datum/sprite_accessory/spines/aquatic - name = "Aquatic" - icon_state = "aqua" - -/datum/sprite_accessory/tail_spines/aquatic - name = "Aquatic" - icon_state = "aqua" - -/datum/sprite_accessory/caps - icon = 'icons/mob/human/species/mush_cap.dmi' - color_src = HAIR_COLOR - em_block = TRUE - -/datum/sprite_accessory/caps/round - name = "Round" - icon_state = "round" - -/datum/sprite_accessory/moth_wings - icon = 'icons/mob/human/species/moth/moth_wings.dmi' - color_src = null - em_block = TRUE - -/datum/sprite_accessory/moth_wings/plain - name = "Plain" - icon_state = "plain" - -/datum/sprite_accessory/moth_wings/monarch - name = "Monarch" - icon_state = "monarch" - -/datum/sprite_accessory/moth_wings/luna - name = "Luna" - icon_state = "luna" - -/datum/sprite_accessory/moth_wings/atlas - name = "Atlas" - icon_state = "atlas" - -/datum/sprite_accessory/moth_wings/reddish - name = "Reddish" - icon_state = "redish" - -/datum/sprite_accessory/moth_wings/royal - name = "Royal" - icon_state = "royal" - -/datum/sprite_accessory/moth_wings/gothic - name = "Gothic" - icon_state = "gothic" - -/datum/sprite_accessory/moth_wings/lovers - name = "Lovers" - icon_state = "lovers" - -/datum/sprite_accessory/moth_wings/whitefly - name = "White Fly" - icon_state = "whitefly" - -/datum/sprite_accessory/moth_wings/burnt_off - name = "Burnt Off" - icon_state = "burnt_off" - locked = TRUE - -/datum/sprite_accessory/moth_wings/firewatch - name = "Firewatch" - icon_state = "firewatch" - -/datum/sprite_accessory/moth_wings/deathhead - name = "Deathshead" - icon_state = "deathhead" - -/datum/sprite_accessory/moth_wings/poison - name = "Poison" - icon_state = "poison" - -/datum/sprite_accessory/moth_wings/ragged - name = "Ragged" - icon_state = "ragged" - -/datum/sprite_accessory/moth_wings/moonfly - name = "Moon Fly" - icon_state = "moonfly" - -/datum/sprite_accessory/moth_wings/snow - name = "Snow" - icon_state = "snow" - -/datum/sprite_accessory/moth_wings/oakworm - name = "Oak Worm" - icon_state = "oakworm" - -/datum/sprite_accessory/moth_wings/jungle - name = "Jungle" - icon_state = "jungle" - -/datum/sprite_accessory/moth_wings/witchwing - name = "Witch Wing" - icon_state = "witchwing" - -/datum/sprite_accessory/moth_wings/rosy - name = "Rosy" - icon_state = "rosy" - -/datum/sprite_accessory/moth_wings/feathery - name = "Feathery" - icon_state = "feathery" - -/datum/sprite_accessory/moth_wings/brown - name = "Brown" - icon_state = "brown" - -/datum/sprite_accessory/moth_wings/plasmafire - name = "Plasmafire" - icon_state = "plasmafire" - -/datum/sprite_accessory/moth_wings/moffra - name = "Moffra" - icon_state = "moffra" - -/datum/sprite_accessory/moth_wings/lightbearer - name = "Lightbearer" - icon_state = "lightbearer" - -/datum/sprite_accessory/moth_wings/dipped - name = "Dipped" - icon_state = "dipped" - -/datum/sprite_accessory/moth_antennae //Finally splitting the sprite - icon = 'icons/mob/human/species/moth/moth_antennae.dmi' - color_src = null - -/datum/sprite_accessory/moth_antennae/plain - name = "Plain" - icon_state = "plain" - -/datum/sprite_accessory/moth_antennae/reddish - name = "Reddish" - icon_state = "reddish" - -/datum/sprite_accessory/moth_antennae/royal - name = "Royal" - icon_state = "royal" - -/datum/sprite_accessory/moth_antennae/gothic - name = "Gothic" - icon_state = "gothic" - -/datum/sprite_accessory/moth_antennae/whitefly - name = "White Fly" - icon_state = "whitefly" - -/datum/sprite_accessory/moth_antennae/lovers - name = "Lovers" - icon_state = "lovers" - -/datum/sprite_accessory/moth_antennae/burnt_off - name = "Burnt Off" - icon_state = "burnt_off" - -/datum/sprite_accessory/moth_antennae/firewatch - name = "Firewatch" - icon_state = "firewatch" - -/datum/sprite_accessory/moth_antennae/deathhead - name = "Deathshead" - icon_state = "deathhead" - -/datum/sprite_accessory/moth_antennae/poison - name = "Poison" - icon_state = "poison" - -/datum/sprite_accessory/moth_antennae/ragged - name = "Ragged" - icon_state = "ragged" - -/datum/sprite_accessory/moth_antennae/moonfly - name = "Moon Fly" - icon_state = "moonfly" - -/datum/sprite_accessory/moth_antennae/oakworm - name = "Oak Worm" - icon_state = "oakworm" - -/datum/sprite_accessory/moth_antennae/jungle - name = "Jungle" - icon_state = "jungle" - -/datum/sprite_accessory/moth_antennae/witchwing - name = "Witch Wing" - icon_state = "witchwing" - -/datum/sprite_accessory/moth_antennae/regal - name = "Regal" - icon_state = "regal" -/datum/sprite_accessory/moth_antennae/rosy - name = "Rosy" - icon_state = "rosy" - -/datum/sprite_accessory/moth_antennae/feathery - name = "Feathery" - icon_state = "feathery" - -/datum/sprite_accessory/moth_antennae/brown - name = "Brown" - icon_state = "brown" - -/datum/sprite_accessory/moth_antennae/plasmafire - name = "Plasmafire" - icon_state = "plasmafire" - -/datum/sprite_accessory/moth_antennae/moffra - name = "Moffra" - icon_state = "moffra" - -/datum/sprite_accessory/moth_antennae/lightbearer - name = "Lightbearer" - icon_state = "lightbearer" - -/datum/sprite_accessory/moth_antennae/dipped - name = "Dipped" - icon_state = "dipped" - -/datum/sprite_accessory/moth_markings // the markings that moths can have. finally something other than the boring tan - icon = 'icons/mob/human/species/moth/moth_markings.dmi' - color_src = null - -/datum/sprite_accessory/moth_markings/reddish - name = "Reddish" - icon_state = "reddish" - -/datum/sprite_accessory/moth_markings/royal - name = "Royal" - icon_state = "royal" - -/datum/sprite_accessory/moth_markings/gothic - name = "Gothic" - icon_state = "gothic" - -/datum/sprite_accessory/moth_markings/whitefly - name = "White Fly" - icon_state = "whitefly" - -/datum/sprite_accessory/moth_markings/lovers - name = "Lovers" - icon_state = "lovers" - -/datum/sprite_accessory/moth_markings/burnt_off - name = "Burnt Off" - icon_state = "burnt_off" - -/datum/sprite_accessory/moth_markings/firewatch - name = "Firewatch" - icon_state = "firewatch" - -/datum/sprite_accessory/moth_markings/deathhead - name = "Deathshead" - icon_state = "deathhead" - -/datum/sprite_accessory/moth_markings/poison - name = "Poison" - icon_state = "poison" - -/datum/sprite_accessory/moth_markings/ragged - name = "Ragged" - icon_state = "ragged" - -/datum/sprite_accessory/moth_markings/moonfly - name = "Moon Fly" - icon_state = "moonfly" - -/datum/sprite_accessory/moth_markings/oakworm - name = "Oak Worm" - icon_state = "oakworm" - -/datum/sprite_accessory/moth_markings/jungle - name = "Jungle" - icon_state = "jungle" - -/datum/sprite_accessory/moth_markings/witchwing - name = "Witch Wing" - icon_state = "witchwing" - -/datum/sprite_accessory/moth_markings/lightbearer - name = "Lightbearer" - icon_state = "lightbearer" - -/datum/sprite_accessory/moth_markings/dipped - name = "Dipped" - icon_state = "dipped" diff --git a/code/datums/sprite_accessories/_sprite_accessory.dm b/code/datums/sprite_accessories/_sprite_accessory.dm new file mode 100644 index 000000000000..4cdd8ab6e589 --- /dev/null +++ b/code/datums/sprite_accessories/_sprite_accessory.dm @@ -0,0 +1,66 @@ +/* + * Hello and welcome to sprite_accessories: For sprite accessories, such as hair, + * facial hair, and possibly tattoos and stuff somewhere along the line. This file is + * intended to be friendly for people with little to no actual coding experience. + * The process of adding in new hairstyles has been made pain-free and easy to do. + * Enjoy! - Doohl + * + * + * Notice: This all gets automatically compiled in a list in dna.dm, so you do not + * have to define any UI values for sprite accessories manually for hair and facial + * hair. Just add in new hair types and the game will naturally adapt. + * + * !!WARNING!!: changing existing hair information can be VERY hazardous to savefiles, + * to the point where you may completely corrupt a server's savefiles. Please refrain + * from doing this unless you absolutely know what you are doing, and have defined a + * conversion in savefile.dm + */ + +/datum/sprite_accessory + /// The icon file the accessory is located in. + var/icon + /// The icon_state of the accessory. + var/icon_state + /// The preview name of the accessory. + var/name + /// Determines if the accessory will be skipped or included in random hair generations. + var/gender = NEUTER + /// Something that can be worn by either gender, but looks different on each. + var/gender_specific = FALSE + /// Determines if the accessory will be skipped by color preferences. + var/use_static + /** + * Currently only used by mutantparts so don't worry about hair and stuff. + * This is the source that this accessory will get its color from. Default is MUTCOLOR, but can also be HAIR, FACEHAIR, EYECOLOR and 0 if none. + */ + var/color_src = MUTANT_COLOR + /// Is this part locked from roundstart selection? Used for parts that apply effects. + var/locked = FALSE + /// Should we center the sprite? + var/center = FALSE + /// The width of the sprite in pixels. Used to center it if necessary. + var/dimension_x = 32 + /// The height of the sprite in pixels. Used to center it if necessary. + var/dimension_y = 32 + /// Should this sprite block emissives? + var/em_block = FALSE + /// Determines if this is considered "sane" for the purpose of [/proc/randomize_human_normie] + /// Basically this is to blacklist the extremely wacky stuff from being picked in random human generation. + var/natural_spawn = TRUE + +/datum/sprite_accessory/blank + name = SPRITE_ACCESSORY_NONE + icon_state = SPRITE_ACCESSORY_NONE + +//////////.////////////////// +// MutantParts Definitions // +///////////////////////////// + +/datum/sprite_accessory/caps + icon = 'icons/mob/human/species/mush_cap.dmi' + color_src = HAIR_COLOR + em_block = TRUE + +/datum/sprite_accessory/caps/round + name = "Round" + icon_state = "round" diff --git a/code/datums/sprite_accessories/clothing.dm b/code/datums/sprite_accessories/clothing.dm new file mode 100644 index 000000000000..a2ab967b701b --- /dev/null +++ b/code/datums/sprite_accessories/clothing.dm @@ -0,0 +1,718 @@ +/datum/sprite_accessory/clothing + abstract_type = /datum/sprite_accessory/clothing + /// Allows you to specify a greyscale config + var/greyscale_config + /// Icon state in the digitigrade template file to use if the wearer is digitigrade. + /// If null, no special digitigrade handling is done. + var/digi_icon_state + /// Color pallete for static colored underwear, like hearts. + /// Used so greyscale copies can have the same palette. + var/greyscale_colors = "#FFFFFF#FFFFFF#FFFFFF" + /// The layer this sprite accessory should render on + var/layer = BODY_LAYER + /// What kind of gender shaping this sprite accessory should use (in case your sprite gets a weird missing pixel in the center) + var/female_sprite_flags = FEMALE_UNIFORM_FULL + +/// Override to return a different icon state given a bodytype or physique +/datum/sprite_accessory/clothing/proc/get_icon_state(physique, bodyshape) + return icon_state + +/** + * Generate an appearance from this clothing datum + * + * * color - if this is NOT a statically colored clothing article and NOT gags, uses this color. + * * physique - physique of the wearer (male or female) + * * bodyshape - bodyshape of the wearer (humanoid, digitigrade, etc) + */ +/datum/sprite_accessory/clothing/proc/make_appearance(color = COLOR_WHITE, physique = MALE, bodyshape = BODYSHAPE_HUMANOID) + var/static/list/cached_icons = list() + var/use_female = physique == FEMALE && female_sprite_flags + var/use_digi = digi_icon_state && (bodyshape & BODYSHAPE_DIGITIGRADE) + var/female_sprite_flags_to_use = female_sprite_flags + var/icon_state_to_use = get_icon_state(physique, bodyshape) + if(use_digi && female_sprite_flags_to_use) + female_sprite_flags_to_use = FEMALE_UNIFORM_TOP_ONLY // No bottom gender shaping for the digi legs + + var/key = "[icon_state_to_use]-[greyscale_config || "ng"]-[use_female]-[use_digi]-[greyscale_colors]" + var/mutable_appearance/result + if(cached_icons[key]) // it's already cached + result = mutable_appearance(icon(cached_icons[key])) + + else if(greyscale_config || use_female || use_digi) // icon ops ahead + var/icon/created = icon(greyscale_config ? SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) : icon, icon_state_to_use) + if(use_female) + created = wear_female_version(icon_state_to_use, icon, female_sprite_flags_to_use) + if(use_digi) + var/icon/replacement = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade_underwear, greyscale_colors), digi_icon_state) + created = replace_icon_legs(created, replacement) + + cached_icons[key] = fcopy_rsc(created) + result = mutable_appearance(created) + + else // no caching necessary + result = mutable_appearance(icon, icon_state) + + result.layer = -layer + result.color = use_static ? null : color + + return result + + +/////////////////////////// +// Underwear Definitions // +/////////////////////////// + +/datum/sprite_accessory/clothing/underwear + icon = 'icons/mob/clothing/underwear.dmi' + use_static = FALSE + em_block = TRUE + abstract_type = /datum/sprite_accessory/clothing/underwear + +//MALE UNDERWEAR +/datum/sprite_accessory/clothing/underwear/nude + name = "Nude" + icon_state = null + gender = NEUTER + +/datum/sprite_accessory/clothing/underwear/nude/make_appearance(mob/living/carbon/human/for_who) + return + +/datum/sprite_accessory/clothing/underwear/male_briefs + name = "Briefs" + icon_state = "male_briefs" + gender = MALE + +/datum/sprite_accessory/clothing/underwear/male_boxers + name = "Boxers" + icon_state = "male_boxers" + gender = MALE + digi_icon_state = "boxers" + +/datum/sprite_accessory/clothing/underwear/male_stripe + name = "Striped Boxers" + icon_state = "male_stripe" + gender = MALE + digi_icon_state = "boxers_stripe" + +/datum/sprite_accessory/clothing/underwear/male_midway + name = "Midway Boxers" + icon_state = "male_midway" + gender = MALE + digi_icon_state = "midway" + +/datum/sprite_accessory/clothing/underwear/male_longjohns + name = "Long Johns" + icon_state = "male_longjohns" + gender = MALE + digi_icon_state = "longjohns" + +/datum/sprite_accessory/clothing/underwear/male_kinky + name = "Jockstrap" + icon_state = "male_kinky" + gender = MALE + +/datum/sprite_accessory/clothing/underwear/male_mankini + name = "Mankini" + icon_state = "male_mankini" + gender = MALE + +/datum/sprite_accessory/clothing/underwear/male_hearts + name = "Hearts Boxers" + icon_state = "male_hearts" + gender = MALE + use_static = TRUE + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#D62626#" + +/datum/sprite_accessory/clothing/underwear/male_commie + name = "Commie Boxers" + icon_state = "male_commie" + gender = MALE + use_static = TRUE + digi_icon_state = "boxers_stripe_twocolor" + greyscale_colors = "#D62626#D1B62C#D62626" + +/datum/sprite_accessory/clothing/underwear/male_usastripe + name = "Freedom Boxers" + icon_state = "male_assblastusa" + gender = MALE + use_static = TRUE + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#2E26D6" + +/datum/sprite_accessory/clothing/underwear/male_uk + name = "UK Boxers" + icon_state = "male_uk" + gender = MALE + use_static = TRUE + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#2E26D6" + +//FEMALE UNDERWEAR +/datum/sprite_accessory/clothing/underwear/female_bikini + name = "Bikini" + icon_state = "female_bikini" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/female_lace + name = "Lace Bikini" + icon_state = "female_lace" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/female_bralette + name = "Bralette w/ Boyshorts" + icon_state = "female_bralette" + gender = FEMALE + digi_icon_state = "short_short" + +/datum/sprite_accessory/clothing/underwear/female_sport + name = "Sports Bra w/ Boyshorts" + icon_state = "female_sport" + gender = FEMALE + digi_icon_state = "short" + +/datum/sprite_accessory/clothing/underwear/female_thong + name = "Thong" + icon_state = "female_thong" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/female_strapless + name = "Strapless Bikini" + icon_state = "female_strapless" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/female_babydoll + name = "Babydoll" + icon_state = "female_babydoll" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/swimsuit_onepiece + name = "One-Piece Swimsuit" + icon_state = "swim_onepiece" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_onepiece + name = "Strapless One-Piece Swimsuit" + icon_state = "swim_strapless_onepiece" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/swimsuit_twopiece + name = "Two-Piece Swimsuit" + icon_state = "swim_twopiece" + gender = FEMALE + digi_icon_state = "short_short" + +/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_twopiece + name = "Strapless Two-Piece Swimsuit" + icon_state = "swim_strapless_twopiece" + gender = FEMALE + digi_icon_state = "short_short" + +/datum/sprite_accessory/clothing/underwear/swimsuit_stripe + name = "Strapless Striped Swimsuit" + icon_state = "swim_stripe" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/swimsuit_halter + name = "Halter Swimsuit" + icon_state = "swim_halter" + gender = FEMALE + +/datum/sprite_accessory/clothing/underwear/female_white_neko + name = "Neko Bikini (White)" + icon_state = "female_neko_white" + gender = FEMALE + use_static = TRUE + +/datum/sprite_accessory/clothing/underwear/female_black_neko + name = "Neko Bikini (Black)" + icon_state = "female_neko_black" + gender = FEMALE + use_static = TRUE + +/datum/sprite_accessory/clothing/underwear/female_commie + name = "Commie Bikini" + icon_state = "female_commie" + gender = FEMALE + use_static = TRUE + +/datum/sprite_accessory/clothing/underwear/female_usastripe + name = "Freedom Bikini" + icon_state = "female_assblastusa" + gender = FEMALE + use_static = TRUE + +/datum/sprite_accessory/clothing/underwear/female_uk + name = "UK Bikini" + icon_state = "female_uk" + gender = FEMALE + use_static = TRUE + +/datum/sprite_accessory/clothing/underwear/female_kinky + name = "Lingerie" + icon_state = "female_kinky" + gender = FEMALE + use_static = TRUE + +//////////////////////////// +// Undershirt Definitions // +//////////////////////////// + +/datum/sprite_accessory/clothing/undershirt + icon = 'icons/mob/clothing/underwear.dmi' + em_block = TRUE + abstract_type = /datum/sprite_accessory/clothing/undershirt + +/datum/sprite_accessory/clothing/undershirt/nude + name = "Nude" + icon_state = null + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/nude/make_appearance(mob/living/carbon/human/for_who) + return + +// please make sure they're sorted alphabetically and categorized + +/datum/sprite_accessory/clothing/undershirt/bluejersey + name = "Jersey (Blue)" + icon_state = "shirt_bluejersey" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/redjersey + name = "Jersey (Red)" + icon_state = "shirt_redjersey" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/bluepolo + name = "Polo Shirt (Blue)" + icon_state = "bluepolo" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/grayyellowpolo + name = "Polo Shirt (Gray-Yellow)" + icon_state = "grayyellowpolo" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/redpolo + name = "Polo Shirt (Red)" + icon_state = "redpolo" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/whitepolo + name = "Polo Shirt (White)" + icon_state = "whitepolo" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/alienshirt + name = "Shirt (Alien)" + icon_state = "shirt_alien" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/mondmondjaja + name = "Shirt (Band)" + icon_state = "band" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/shirt_black + name = "Shirt (Black)" + icon_state = "shirt_black" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/blueshirt + name = "Shirt (Blue)" + icon_state = "shirt_blue" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/clownshirt + name = "Shirt (Clown)" + icon_state = "shirt_clown" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/commie + name = "Shirt (Commie)" + icon_state = "shirt_commie" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/greenshirt + name = "Shirt (Green)" + icon_state = "shirt_green" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/shirt_grey + name = "Shirt (Grey)" + icon_state = "shirt_grey" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/ian + name = "Shirt (Ian)" + icon_state = "ian" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/ilovent + name = "Shirt (I Love NT)" + icon_state = "ilovent" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/lover + name = "Shirt (Lover)" + icon_state = "lover" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/matroska + name = "Shirt (Matroska)" + icon_state = "matroska" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/meat + name = "Shirt (Meat)" + icon_state = "shirt_meat" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/nano + name = "Shirt (Nanotrasen)" + icon_state = "shirt_nano" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/peace + name = "Shirt (Peace)" + icon_state = "peace" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/pacman + name = "Shirt (Pogoman)" + icon_state = "pogoman" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/question + name = "Shirt (Question)" + icon_state = "shirt_question" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/redshirt + name = "Shirt (Red)" + icon_state = "shirt_red" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/skull + name = "Shirt (Skull)" + icon_state = "shirt_skull" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/ss13 + name = "Shirt (SS13)" + icon_state = "shirt_ss13" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/stripe + name = "Shirt (Striped)" + icon_state = "shirt_stripes" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tiedye + name = "Shirt (Tie-dye)" + icon_state = "shirt_tiedye" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/uk + name = "Shirt (UK)" + icon_state = "uk" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/usa + name = "Shirt (USA)" + icon_state = "shirt_assblastusa" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/shirt_white + name = "Shirt (White)" + icon_state = "shirt_white" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/blackshortsleeve + name = "Short-sleeved Shirt (Black)" + icon_state = "blackshortsleeve" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/blueshortsleeve + name = "Short-sleeved Shirt (Blue)" + icon_state = "blueshortsleeve" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/greenshortsleeve + name = "Short-sleeved Shirt (Green)" + icon_state = "greenshortsleeve" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/purpleshortsleeve + name = "Short-sleeved Shirt (Purple)" + icon_state = "purpleshortsleeve" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/whiteshortsleeve + name = "Short-sleeved Shirt (White)" + icon_state = "whiteshortsleeve" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/sports_bra + name = "Sports Bra" + icon_state = "sports_bra" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/sports_bra2 + name = "Sports Bra (Alt)" + icon_state = "sports_bra_alt" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/blueshirtsport + name = "Sports Shirt (Blue)" + icon_state = "blueshirtsport" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/greenshirtsport + name = "Sports Shirt (Green)" + icon_state = "greenshirtsport" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/redshirtsport + name = "Sports Shirt (Red)" + icon_state = "redshirtsport" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tank_black + name = "Tank Top (Black)" + icon_state = "tank_black" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tankfire + name = "Tank Top (Fire)" + icon_state = "tank_fire" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tank_grey + name = "Tank Top (Grey)" + icon_state = "tank_grey" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/female_midriff + name = "Tank Top (Midriff)" + icon_state = "tank_midriff" + gender = FEMALE + +/datum/sprite_accessory/clothing/undershirt/tank_red + name = "Tank Top (Red)" + icon_state = "tank_red" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tankstripe + name = "Tank Top (Striped)" + icon_state = "tank_stripes" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tank_white + name = "Tank Top (White)" + icon_state = "tank_white" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/redtop + name = "Top (Red)" + icon_state = "redtop" + gender = FEMALE + +/datum/sprite_accessory/clothing/undershirt/whitetop + name = "Top (White)" + icon_state = "whitetop" + gender = FEMALE + +/datum/sprite_accessory/clothing/undershirt/tshirt_blue + name = "T-Shirt (Blue)" + icon_state = "blueshirt" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tshirt_green + name = "T-Shirt (Green)" + icon_state = "greenshirt" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/tshirt_red + name = "T-Shirt (Red)" + icon_state = "redshirt" + gender = NEUTER + +/datum/sprite_accessory/clothing/undershirt/yellowshirt + name = "T-Shirt (Yellow)" + icon_state = "yellowshirt" + gender = NEUTER + +/////////////////////// +// Socks Definitions // +/////////////////////// + +/datum/sprite_accessory/clothing/socks + icon = 'icons/mob/clothing/underwear.dmi' + em_block = TRUE + abstract_type = /datum/sprite_accessory/clothing/socks + +/datum/sprite_accessory/clothing/socks/nude + name = "Nude" + icon_state = null + +/datum/sprite_accessory/clothing/socks/nude/make_appearance(mob/living/carbon/human/for_who) + return + +// please make sure they're sorted alphabetically and categorized + +/datum/sprite_accessory/clothing/socks/ace_knee + name = "Knee-high (Ace)" + icon_state = "ace_knee" + +/datum/sprite_accessory/clothing/socks/bee_knee + name = "Knee-high (Bee)" + icon_state = "bee_knee" + +/datum/sprite_accessory/clothing/socks/black_knee + name = "Knee-high (Black)" + icon_state = "black_knee" + +/datum/sprite_accessory/clothing/socks/commie_knee + name = "Knee-High (Commie)" + icon_state = "commie_knee" + +/datum/sprite_accessory/clothing/socks/usa_knee + name = "Knee-High (Freedom)" + icon_state = "assblastusa_knee" + +/datum/sprite_accessory/clothing/socks/rainbow_knee + name = "Knee-high (Rainbow)" + icon_state = "rainbow_knee" + +/datum/sprite_accessory/clothing/socks/striped_knee + name = "Knee-high (Striped)" + icon_state = "striped_knee" + +/datum/sprite_accessory/clothing/socks/thin_knee + name = "Knee-high (Thin)" + icon_state = "thin_knee" + +/datum/sprite_accessory/clothing/socks/trans_knee + name = "Knee-high (Trans)" + icon_state = "trans_knee" + +/datum/sprite_accessory/clothing/socks/uk_knee + name = "Knee-High (UK)" + icon_state = "uk_knee" + +/datum/sprite_accessory/clothing/socks/white_knee + name = "Knee-high (White)" + icon_state = "white_knee" + +/datum/sprite_accessory/clothing/socks/fishnet_knee + name = "Knee-high (Fishnet)" + icon_state = "fishnet_knee" + +/datum/sprite_accessory/clothing/socks/black_norm + name = "Normal (Black)" + icon_state = "black_norm" + +/datum/sprite_accessory/clothing/socks/white_norm + name = "Normal (White)" + icon_state = "white_norm" + +/datum/sprite_accessory/clothing/socks/pantyhose + name = "Pantyhose" + icon_state = "pantyhose" + +/datum/sprite_accessory/clothing/socks/black_short + name = "Short (Black)" + icon_state = "black_short" + +/datum/sprite_accessory/clothing/socks/white_short + name = "Short (White)" + icon_state = "white_short" + +/datum/sprite_accessory/clothing/socks/stockings_blue + name = "Stockings (Blue)" + icon_state = "stockings_blue" + +/datum/sprite_accessory/clothing/socks/stockings_cyan + name = "Stockings (Cyan)" + icon_state = "stockings_cyan" + +/datum/sprite_accessory/clothing/socks/stockings_dpink + name = "Stockings (Dark Pink)" + icon_state = "stockings_dpink" + +/datum/sprite_accessory/clothing/socks/stockings_green + name = "Stockings (Green)" + icon_state = "stockings_green" + +/datum/sprite_accessory/clothing/socks/stockings_orange + name = "Stockings (Orange)" + icon_state = "stockings_orange" + +/datum/sprite_accessory/clothing/socks/stockings_programmer + name = "Stockings (Programmer)" + icon_state = "stockings_lpink" + +/datum/sprite_accessory/clothing/socks/stockings_purple + name = "Stockings (Purple)" + icon_state = "stockings_purple" + +/datum/sprite_accessory/clothing/socks/stockings_yellow + name = "Stockings (Yellow)" + icon_state = "stockings_yellow" + +/datum/sprite_accessory/clothing/socks/stockings_fishnet + name = "Stockings (Fishnet)" + icon_state = "fishnet_full" + +/datum/sprite_accessory/clothing/socks/ace_thigh + name = "Thigh-high (Ace)" + icon_state = "ace_thigh" + +/datum/sprite_accessory/clothing/socks/bee_thigh + name = "Thigh-high (Bee)" + icon_state = "bee_thigh" + +/datum/sprite_accessory/clothing/socks/black_thigh + name = "Thigh-high (Black)" + icon_state = "black_thigh" + +/datum/sprite_accessory/clothing/socks/commie_thigh + name = "Thigh-high (Commie)" + icon_state = "commie_thigh" + +/datum/sprite_accessory/clothing/socks/usa_thigh + name = "Thigh-high (Freedom)" + icon_state = "assblastusa_thigh" + +/datum/sprite_accessory/clothing/socks/rainbow_thigh + name = "Thigh-high (Rainbow)" + icon_state = "rainbow_thigh" + +/datum/sprite_accessory/clothing/socks/striped_thigh + name = "Thigh-high (Striped)" + icon_state = "striped_thigh" + +/datum/sprite_accessory/clothing/socks/thin_thigh + name = "Thigh-high (Thin)" + icon_state = "thin_thigh" + +/datum/sprite_accessory/clothing/socks/trans_thigh + name = "Thigh-high (Trans)" + icon_state = "trans_thigh" + +/datum/sprite_accessory/clothing/socks/uk_thigh + name = "Thigh-high (UK)" + icon_state = "uk_thigh" + +/datum/sprite_accessory/clothing/socks/white_thigh + name = "Thigh-high (White)" + icon_state = "white_thigh" + +/datum/sprite_accessory/clothing/socks/fishnet_thigh + name = "Thigh-high (Fishnet)" + icon_state = "fishnet_thigh" + +/datum/sprite_accessory/clothing/socks/thocks + name = "Thocks" + icon_state = "thocks" diff --git a/code/datums/sprite_accessories/ears.dm b/code/datums/sprite_accessories/ears.dm new file mode 100644 index 000000000000..bc770a2ec884 --- /dev/null +++ b/code/datums/sprite_accessories/ears.dm @@ -0,0 +1,40 @@ +/datum/sprite_accessory/ears + icon = 'icons/mob/human/cat_features.dmi' + em_block = TRUE + +/datum/sprite_accessory/ears/cat + name = "Cat" + icon_state = "cat" + color_src = HAIR_COLOR + +/datum/sprite_accessory/ears/cat/big + name = "Big" + icon_state = "big" + +/datum/sprite_accessory/ears/cat/miqo + name = "Coeurl" + icon_state = "miqo" + +/datum/sprite_accessory/ears/cat/fold + name = "Fold" + icon_state = "fold" + +/datum/sprite_accessory/ears/cat/lynx + name = "Lynx" + icon_state = "lynx" + +/datum/sprite_accessory/ears/cat/round + name = "Round" + icon_state = "round" + +/datum/sprite_accessory/ears/cat/cybernetic + name = "Cybernetic" + icon_state = "cyber" + locked = TRUE + +/datum/sprite_accessory/ears/fox + icon = 'icons/mob/human/fox_features.dmi' + name = "Fox" + icon_state = "fox" + color_src = HAIR_COLOR + locked = TRUE diff --git a/code/datums/sprite_accessories/facial_hair.dm b/code/datums/sprite_accessories/facial_hair.dm new file mode 100644 index 000000000000..1fd30435b4da --- /dev/null +++ b/code/datums/sprite_accessories/facial_hair.dm @@ -0,0 +1,161 @@ +/datum/sprite_accessory/facial_hair + icon = 'icons/mob/human/human_face.dmi' + gender = MALE // barf (unless you're a dorf, dorfs dig chix w/ beards :P) + em_block = TRUE + +// please make sure they're sorted alphabetically and categorized + +/datum/sprite_accessory/facial_hair/abe + name = "Beard (Abraham Lincoln)" + icon_state = "facial_abe" + +/datum/sprite_accessory/facial_hair/brokenman + name = "Beard (Broken Man)" + icon_state = "facial_brokenman" + natural_spawn = FALSE + +/datum/sprite_accessory/facial_hair/chinstrap + name = "Beard (Chinstrap)" + icon_state = "facial_chin" + +/datum/sprite_accessory/facial_hair/dwarf + name = "Beard (Dwarf)" + icon_state = "facial_dwarf" + +/datum/sprite_accessory/facial_hair/fullbeard + name = "Beard (Full)" + icon_state = "facial_fullbeard" + +/datum/sprite_accessory/facial_hair/croppedfullbeard + name = "Beard (Cropped Fullbeard)" + icon_state = "facial_croppedfullbeard" + +/datum/sprite_accessory/facial_hair/gt + name = "Beard (Goatee)" + icon_state = "facial_gt" + +/datum/sprite_accessory/facial_hair/hip + name = "Beard (Hipster)" + icon_state = "facial_hip" + +/datum/sprite_accessory/facial_hair/jensen + name = "Beard (Jensen)" + icon_state = "facial_jensen" + +/datum/sprite_accessory/facial_hair/neckbeard + name = "Beard (Neckbeard)" + icon_state = "facial_neckbeard" + +/datum/sprite_accessory/facial_hair/vlongbeard + name = "Beard (Very Long)" + icon_state = "facial_wise" + +/datum/sprite_accessory/facial_hair/muttonmus + name = "Beard (Muttonmus)" + icon_state = "facial_muttonmus" + +/datum/sprite_accessory/facial_hair/martialartist + name = "Beard (Martial Artist)" + icon_state = "facial_martialartist" + natural_spawn = FALSE + +/datum/sprite_accessory/facial_hair/chinlessbeard + name = "Beard (Chinless Beard)" + icon_state = "facial_chinlessbeard" + +/datum/sprite_accessory/facial_hair/moonshiner + name = "Beard (Moonshiner)" + icon_state = "facial_moonshiner" + +/datum/sprite_accessory/facial_hair/longbeard + name = "Beard (Long)" + icon_state = "facial_longbeard" + +/datum/sprite_accessory/facial_hair/volaju + name = "Beard (Volaju)" + icon_state = "facial_volaju" + +/datum/sprite_accessory/facial_hair/threeoclock + name = "Beard (Three o Clock Shadow)" + icon_state = "facial_3oclock" + +/datum/sprite_accessory/facial_hair/fiveoclock + name = "Beard (Five o Clock Shadow)" + icon_state = "facial_fiveoclock" + +/datum/sprite_accessory/facial_hair/fiveoclockm + name = "Beard (Five o Clock Moustache)" + icon_state = "facial_5oclockmoustache" + +/datum/sprite_accessory/facial_hair/sevenoclock + name = "Beard (Seven o Clock Shadow)" + icon_state = "facial_7oclock" + +/datum/sprite_accessory/facial_hair/sevenoclockm + name = "Beard (Seven o Clock Moustache)" + icon_state = "facial_7oclockmoustache" + +/datum/sprite_accessory/facial_hair/moustache + name = "Moustache" + icon_state = "facial_moustache" + +/datum/sprite_accessory/facial_hair/pencilstache + name = "Moustache (Pencilstache)" + icon_state = "facial_pencilstache" + +/datum/sprite_accessory/facial_hair/smallstache + name = "Moustache (Smallstache)" + icon_state = "facial_smallstache" + +/datum/sprite_accessory/facial_hair/walrus + name = "Moustache (Walrus)" + icon_state = "facial_walrus" + +/datum/sprite_accessory/facial_hair/fu + name = "Moustache (Fu Manchu)" + icon_state = "facial_fumanchu" + +/datum/sprite_accessory/facial_hair/hogan + name = "Moustache (Hulk Hogan)" + icon_state = "facial_hogan" //-Neek + +/datum/sprite_accessory/facial_hair/selleck + name = "Moustache (Selleck)" + icon_state = "facial_selleck" + +/datum/sprite_accessory/facial_hair/chaplin + name = "Moustache (Square)" + icon_state = "facial_chaplin" + +/datum/sprite_accessory/facial_hair/vandyke + name = "Moustache (Van Dyke)" + icon_state = "facial_vandyke" + +/datum/sprite_accessory/facial_hair/watson + name = "Moustache (Watson)" + icon_state = "facial_watson" + +/datum/sprite_accessory/facial_hair/handlebar + name = "Moustache (Handlebar)" + icon_state = "facial_handlebar" + +/datum/sprite_accessory/facial_hair/handlebar2 + name = "Moustache (Handlebar 2)" + icon_state = "facial_handlebar2" + +/datum/sprite_accessory/facial_hair/elvis + name = "Sideburns (Elvis)" + icon_state = "facial_elvis" + +/datum/sprite_accessory/facial_hair/mutton + name = "Sideburns (Mutton Chops)" + icon_state = "facial_mutton" + +/datum/sprite_accessory/facial_hair/sideburn + name = "Sideburns" + icon_state = "facial_sideburn" + +/datum/sprite_accessory/facial_hair/shaved + name = "Shaved" + icon_state = SPRITE_ACCESSORY_NONE + gender = NEUTER diff --git a/code/datums/sprite_accessories/frills.dm b/code/datums/sprite_accessories/frills.dm new file mode 100644 index 000000000000..b470da027899 --- /dev/null +++ b/code/datums/sprite_accessories/frills.dm @@ -0,0 +1,14 @@ +/datum/sprite_accessory/frills + icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' + +/datum/sprite_accessory/frills/simple + name = "Simple" + icon_state = "simple" + +/datum/sprite_accessory/frills/short + name = "Short" + icon_state = "short" + +/datum/sprite_accessory/frills/aquatic + name = "Aquatic" + icon_state = "aqua" diff --git a/code/datums/sprite_accessories/hair.dm b/code/datums/sprite_accessories/hair.dm new file mode 100644 index 000000000000..7a2dbf475d77 --- /dev/null +++ b/code/datums/sprite_accessories/hair.dm @@ -0,0 +1,1022 @@ +/datum/hair_mask + var/icon/icon = 'icons/mob/human/hair_masks.dmi' + var/icon_state = "" + /// Strict coverage zones will always have the hair mask applied to them, even if a piece of hair at that location would normally resist being masked. + /// If a piece of headware only covers the top of the head, it should only strictly cover the top zone. But a mostly-enclosed helmet might strictly cover almost all zones. + var/strict_coverage_zones = NONE + +/datum/hair_mask/standard_hat_middle + icon_state = "hide_above_45deg" + strict_coverage_zones = HAIR_APPENDAGE_TOP + +/datum/hair_mask/standard_hat_low + icon_state = "hide_above_45deg_low" + strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR + +/datum/hair_mask/winterhood + icon_state = "hide_winterhood" + strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR + +/datum/hair_mask/hoodie + icon_state = "hide_hoodie" + strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR + +// Cache of each hairstyle's icon after being blended with the given masks +// "joined mask types" is each mask's type as a string joined by commas (for no masks, it is the empty string) +// /datum/sprite_accessory/hair path -> list(joined mask types -> icon) +GLOBAL_LIST_EMPTY(blended_hair_icons_cache) + +/datum/sprite_accessory/hair + icon = 'icons/mob/human/human_face.dmi' // default icon for all hairs + var/y_offset = 0 // Y offset to apply so we can have hair that reaches above the player sprite's visual bounding box + + // Some hair will have "appendages", such as pony tails, that stick out from certain parts of the head. These can be layered above or below headwear and resist being masked away by hair masks. + // Lists should be icon_state strings associated with the HAIR_APPENDAGE defines specifying the part of the head they stick out from. + // hair_appendages_inner contains icon_states that go in the normal hair layer, hair_appendages_outer contains icon_states that go above the layer for headwear. + // hair_appendages_inner will be masked normally if their HAIR_APPENDAGE zone is strictly masked by a piece of clothing (a fully enclosed helmet with a transparent visor will strictly mask all zones, a small hat will only strictly mask the top, etc.). + // hair_appendages_outer will never be masked at all and will just not be shown if their zone has strict masking. These should generally not have visible sprites for every dir. + var/list/hair_appendages_inner = null + var/list/hair_appendages_outer = null + +/// Retrieve the base hair icon with all hair appendeges blended in, with hair masks applied, from the cache, or generate it if it doesn't exist +/datum/sprite_accessory/hair/proc/getCachedIcon(list/hair_masks) + var/icon/cachedIcon + var/joinedMasks = LAZYLEN(hair_masks) ? jointext(hair_masks, ",") : "" + var/list/masks_to_icons = GLOB.blended_hair_icons_cache[type] + if(!masks_to_icons) + GLOB.blended_hair_icons_cache[type] = list() + else + cachedIcon = masks_to_icons[joinedMasks] + + if(!cachedIcon) + if(LAZYLEN(hair_masks)) + if(LAZYLEN(hair_appendages_inner)) + // Check if there are any hair appendages in a zone that is not strictly masked + var/found_mask_dodger = FALSE + for(var/datum/hair_mask/mask as anything in hair_masks) + for(var/appendage in hair_appendages_inner) + var/zone = hair_appendages_inner[appendage] + if(!(zone & mask.strict_coverage_zones)) + found_mask_dodger = TRUE + + if(found_mask_dodger) + // We have to process each icon individually + cachedIcon = icon(icon, icon_state) + // mask the base icon + for(var/datum/hair_mask/mask as anything in hair_masks) + var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) + mask_icon.Shift(SOUTH, y_offset) + cachedIcon.Blend(mask_icon, ICON_ADD) + + // mask the appendages if required and add them to the base icon + for(var/appendage_icon_state in hair_appendages_inner) + var/icon/appendage_icon = icon(icon, appendage_icon_state) + var/zone = hair_appendages_inner[appendage_icon_state] + for(var/datum/hair_mask/mask as anything in hair_masks) + if(zone & mask.strict_coverage_zones) + var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) + mask_icon.Shift(SOUTH, y_offset) + appendage_icon.Blend(mask_icon, ICON_ADD) + cachedIcon.Blend(appendage_icon, ICON_OVERLAY) + else + // No mask dodgers, so we can just mask the full (hopefully cached) icon + cachedIcon = icon(getCachedIcon()) + for(var/datum/hair_mask/mask as anything in hair_masks) + var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) + mask_icon.Shift(SOUTH, y_offset) + cachedIcon.Blend(mask_icon, ICON_ADD) + else + // No hair appendages, so just apply all hair masks to the base icon + cachedIcon = icon(icon, icon_state) + for(var/datum/hair_mask/mask as anything in hair_masks) + var/icon/mask_icon = icon('icons/mob/human/hair_masks.dmi', mask.icon_state) + mask_icon.Shift(SOUTH, y_offset) + cachedIcon.Blend(mask_icon, ICON_ADD) + else + // no hair masks + cachedIcon = icon(icon, icon_state) + if(LAZYLEN(hair_appendages_inner)) + for(var/appendage_icon_state in hair_appendages_inner) + var/icon/appendage_icon = icon(icon, appendage_icon_state) + cachedIcon.Blend(appendage_icon, ICON_OVERLAY) + // set cache + GLOB.blended_hair_icons_cache[type][joinedMasks] = cachedIcon + return cachedIcon + + +// please make sure they're sorted alphabetically and, where needed, categorized +// try to capitalize the names please~ +// try to spell +// you do not need to define _s or _l sub-states, game automatically does this for you + +/datum/sprite_accessory/hair/afro + name = "Afro" + icon_state = "hair_afro" + +/datum/sprite_accessory/hair/afro2 + name = "Afro 2" + icon_state = "hair_afro2" + +/datum/sprite_accessory/hair/afro_large + name = "Afro (Large)" + icon_state = "hair_bigafro" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/afro_huge + name = "Afro (Huge)" + icon_state = "hair_hugeafro" + y_offset = 6 + natural_spawn = FALSE + +/datum/sprite_accessory/hair/allthefuzz + name = "All The Fuzz" + icon_state = "hair_allthefuzz" + +/datum/sprite_accessory/hair/antenna + name = "Ahoge" + icon_state = "hair_antenna" + hair_appendages_inner = list("hair_antenna_a1" = HAIR_APPENDAGE_TOP) + +/datum/sprite_accessory/hair/bald + name = "Bald" + icon_state = null + +/datum/sprite_accessory/hair/balding + name = "Balding Hair" + icon_state = "hair_e" + +/datum/sprite_accessory/hair/bedhead + name = "Bedhead" + icon_state = "hair_bedhead" + +/datum/sprite_accessory/hair/bedhead2 + name = "Bedhead 2" + icon_state = "hair_bedheadv2" + +/datum/sprite_accessory/hair/bedhead3 + name = "Bedhead 3" + icon_state = "hair_bedheadv3" + +/datum/sprite_accessory/hair/bedheadv4 + name = "Bedhead 4x" + icon_state = "hair_bedheadv4" + +/datum/sprite_accessory/hair/bedheadlong + name = "Long Bedhead" + icon_state = "hair_long_bedhead" + +/datum/sprite_accessory/hair/bedheadfloorlength + name = "Floorlength Bedhead" + icon_state = "hair_floorlength_bedhead" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/badlycut + name = "Shorter Long Bedhead" + icon_state = "hair_verybadlycut" + +/datum/sprite_accessory/hair/beehive + name = "Beehive" + icon_state = "hair_beehive" + +/datum/sprite_accessory/hair/beehive2 + name = "Beehive 2" + icon_state = "hair_beehivev2" + +/datum/sprite_accessory/hair/bob + name = "Bob Hair" + icon_state = "hair_bob" + +/datum/sprite_accessory/hair/bob2 + name = "Bob Hair 2" + icon_state = "hair_bob2" + +/datum/sprite_accessory/hair/bob3 + name = "Bob Hair 3" + icon_state = "hair_bobcut" + +/datum/sprite_accessory/hair/bob4 + name = "Bob Hair 4" + icon_state = "hair_bob4" + +/datum/sprite_accessory/hair/bobcurl + name = "Bobcurl" + icon_state = "hair_bobcurl" + +/datum/sprite_accessory/hair/boddicker + name = "Boddicker" + icon_state = "hair_boddicker" + +/datum/sprite_accessory/hair/bowlcut + name = "Bowlcut" + icon_state = "hair_bowlcut" + +/datum/sprite_accessory/hair/bowlcut2 + name = "Bowlcut 2" + icon_state = "hair_bowlcut2" + +/datum/sprite_accessory/hair/braid + name = "Braid (Floorlength)" + icon_state = "hair_braid" + hair_appendages_inner = list("hair_braid_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_braid_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/braided + name = "Braided" + icon_state = "hair_braided" + +/datum/sprite_accessory/hair/front_braid + name = "Braided Front" + icon_state = "hair_braidfront" + hair_appendages_inner = list("hair_braidfront_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_braidfront_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/not_floorlength_braid + name = "Braid (High)" + icon_state = "hair_braid2" + hair_appendages_inner = list("hair_braid2_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_braid2_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/lowbraid + name = "Braid (Low)" + icon_state = "hair_hbraid" + +/datum/sprite_accessory/hair/shortbraid + name = "Braid (Short)" + icon_state = "hair_shortbraid" + hair_appendages_inner = list("hair_shortbraid_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_shortbraid_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/braidtail + name = "Braided Tail" + icon_state = "hair_braidtail" + hair_appendages_inner = list("hair_braidtail_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_braidtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/bun + name = "Bun Head" + icon_state = "hair_bun" + +/datum/sprite_accessory/hair/bun2 + name = "Bun Head 2" + icon_state = "hair_bunhead2" + hair_appendages_inner = list("hair_bunhead2_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_bunhead2_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/bun3 + name = "Bun Head 3" + icon_state = "hair_bun3" + +/datum/sprite_accessory/hair/largebun + name = "Bun (Large)" + icon_state = "hair_largebun" + +/datum/sprite_accessory/hair/manbun + name = "Bun (Manbun)" + icon_state = "hair_manbun" + hair_appendages_inner = list("hair_manbun_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_manbun_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/tightbun + name = "Bun (Tight)" + icon_state = "hair_tightbun" + +/datum/sprite_accessory/hair/business + name = "Business Hair" + icon_state = "hair_business" + +/datum/sprite_accessory/hair/business2 + name = "Business Hair 2" + icon_state = "hair_business2" + +/datum/sprite_accessory/hair/business3 + name = "Business Hair 3" + icon_state = "hair_business3" + +/datum/sprite_accessory/hair/business4 + name = "Business Hair 4" + icon_state = "hair_business4" + +/datum/sprite_accessory/hair/buzz + name = "Buzzcut" + icon_state = "hair_buzzcut" + +/datum/sprite_accessory/hair/chinbob + name = "Chin-Length Bob Cut" + icon_state = "hair_chinbob" + +/datum/sprite_accessory/hair/comet + name = "Comet" + icon_state = "hair_comet" + +/datum/sprite_accessory/hair/cia + name = "CIA" + icon_state = "hair_cia" + +/datum/sprite_accessory/hair/coffeehouse + name = "Coffee House" + icon_state = "hair_coffeehouse" + +/datum/sprite_accessory/hair/combover + name = "Combover" + icon_state = "hair_combover" + +/datum/sprite_accessory/hair/cornrows1 + name = "Cornrows" + icon_state = "hair_cornrows" + +/datum/sprite_accessory/hair/cornrows2 + name = "Cornrows 2" + icon_state = "hair_cornrows2" + +/datum/sprite_accessory/hair/cornrowbun + name = "Cornrow Bun" + icon_state = "hair_cornrowbun" + +/datum/sprite_accessory/hair/cornrowbraid + name = "Cornrow Braid" + icon_state = "hair_cornrowbraid" + +/datum/sprite_accessory/hair/cornrowdualtail + name = "Cornrow Tail" + icon_state = "hair_cornrowtail" + hair_appendages_inner = list("hair_cornrowtail_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_cornrowtail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/crew + name = "Crewcut" + icon_state = "hair_crewcut" + +/datum/sprite_accessory/hair/curls + name = "Curls" + icon_state = "hair_curls" + +/datum/sprite_accessory/hair/cut + name = "Cut Hair" + icon_state = "hair_c" + +/datum/sprite_accessory/hair/dandpompadour + name = "Dandy Pompadour" + icon_state = "hair_dandypompadour" + +/datum/sprite_accessory/hair/devillock + name = "Devil Lock" + icon_state = "hair_devilock" + +/datum/sprite_accessory/hair/doublebun + name = "Double Bun" + icon_state = "hair_doublebun" + hair_appendages_inner = list("hair_doublebun_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_doublebun_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/dreadlocks + name = "Dreadlocks" + icon_state = "hair_dreads" + +/datum/sprite_accessory/hair/drillhair + name = "Drillruru" + icon_state = "hair_drillruru" + hair_appendages_inner = list("hair_drillruru_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_drillruru_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/drillhairextended + name = "Drill Hair (Extended)" + icon_state = "hair_drillhairextended" + hair_appendages_inner = list("hair_drillhairextended_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_drillhairextended_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/emo + name = "Emo" + icon_state = "hair_emo" + +/datum/sprite_accessory/hair/emofrine + name = "Emo Fringe" + icon_state = "hair_emofringe" + +/datum/sprite_accessory/hair/nofade + name = "Fade (None)" + icon_state = "hair_nofade" + +/datum/sprite_accessory/hair/highfade + name = "Fade (High)" + icon_state = "hair_highfade" + +/datum/sprite_accessory/hair/medfade + name = "Fade (Medium)" + icon_state = "hair_medfade" + +/datum/sprite_accessory/hair/lowfade + name = "Fade (Low)" + icon_state = "hair_lowfade" + +/datum/sprite_accessory/hair/baldfade + name = "Fade (Bald)" + icon_state = "hair_baldfade" + +/datum/sprite_accessory/hair/feather + name = "Feather" + icon_state = "hair_feather" + +/datum/sprite_accessory/hair/father + name = "Father" + icon_state = "hair_father" + +/datum/sprite_accessory/hair/sargeant + name = "Flat Top" + icon_state = "hair_sargeant" + +/datum/sprite_accessory/hair/flair + name = "Flair" + icon_state = "hair_flair" + +/datum/sprite_accessory/hair/bigflattop + name = "Flat Top (Big)" + icon_state = "hair_bigflattop" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/flow_hair + name = "Flow Hair" + icon_state = "hair_f" + +/datum/sprite_accessory/hair/gelled + name = "Gelled Back" + icon_state = "hair_gelled" + +/datum/sprite_accessory/hair/gentle + name = "Gentle" + icon_state = "hair_gentle" + +/datum/sprite_accessory/hair/halfbang + name = "Half-banged Hair" + icon_state = "hair_halfbang" + +/datum/sprite_accessory/hair/halfbang2 + name = "Half-banged Hair 2" + icon_state = "hair_halfbang2" + +/datum/sprite_accessory/hair/halfshaved + name = "Half-shaved" + icon_state = "hair_halfshaved" + +/datum/sprite_accessory/hair/hedgehog + name = "Hedgehog Hair" + icon_state = "hair_hedgehog" + +/datum/sprite_accessory/hair/himecut + name = "Hime Cut" + icon_state = "hair_himecut" + +/datum/sprite_accessory/hair/himecut2 + name = "Hime Cut 2" + icon_state = "hair_himecut2" + +/datum/sprite_accessory/hair/shorthime + name = "Hime Cut (Short)" + icon_state = "hair_shorthime" + +/datum/sprite_accessory/hair/himeup + name = "Hime Updo" + icon_state = "hair_himeup" + +/datum/sprite_accessory/hair/hitop + name = "Hitop" + icon_state = "hair_hitop" + +/datum/sprite_accessory/hair/jade + name = "Jade" + icon_state = "hair_jade" + +/datum/sprite_accessory/hair/jensen + name = "Jensen Hair" + icon_state = "hair_jensen" + +/datum/sprite_accessory/hair/joestar + name = "Joestar" + icon_state = "hair_joestar" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/keanu + name = "Keanu Hair" + icon_state = "hair_keanu" + +/datum/sprite_accessory/hair/kusangi + name = "Kusanagi Hair" + icon_state = "hair_kusanagi" + +/datum/sprite_accessory/hair/long + name = "Long Hair 1" + icon_state = "hair_long" + hair_appendages_inner = list("hair_long_a1" = HAIR_APPENDAGE_HANGING_REAR) + +/datum/sprite_accessory/hair/long2 + name = "Long Hair 2" + icon_state = "hair_long2" + hair_appendages_inner = list("hair_long2_a1" = HAIR_APPENDAGE_HANGING_REAR) + +/datum/sprite_accessory/hair/long3 + name = "Long Hair 3" + icon_state = "hair_long3" + hair_appendages_inner = list("hair_long3_a1" = HAIR_APPENDAGE_HANGING_REAR) + +/datum/sprite_accessory/hair/long_over_eye + name = "Long Over Eye" + icon_state = "hair_longovereye" + +/datum/sprite_accessory/hair/longbangs + name = "Long Bangs" + icon_state = "hair_lbangs" + +/datum/sprite_accessory/hair/longemo + name = "Long Emo" + icon_state = "hair_longemo" + +/datum/sprite_accessory/hair/longfringe + name = "Long Fringe" + icon_state = "hair_longfringe" + +/datum/sprite_accessory/hair/sidepartlongalt + name = "Long Side Part" + icon_state = "hair_longsidepart" + hair_appendages_inner = list("hair_longsidepart_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_longsidepart_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/megaeyebrows + name = "Mega Eyebrows" + icon_state = "hair_megaeyebrows" + +/datum/sprite_accessory/hair/messy + name = "Messy" + icon_state = "hair_messy" + +/datum/sprite_accessory/hair/modern + name = "Modern" + icon_state = "hair_modern" + +/datum/sprite_accessory/hair/mohawk + name = "Mohawk" + icon_state = "hair_d" + natural_spawn = FALSE // sorry little one + +/datum/sprite_accessory/hair/nitori + name = "Nitori" + icon_state = "hair_nitori" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/reversemohawk + name = "Mohawk (Reverse)" + icon_state = "hair_reversemohawk" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/shavedmohawk + name = "Mohawk (Shaved)" + icon_state = "hair_shavedmohawk" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/unshavenmohawk + name = "Mohawk (Unshaven)" + icon_state = "hair_unshaven_mohawk" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/mulder + name = "Mulder" + icon_state = "hair_mulder" + +/datum/sprite_accessory/hair/odango + name = "Odango" + icon_state = "hair_odango" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/ombre + name = "Ombre" + icon_state = "hair_ombre" + +/datum/sprite_accessory/hair/oneshoulder + name = "One Shoulder" + icon_state = "hair_oneshoulder" + +/datum/sprite_accessory/hair/over_eye + name = "Over Eye" + icon_state = "hair_shortovereye" + +/datum/sprite_accessory/hair/hair_overeyetwo + name = "Over Eye 2" + icon_state = "hair_overeyetwo" + +/datum/sprite_accessory/hair/oxton + name = "Oxton" + icon_state = "hair_oxton" + +/datum/sprite_accessory/hair/parted + name = "Parted" + icon_state = "hair_parted" + +/datum/sprite_accessory/hair/partedside + name = "Parted (Side)" + icon_state = "hair_part" + +/datum/sprite_accessory/hair/kagami + name = "Pigtails" + icon_state = "hair_kagami" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/pigtail + name = "Pigtails 2" + icon_state = "hair_pigtails" + natural_spawn = FALSE + +/datum/sprite_accessory/hair/pigtail2 + name = "Pigtails 3" + icon_state = "hair_pigtails2" + natural_spawn = FALSE + hair_appendages_inner = list("hair_pigtails2_a1" = HAIR_APPENDAGE_LEFT, "hair_pigtails2_a2" = HAIR_APPENDAGE_RIGHT) + +/datum/sprite_accessory/hair/pixie + name = "Pixie Cut" + icon_state = "hair_pixie" + +/datum/sprite_accessory/hair/pompadour + name = "Pompadour" + icon_state = "hair_pompadour" + +/datum/sprite_accessory/hair/bigpompadour + name = "Pompadour (Big)" + icon_state = "hair_bigpompadour" + +/datum/sprite_accessory/hair/ponytail1 + name = "Ponytail" + icon_state = "hair_ponytail" + +/datum/sprite_accessory/hair/ponytail2 + name = "Ponytail 2" + icon_state = "hair_ponytail2" + +/datum/sprite_accessory/hair/ponytail3 + name = "Ponytail 3" + icon_state = "hair_ponytail3" + +/datum/sprite_accessory/hair/ponytail4 + name = "Ponytail 4" + icon_state = "hair_ponytail4" + hair_appendages_inner = list("hair_ponytail4_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_ponytail4_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/ponytail5 + name = "Ponytail 5" + icon_state = "hair_ponytail5" + hair_appendages_inner = list("hair_ponytail5_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_ponytail5_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/ponytail6 + name = "Ponytail 6" + icon_state = "hair_ponytail6" + hair_appendages_inner = list("hair_ponytail6_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_ponytail6_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/ponytail7 + name = "Ponytail 7" + icon_state = "hair_ponytail7" + hair_appendages_inner = list("hair_ponytail7_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_ponytail7_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/highponytail + name = "Ponytail (High)" + icon_state = "hair_highponytail" + hair_appendages_inner = list("hair_highponytail_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_highponytail_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/stail + name = "Ponytail (Short)" + icon_state = "hair_stail" + hair_appendages_inner = list("hair_stail_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_stail_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/longponytail + name = "Ponytail (Long)" + icon_state = "hair_longstraightponytail" + hair_appendages_inner = list("hair_longstraightponytail_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_longstraightponytail_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/countryponytail + name = "Ponytail (Country)" + icon_state = "hair_country" + hair_appendages_inner = list("hair_country_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_country_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/fringetail + name = "Ponytail (Fringe)" + icon_state = "hair_fringetail" + +/datum/sprite_accessory/hair/sidetail + name = "Ponytail (Side)" + icon_state = "hair_sidetail" + +/datum/sprite_accessory/hair/sidetail2 + name = "Ponytail (Side) 2" + icon_state = "hair_sidetail2" + +/datum/sprite_accessory/hair/sidetail3 + name = "Ponytail (Side) 3" + icon_state = "hair_sidetail3" + hair_appendages_inner = list("hair_sidetail3_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_sidetail3_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/sidetail4 + name = "Ponytail (Side) 4" + icon_state = "hair_sidetail4" + hair_appendages_inner = list("hair_sidetail4_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_sidetail4_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/spikyponytail + name = "Ponytail (Spiky)" + icon_state = "hair_spikyponytail" + hair_appendages_inner = list("hair_spikyponytail_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_spikyponytail_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/poofy + name = "Poofy" + icon_state = "hair_poofy" + +/datum/sprite_accessory/hair/quiff + name = "Quiff" + icon_state = "hair_quiff" + +/datum/sprite_accessory/hair/ronin + name = "Ronin" + icon_state = "hair_ronin" + +/datum/sprite_accessory/hair/shaved + name = "Shaved" + icon_state = "hair_shaved" + +/datum/sprite_accessory/hair/shavedpart + name = "Shaved Part" + icon_state = "hair_shavedpart" + +/datum/sprite_accessory/hair/shortbangs + name = "Short Bangs" + icon_state = "hair_shortbangs" + +/datum/sprite_accessory/hair/shortbangs2 + name = "Short Bangs 2" + icon_state = "hair_shortbangs2" + +/datum/sprite_accessory/hair/short + name = "Short Hair" + icon_state = "hair_a" + +/datum/sprite_accessory/hair/shorthair2 + name = "Short Hair 2" + icon_state = "hair_shorthair2" + +/datum/sprite_accessory/hair/shorthair3 + name = "Short Hair 3" + icon_state = "hair_shorthair3" + +/datum/sprite_accessory/hair/shorthair4 + name = "Short Hair 4" + icon_state = "hair_d" + +/datum/sprite_accessory/hair/shorthair5 + name = "Short Hair 5" + icon_state = "hair_e" + +/datum/sprite_accessory/hair/shorthair6 + name = "Short Hair 6" + icon_state = "hair_f" + +/datum/sprite_accessory/hair/shorthair7 + name = "Short Hair 7" + icon_state = "hair_shorthairg" + +/datum/sprite_accessory/hair/shorthaireighties + name = "Short Hair 80s" + icon_state = "hair_80s" + +/datum/sprite_accessory/hair/rosa + name = "Short Hair Rosa" + icon_state = "hair_rosa" + +/datum/sprite_accessory/hair/shoulderlength + name = "Shoulder-length Hair" + icon_state = "hair_b" + +/datum/sprite_accessory/hair/sidecut + name = "Sidecut" + icon_state = "hair_sidecut" + +/datum/sprite_accessory/hair/skinhead + name = "Skinhead" + icon_state = "hair_skinhead" + +/datum/sprite_accessory/hair/protagonist + name = "Slightly Long Hair" + icon_state = "hair_protagonist" + +/datum/sprite_accessory/hair/spiky + name = "Spiky" + icon_state = "hair_spikey" + +/datum/sprite_accessory/hair/spiky2 + name = "Spiky 2" + icon_state = "hair_spiky" + +/datum/sprite_accessory/hair/spiky3 + name = "Spiky 3" + icon_state = "hair_spiky2" + +/datum/sprite_accessory/hair/swept + name = "Swept Back Hair" + icon_state = "hair_swept" + +/datum/sprite_accessory/hair/swept2 + name = "Swept Back Hair 2" + icon_state = "hair_swept2" + +/datum/sprite_accessory/hair/thinning + name = "Thinning" + icon_state = "hair_thinning" + +/datum/sprite_accessory/hair/thinningfront + name = "Thinning (Front)" + icon_state = "hair_thinningfront" + +/datum/sprite_accessory/hair/thinningrear + name = "Thinning (Rear)" + icon_state = "hair_thinningrear" + +/datum/sprite_accessory/hair/topknot + name = "Topknot" + icon_state = "hair_topknot" + +/datum/sprite_accessory/hair/tressshoulder + name = "Tress Shoulder" + icon_state = "hair_tressshoulder" + hair_appendages_inner = list("hair_tressshoulder_a1" = HAIR_APPENDAGE_HANGING_FRONT) + hair_appendages_outer = list("hair_tressshoulder_a1o" = HAIR_APPENDAGE_HANGING_FRONT) + +/datum/sprite_accessory/hair/trimmed + name = "Trimmed" + icon_state = "hair_trimmed" + +/datum/sprite_accessory/hair/trimflat + name = "Trim Flat" + icon_state = "hair_trimflat" + +/datum/sprite_accessory/hair/twintails + name = "Twintails" + icon_state = "hair_twintail" + +/datum/sprite_accessory/hair/undercut + name = "Undercut" + icon_state = "hair_undercut" + +/datum/sprite_accessory/hair/undercutleft + name = "Undercut Left" + icon_state = "hair_undercutleft" + +/datum/sprite_accessory/hair/undercutright + name = "Undercut Right" + icon_state = "hair_undercutright" + +/datum/sprite_accessory/hair/unkept + name = "Unkept" + icon_state = "hair_unkept" + +/datum/sprite_accessory/hair/updo + name = "Updo" + icon_state = "hair_updo" + hair_appendages_inner = list("hair_updo_a1" = HAIR_APPENDAGE_TOP) + +/datum/sprite_accessory/hair/longer + name = "Very Long Hair" + icon_state = "hair_vlong" + +/datum/sprite_accessory/hair/longest + name = "Very Long Hair 2" + icon_state = "hair_longest" + +/datum/sprite_accessory/hair/longest2 + name = "Very Long Over Eye" + icon_state = "hair_longest2" + +/datum/sprite_accessory/hair/veryshortovereye + name = "Very Short Over Eye" + icon_state = "hair_veryshortovereyealternate" + +/datum/sprite_accessory/hair/longestalt + name = "Very Long with Fringe" + icon_state = "hair_vlongfringe" + +/datum/sprite_accessory/hair/volaju + name = "Volaju" + icon_state = "hair_volaju" + +/datum/sprite_accessory/hair/wisp + name = "Wisp" + icon_state = "hair_wisp" + hair_appendages_inner = list("hair_wisp_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_wisp_a1o" = HAIR_APPENDAGE_REAR) + +/datum/sprite_accessory/hair/ziegler + name = "Ziegler" + icon_state = "hair_ziegler" + hair_appendages_inner = list("hair_ziegler_a1" = HAIR_APPENDAGE_REAR) + hair_appendages_outer = list("hair_ziegler_a1o" = HAIR_APPENDAGE_REAR) + +/* +///////////////////////////////////// +/ =---------------------------= / +/ == Gradient Hair Definitions == / +/ =---------------------------= / +///////////////////////////////////// +*/ + +/datum/sprite_accessory/gradient + icon = 'icons/mob/human/species/hair_gradients.dmi' + ///whether this gradient applies to hair and/or beards. Some gradients do not work well on beards. + var/gradient_category = GRADIENT_APPLIES_TO_HAIR|GRADIENT_APPLIES_TO_FACIAL_HAIR + +/datum/sprite_accessory/gradient/none + name = SPRITE_ACCESSORY_NONE + icon_state = SPRITE_ACCESSORY_NONE + +/datum/sprite_accessory/gradient/full + name = "Full" + icon_state = "full" + +/datum/sprite_accessory/gradient/fadeup + name = "Fade Up" + icon_state = "fadeup" + +/datum/sprite_accessory/gradient/fadedown + name = "Fade Down" + icon_state = "fadedown" + +/datum/sprite_accessory/gradient/vertical_split + name = "Vertical Split" + icon_state = "vsplit" + +/datum/sprite_accessory/gradient/horizontal_split + name = "Horizontal Split" + icon_state = "bottomflat" + +/datum/sprite_accessory/gradient/reflected + name = "Reflected" + icon_state = "reflected_high" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/reflected/beard + icon_state = "reflected_high_beard" + gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR + +/datum/sprite_accessory/gradient/reflected_inverse + name = "Reflected Inverse" + icon_state = "reflected_inverse_high" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/reflected_inverse/beard + icon_state = "reflected_inverse_high_beard" + gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR + +/datum/sprite_accessory/gradient/wavy + name = "Wavy" + icon_state = "wavy" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/long_fade_up + name = "Long Fade Up" + icon_state = "long_fade_up" + +/datum/sprite_accessory/gradient/long_fade_down + name = "Long Fade Down" + icon_state = "long_fade_down" + +/datum/sprite_accessory/gradient/short_fade_up + name = "Short Fade Up" + icon_state = "short_fade_up" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/short_fade_up/beard + icon_state = "short_fade_down" + gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR + +/datum/sprite_accessory/gradient/short_fade_down + name = "Short Fade Down" + icon_state = "short_fade_down_beard" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/short_fade_down/beard + icon_state = "short_fade_down_beard" + gradient_category = GRADIENT_APPLIES_TO_FACIAL_HAIR + +/datum/sprite_accessory/gradient/wavy_spike + name = "Spiked Wavy" + icon_state = "wavy_spiked" + gradient_category = GRADIENT_APPLIES_TO_HAIR + +/datum/sprite_accessory/gradient/striped + name = "striped" + icon_state = "striped" + +/datum/sprite_accessory/gradient/striped_vertical + name = "Striped Vertical" + icon_state = "striped_vertical" diff --git a/code/datums/sprite_accessories/horns.dm b/code/datums/sprite_accessories/horns.dm new file mode 100644 index 000000000000..dc9331281777 --- /dev/null +++ b/code/datums/sprite_accessories/horns.dm @@ -0,0 +1,23 @@ +/datum/sprite_accessory/horns + icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' + em_block = TRUE + +/datum/sprite_accessory/horns/simple + name = "Simple" + icon_state = "simple" + +/datum/sprite_accessory/horns/short + name = "Short" + icon_state = "short" + +/datum/sprite_accessory/horns/curled + name = "Curled" + icon_state = "curled" + +/datum/sprite_accessory/horns/ram + name = "Ram" + icon_state = "ram" + +/datum/sprite_accessory/horns/angler + name = "Angeler" + icon_state = "angler" diff --git a/code/datums/sprite_accessories/markings.dm b/code/datums/sprite_accessories/markings.dm new file mode 100644 index 000000000000..d8e483a80ec7 --- /dev/null +++ b/code/datums/sprite_accessories/markings.dm @@ -0,0 +1,85 @@ +/datum/sprite_accessory/lizard_markings + icon = 'icons/mob/human/species/lizard/lizard_markings.dmi' + +/datum/sprite_accessory/lizard_markings/dtiger + name = "Dark Tiger Body" + icon_state = "dtiger" + gender_specific = TRUE + +/datum/sprite_accessory/lizard_markings/ltiger + name = "Light Tiger Body" + icon_state = "ltiger" + gender_specific = TRUE + +/datum/sprite_accessory/lizard_markings/lbelly + name = "Light Belly" + icon_state = "lbelly" + gender_specific = TRUE + +/datum/sprite_accessory/moth_markings // the markings that moths can have. finally something other than the boring tan + icon = 'icons/mob/human/species/moth/moth_markings.dmi' + color_src = null + +/datum/sprite_accessory/moth_markings/reddish + name = "Reddish" + icon_state = "reddish" + +/datum/sprite_accessory/moth_markings/royal + name = "Royal" + icon_state = "royal" + +/datum/sprite_accessory/moth_markings/gothic + name = "Gothic" + icon_state = "gothic" + +/datum/sprite_accessory/moth_markings/whitefly + name = "White Fly" + icon_state = "whitefly" + +/datum/sprite_accessory/moth_markings/lovers + name = "Lovers" + icon_state = "lovers" + +/datum/sprite_accessory/moth_markings/burnt_off + name = "Burnt Off" + icon_state = "burnt_off" + +/datum/sprite_accessory/moth_markings/firewatch + name = "Firewatch" + icon_state = "firewatch" + +/datum/sprite_accessory/moth_markings/deathhead + name = "Deathshead" + icon_state = "deathhead" + +/datum/sprite_accessory/moth_markings/poison + name = "Poison" + icon_state = "poison" + +/datum/sprite_accessory/moth_markings/ragged + name = "Ragged" + icon_state = "ragged" + +/datum/sprite_accessory/moth_markings/moonfly + name = "Moon Fly" + icon_state = "moonfly" + +/datum/sprite_accessory/moth_markings/oakworm + name = "Oak Worm" + icon_state = "oakworm" + +/datum/sprite_accessory/moth_markings/jungle + name = "Jungle" + icon_state = "jungle" + +/datum/sprite_accessory/moth_markings/witchwing + name = "Witch Wing" + icon_state = "witchwing" + +/datum/sprite_accessory/moth_markings/lightbearer + name = "Lightbearer" + icon_state = "lightbearer" + +/datum/sprite_accessory/moth_markings/dipped + name = "Dipped" + icon_state = "dipped" diff --git a/code/datums/sprite_accessories/moth_antennae.dm b/code/datums/sprite_accessories/moth_antennae.dm new file mode 100644 index 000000000000..54a8cdc979a0 --- /dev/null +++ b/code/datums/sprite_accessories/moth_antennae.dm @@ -0,0 +1,94 @@ +/datum/sprite_accessory/moth_antennae //Finally splitting the sprite + icon = 'icons/mob/human/species/moth/moth_antennae.dmi' + color_src = null + +/datum/sprite_accessory/moth_antennae/plain + name = "Plain" + icon_state = "plain" + +/datum/sprite_accessory/moth_antennae/reddish + name = "Reddish" + icon_state = "reddish" + +/datum/sprite_accessory/moth_antennae/royal + name = "Royal" + icon_state = "royal" + +/datum/sprite_accessory/moth_antennae/gothic + name = "Gothic" + icon_state = "gothic" + +/datum/sprite_accessory/moth_antennae/whitefly + name = "White Fly" + icon_state = "whitefly" + +/datum/sprite_accessory/moth_antennae/lovers + name = "Lovers" + icon_state = "lovers" + +/datum/sprite_accessory/moth_antennae/burnt_off + name = "Burnt Off" + icon_state = "burnt_off" + +/datum/sprite_accessory/moth_antennae/firewatch + name = "Firewatch" + icon_state = "firewatch" + +/datum/sprite_accessory/moth_antennae/deathhead + name = "Deathshead" + icon_state = "deathhead" + +/datum/sprite_accessory/moth_antennae/poison + name = "Poison" + icon_state = "poison" + +/datum/sprite_accessory/moth_antennae/ragged + name = "Ragged" + icon_state = "ragged" + +/datum/sprite_accessory/moth_antennae/moonfly + name = "Moon Fly" + icon_state = "moonfly" + +/datum/sprite_accessory/moth_antennae/oakworm + name = "Oak Worm" + icon_state = "oakworm" + +/datum/sprite_accessory/moth_antennae/jungle + name = "Jungle" + icon_state = "jungle" + +/datum/sprite_accessory/moth_antennae/witchwing + name = "Witch Wing" + icon_state = "witchwing" + +/datum/sprite_accessory/moth_antennae/regal + name = "Regal" + icon_state = "regal" +/datum/sprite_accessory/moth_antennae/rosy + name = "Rosy" + icon_state = "rosy" + +/datum/sprite_accessory/moth_antennae/feathery + name = "Feathery" + icon_state = "feathery" + +/datum/sprite_accessory/moth_antennae/brown + name = "Brown" + icon_state = "brown" + +/datum/sprite_accessory/moth_antennae/plasmafire + name = "Plasmafire" + icon_state = "plasmafire" + +/datum/sprite_accessory/moth_antennae/moffra + name = "Moffra" + icon_state = "moffra" + +/datum/sprite_accessory/moth_antennae/lightbearer + name = "Lightbearer" + icon_state = "lightbearer" + +/datum/sprite_accessory/moth_antennae/dipped + name = "Dipped" + icon_state = "dipped" diff --git a/code/datums/sprite_accessories/pod_hair.dm b/code/datums/sprite_accessories/pod_hair.dm new file mode 100644 index 000000000000..b1f696098e6f --- /dev/null +++ b/code/datums/sprite_accessories/pod_hair.dm @@ -0,0 +1,43 @@ +/datum/sprite_accessory/pod_hair + icon = 'icons/mob/human/species/podperson_hair.dmi' + em_block = TRUE + +/datum/sprite_accessory/pod_hair/ivy + name = "Ivy" + icon_state = "ivy" + +/datum/sprite_accessory/pod_hair/cabbage + name = "Cabbage" + icon_state = "cabbage" + +/datum/sprite_accessory/pod_hair/spinach + name = "Spinach" + icon_state = "spinach" + +/datum/sprite_accessory/pod_hair/prayer + name = "Prayer" + icon_state = "prayer" + +/datum/sprite_accessory/pod_hair/vine + name = "Vine" + icon_state = "vine" + +/datum/sprite_accessory/pod_hair/shrub + name = "Shrub" + icon_state = "shrub" + +/datum/sprite_accessory/pod_hair/rose + name = "Rose" + icon_state = "rose" + +/datum/sprite_accessory/pod_hair/orchid + name = "Orchid" + icon_state = "orchid" + +/datum/sprite_accessory/pod_hair/fig + name = "Fig" + icon_state = "fig" + +/datum/sprite_accessory/pod_hair/hibiscus + name = "Hibiscus" + icon_state = "hibiscus" diff --git a/code/datums/sprite_accessories/snouts.dm b/code/datums/sprite_accessories/snouts.dm new file mode 100644 index 000000000000..c4add777bbe1 --- /dev/null +++ b/code/datums/sprite_accessories/snouts.dm @@ -0,0 +1,19 @@ +/datum/sprite_accessory/snouts + icon = 'icons/mob/human/species/lizard/lizard_misc.dmi' + em_block = TRUE + +/datum/sprite_accessory/snouts/sharp + name = "Sharp" + icon_state = "sharp" + +/datum/sprite_accessory/snouts/round + name = "Round" + icon_state = "round" + +/datum/sprite_accessory/snouts/sharplight + name = "Sharp + Light" + icon_state = "sharplight" + +/datum/sprite_accessory/snouts/roundlight + name = "Round + Light" + icon_state = "roundlight" diff --git a/code/datums/sprite_accessories/spines.dm b/code/datums/sprite_accessories/spines.dm new file mode 100644 index 000000000000..8812c07dbc45 --- /dev/null +++ b/code/datums/sprite_accessories/spines.dm @@ -0,0 +1,47 @@ +/datum/sprite_accessory/spines + icon = 'icons/mob/human/species/lizard/lizard_spines.dmi' + em_block = TRUE + +/datum/sprite_accessory/tail_spines + icon = 'icons/mob/human/species/lizard/lizard_spines.dmi' + em_block = TRUE + +/datum/sprite_accessory/spines/short + name = "Short" + icon_state = "short" + +/datum/sprite_accessory/tail_spines/short + name = "Short" + icon_state = "short" + +/datum/sprite_accessory/spines/shortmeme + name = "Short + Membrane" + icon_state = "shortmeme" + +/datum/sprite_accessory/tail_spines/shortmeme + name = "Short + Membrane" + icon_state = "shortmeme" + +/datum/sprite_accessory/spines/long + name = "Long" + icon_state = "long" + +/datum/sprite_accessory/tail_spines/long + name = "Long" + icon_state = "long" + +/datum/sprite_accessory/spines/longmeme + name = "Long + Membrane" + icon_state = "longmeme" + +/datum/sprite_accessory/tail_spines/longmeme + name = "Long + Membrane" + icon_state = "longmeme" + +/datum/sprite_accessory/spines/aquatic + name = "Aquatic" + icon_state = "aqua" + +/datum/sprite_accessory/tail_spines/aquatic + name = "Aquatic" + icon_state = "aqua" diff --git a/code/datums/sprite_accessories/tails.dm b/code/datums/sprite_accessories/tails.dm new file mode 100644 index 000000000000..3948108670d4 --- /dev/null +++ b/code/datums/sprite_accessories/tails.dm @@ -0,0 +1,97 @@ +/datum/sprite_accessory/tails + em_block = TRUE + /// Describes which tail spine sprites to use, if any. + var/spine_key = NONE + +///Used for fish-infused tails, which come in different flavors. +/datum/sprite_accessory/tails/fish + icon = 'icons/mob/human/fish_features.dmi' + color_src = TRUE + +/datum/sprite_accessory/tails/fish/simple + name = "Simple" + icon_state = "simple" + +/datum/sprite_accessory/tails/fish/crescent + name = "Crescent" + icon_state = "crescent" + +/datum/sprite_accessory/tails/fish/long + name = "Long" + icon_state = "long" + center = TRUE + dimension_x = 38 + +/datum/sprite_accessory/tails/fish/shark + name = "Shark" + icon_state = "shark" + +/datum/sprite_accessory/tails/fish/chonky + name = "Chonky" + icon_state = "chonky" + center = TRUE + dimension_x = 36 + +/datum/sprite_accessory/tails/lizard + icon = 'icons/mob/human/species/lizard/lizard_tails.dmi' + spine_key = SPINE_KEY_LIZARD + +/datum/sprite_accessory/tails/lizard/none + name = SPRITE_ACCESSORY_NONE + icon_state = "none" + natural_spawn = FALSE + +/datum/sprite_accessory/tails/lizard/smooth + name = "Smooth" + icon_state = "smooth" + +/datum/sprite_accessory/tails/lizard/dtiger + name = "Dark Tiger" + icon_state = "dtiger" + +/datum/sprite_accessory/tails/lizard/ltiger + name = "Light Tiger" + icon_state = "ltiger" + +/datum/sprite_accessory/tails/lizard/spikes + name = "Spikes" + icon_state = "spikes" + +/datum/sprite_accessory/tails/lizard/short + name = "Short" + icon_state = "short" + spine_key = NONE + +/datum/sprite_accessory/tails/felinid/cat + name = "Cat" + icon = 'icons/mob/human/cat_features.dmi' + icon_state = "default" + color_src = HAIR_COLOR + +/datum/sprite_accessory/tails/monkey + +/datum/sprite_accessory/tails/monkey/none + name = SPRITE_ACCESSORY_NONE + icon_state = "none" + natural_spawn = FALSE + +/datum/sprite_accessory/tails/monkey/default + name = "Monkey" + icon = 'icons/mob/human/species/monkey/monkey_tail.dmi' + icon_state = "default" + color_src = FALSE + +/datum/sprite_accessory/tails/xeno + icon_state = "default" + color_src = FALSE + center = TRUE + +/datum/sprite_accessory/tails/xeno/default + name = "Xeno" + icon = 'icons/mob/human/species/alien/tail_xenomorph.dmi' + dimension_x = 40 + +/datum/sprite_accessory/tails/xeno/queen + name = "Xeno Queen" + icon = 'icons/mob/human/species/alien/tail_xenomorph_queen.dmi' + dimension_x = 64 diff --git a/code/datums/sprite_accessories/wings.dm b/code/datums/sprite_accessories/wings.dm new file mode 100644 index 000000000000..083f6f6f5482 --- /dev/null +++ b/code/datums/sprite_accessories/wings.dm @@ -0,0 +1,249 @@ +/datum/sprite_accessory/moth_wings + icon = 'icons/mob/human/species/moth/moth_wings.dmi' + color_src = null + em_block = TRUE + +/datum/sprite_accessory/moth_wings/plain + name = "Plain" + icon_state = "plain" + +/datum/sprite_accessory/moth_wings/monarch + name = "Monarch" + icon_state = "monarch" + +/datum/sprite_accessory/moth_wings/luna + name = "Luna" + icon_state = "luna" + +/datum/sprite_accessory/moth_wings/atlas + name = "Atlas" + icon_state = "atlas" + +/datum/sprite_accessory/moth_wings/reddish + name = "Reddish" + icon_state = "redish" + +/datum/sprite_accessory/moth_wings/royal + name = "Royal" + icon_state = "royal" + +/datum/sprite_accessory/moth_wings/gothic + name = "Gothic" + icon_state = "gothic" + +/datum/sprite_accessory/moth_wings/lovers + name = "Lovers" + icon_state = "lovers" + +/datum/sprite_accessory/moth_wings/whitefly + name = "White Fly" + icon_state = "whitefly" + +/datum/sprite_accessory/moth_wings/burnt_off + name = "Burnt Off" + icon_state = "burnt_off" + locked = TRUE + +/datum/sprite_accessory/moth_wings/firewatch + name = "Firewatch" + icon_state = "firewatch" + +/datum/sprite_accessory/moth_wings/deathhead + name = "Deathshead" + icon_state = "deathhead" + +/datum/sprite_accessory/moth_wings/poison + name = "Poison" + icon_state = "poison" + +/datum/sprite_accessory/moth_wings/ragged + name = "Ragged" + icon_state = "ragged" + +/datum/sprite_accessory/moth_wings/moonfly + name = "Moon Fly" + icon_state = "moonfly" + +/datum/sprite_accessory/moth_wings/snow + name = "Snow" + icon_state = "snow" + +/datum/sprite_accessory/moth_wings/oakworm + name = "Oak Worm" + icon_state = "oakworm" + +/datum/sprite_accessory/moth_wings/jungle + name = "Jungle" + icon_state = "jungle" + +/datum/sprite_accessory/moth_wings/witchwing + name = "Witch Wing" + icon_state = "witchwing" + +/datum/sprite_accessory/moth_wings/rosy + name = "Rosy" + icon_state = "rosy" + +/datum/sprite_accessory/moth_wings/feathery + name = "Feathery" + icon_state = "feathery" + +/datum/sprite_accessory/moth_wings/brown + name = "Brown" + icon_state = "brown" + +/datum/sprite_accessory/moth_wings/plasmafire + name = "Plasmafire" + icon_state = "plasmafire" + +/datum/sprite_accessory/moth_wings/moffra + name = "Moffra" + icon_state = "moffra" + +/datum/sprite_accessory/moth_wings/lightbearer + name = "Lightbearer" + icon_state = "lightbearer" + +/datum/sprite_accessory/moth_wings/dipped + name = "Dipped" + icon_state = "dipped" + +/datum/sprite_accessory/wings + icon = 'icons/mob/human/species/wings.dmi' + em_block = TRUE + +/datum/sprite_accessory/wings_open + icon = 'icons/mob/human/species/wings.dmi' + em_block = TRUE + +/datum/sprite_accessory/wings/angel + name = "Angel" + icon_state = "angel" + color_src = FALSE + dimension_x = 46 + center = TRUE + dimension_y = 34 + locked = TRUE + +/datum/sprite_accessory/wings_open/angel + name = "Angel" + icon_state = "angel" + color_src = FALSE + dimension_x = 46 + center = TRUE + dimension_y = 34 + +/datum/sprite_accessory/wings/dragon + name = "Dragon" + icon_state = "dragon" + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/dragon + name = "Dragon" + icon_state = "dragon" + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/megamoth + name = "Megamoth" + icon_state = "megamoth" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/megamoth + name = "Megamoth" + icon_state = "megamoth" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/mothra + name = "Mothra" + icon_state = "mothra" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/mothra + name = "Mothra" + icon_state = "mothra" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/skeleton + name = "Skeleton" + icon_state = "skele" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/skeleton + name = "Skeleton" + icon_state = "skele" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/robotic + name = "Robotic" + icon_state = "robotic" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/robotic + name = "Robotic" + icon_state = "robotic" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/fly + name = "Fly" + icon_state = "fly" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/fly + name = "Fly" + icon_state = "fly" + color_src = FALSE + dimension_x = 96 + center = TRUE + dimension_y = 32 + +/datum/sprite_accessory/wings/slime + name = "Slime" + icon_state = "slime" + dimension_x = 96 + center = TRUE + dimension_y = 32 + locked = TRUE + +/datum/sprite_accessory/wings_open/slime + name = "Slime" + icon_state = "slime" + dimension_x = 96 + center = TRUE + dimension_y = 32 diff --git a/tgstation.dme b/tgstation.dme index 00cc6b2f93ba..66d0a6651369 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -893,7 +893,6 @@ #include "code\datums\signals.dm" #include "code\datums\sound_token.dm" #include "code\datums\spawners_menu.dm" -#include "code\datums\sprite_accessories.dm" #include "code\datums\station_alert.dm" #include "code\datums\station_integrity.dm" #include "code\datums\stock_market_events.dm" @@ -2048,6 +2047,20 @@ #include "code\datums\skills\fishing.dm" #include "code\datums\skills\gaming.dm" #include "code\datums\skills\mining.dm" +#include "code\datums\sprite_accessories\_sprite_accessory.dm" +#include "code\datums\sprite_accessories\clothing.dm" +#include "code\datums\sprite_accessories\ears.dm" +#include "code\datums\sprite_accessories\facial_hair.dm" +#include "code\datums\sprite_accessories\frills.dm" +#include "code\datums\sprite_accessories\hair.dm" +#include "code\datums\sprite_accessories\horns.dm" +#include "code\datums\sprite_accessories\markings.dm" +#include "code\datums\sprite_accessories\moth_antennae.dm" +#include "code\datums\sprite_accessories\pod_hair.dm" +#include "code\datums\sprite_accessories\snouts.dm" +#include "code\datums\sprite_accessories\spines.dm" +#include "code\datums\sprite_accessories\tails.dm" +#include "code\datums\sprite_accessories\wings.dm" #include "code\datums\station_traits\_station_trait.dm" #include "code\datums\station_traits\admin_panel.dm" #include "code\datums\station_traits\job_traits.dm" From b08ec39e1b6c71b5fa4cb63ab7fbb96e18c044d8 Mon Sep 17 00:00:00 2001 From: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:00:03 -0500 Subject: [PATCH 043/101] Better Unit Tests - Runs All Of Them On A Single Map (exc. map unit tests) and C&D Split-Up (#96368) ## About The Pull Request ### Don't run every single test I've had this on my mind for several months and it being brought up recently reminded me so here we are. Unit tests only run on `runtimestation_minimal.dmm` now with the exception of map tests. The following unit tests are declared as map tests but I didn't see any map dependent logic so :shrug: - `/datum/unit_test/maptest_baseturfs_unmodified_scrape` - `/datum/unit_test/maptest_baseturfs_placed_on_top` - `/datum/unit_test/maptest_baseturfs_placed_on_bottom` - `/datum/unit_test/maptest_get_turf_pixel` - `/datum/unit_test/maptest_load_map_security` - `/datum/unit_test/maptest_modular_map_loader` - `/datum/unit_test/maptest_turf_icons` `/datum/unit_test/subsystem_init` isn't really a mapping unit test but I figured somehow, someway, maps might fuck with subsystem initializations so I just decided to include it. ### Splits up the create & destroy test Idk, pretty simple. Create & destroy is now split up across all integration tests and ran in parallel. ## Why It's Good For The Game oranges promised me "500 nzd" yo ## Changelog No player facing changes --- .github/workflows/run_integration_tests.yml | 4 +- _maps/runtimestation_minimal.json | 6 +- code/datums/map_config.dm | 7 ++ .../view_variables/reference_tracking.dm | 6 +- .../capture_the_flag/ctf_controller.dm | 5 +- code/modules/capture_the_flag/ctf_game.dm | 14 ++-- code/modules/unit_tests/_unit_tests.dm | 12 +++- code/modules/unit_tests/area_contents.dm | 1 + .../modules/unit_tests/atmospherics_sanity.dm | 1 + code/modules/unit_tests/cable_powernets.dm | 1 + .../unit_tests/cargo_dep_order_locations.dm | 1 + code/modules/unit_tests/create_and_destroy.dm | 30 ++++++++- .../unit_tests/dcs_check_list_arguments.dm | 1 + code/modules/unit_tests/firedoor_regions.dm | 1 + code/modules/unit_tests/map_landmarks.dm | 1 + .../unit_tests/mapload_space_verification.dm | 1 + code/modules/unit_tests/mapping.dm | 1 + .../unit_tests/mapping_nearstation_test.dm | 1 + code/modules/unit_tests/orphaned_genturf.dm | 1 + code/modules/unit_tests/required_map_items.dm | 1 + code/modules/unit_tests/subsystem_init.dm | 1 + code/modules/unit_tests/unit_test.dm | 67 +++++++++++++------ config/maps.txt | 5 ++ tools/ci/ci_config_maps.txt | 32 +++++++++ tools/ci/run_server.sh | 8 ++- 25 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 tools/ci/ci_config_maps.txt diff --git a/.github/workflows/run_integration_tests.yml b/.github/workflows/run_integration_tests.yml index a3975538817a..0f87ce2004d4 100644 --- a/.github/workflows/run_integration_tests.yml +++ b/.github/workflows/run_integration_tests.yml @@ -38,7 +38,7 @@ jobs: - name: Download build outputs uses: actions/download-artifact@v8 with: - name: build-artifact-${{ inputs.major || env.BYOND_MAJOR }}-${{ inputs.minor || env.BYOND_MINOR}} + name: build-artifact-${{ inputs.major || env.BYOND_MAJOR }}-${{ inputs.minor || env.BYOND_MINOR }} path: ./ - name: Setup database env: @@ -65,7 +65,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: test_artifacts_${{ inputs.map }}_${{ inputs.major }}_${{ inputs.minor }} + name: test_artifacts_${{ inputs.map }}_${{ inputs.major || env.BYOND_MAJOR }}_${{ inputs.minor || env.BYOND_MINOR }} path: data/screenshots_new/ retention-days: 1 - name: On test fail, write a step summary diff --git a/_maps/runtimestation_minimal.json b/_maps/runtimestation_minimal.json index d638519ef912..3923a1f15a17 100644 --- a/_maps/runtimestation_minimal.json +++ b/_maps/runtimestation_minimal.json @@ -13,7 +13,7 @@ "/datum/unit_test/nuke_cinematic", "/datum/unit_test/atmospherics_sanity", "/datum/unit_test/maptest_mapload_space_verification", - "/datum/unit_test/modify_fantasy_variable", - "/datum/unit_test/create_and_destroy" - ] + "/datum/unit_test/modify_fantasy_variable" + ], + "is_unit_test_map": true } diff --git a/code/datums/map_config.dm b/code/datums/map_config.dm index 021c53b336d4..c3793f141e5c 100644 --- a/code/datums/map_config.dm +++ b/code/datums/map_config.dm @@ -60,8 +60,12 @@ /// Boolean - if TRUE, players spawn with grappling hooks in their bags var/give_players_hooks = FALSE +#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) /// List of unit tests that are skipped when running this map var/list/skipped_tests + /// If TRUE, only unit tests with UNIT_TEST_DEBUG_MAP_ONLY will run on this map + var/is_unit_test_map = FALSE +#endif /// Boolean that tells SSmapping to load all away missions in the codebase. var/load_all_away_missions = FALSE @@ -266,6 +270,9 @@ stack_trace("Invalid path in mapping config for ignored unit tests: \[[path_as_text]\]") continue LAZYADD(skipped_tests, path_real) + + if ("is_unit_test_map" in json) + is_unit_test_map = json["is_unit_test_map"] #endif defaulted = FALSE diff --git a/code/modules/admin/view_variables/reference_tracking.dm b/code/modules/admin/view_variables/reference_tracking.dm index cccb97f6c9bb..d7123e844844 100644 --- a/code/modules/admin/view_variables/reference_tracking.dm +++ b/code/modules/admin/view_variables/reference_tracking.dm @@ -205,7 +205,7 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b) DoSearchVar(element_in_list, "[container_name] -> [element_in_list] (list)", search_time, recursion_count + 1) //Check normal entrys else if(element_in_list == src) - #ifdef REFERENCE_TRACKING_DEBUG +#ifdef REFERENCE_TRACKING_DEBUG if(SSgarbage.should_save_refs) if(!found_refs) found_refs = list() @@ -213,9 +213,9 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b) continue else log_reftracker("Found [type] [text_ref(src)] in list [container_name].") - #else +#else log_reftracker("Found [type] [text_ref(src)] in list [container_name].") - #endif +#endif // This is dumb as hell I'm sorry // I don't want the garbage subsystem to count as a ref for the purposes of this number diff --git a/code/modules/capture_the_flag/ctf_controller.dm b/code/modules/capture_the_flag/ctf_controller.dm index dbc152d6bf46..55becb73c5d8 100644 --- a/code/modules/capture_the_flag/ctf_controller.dm +++ b/code/modules/capture_the_flag/ctf_controller.dm @@ -233,10 +233,7 @@ ///Creates a CTF game with the provided team ID then returns a reference to the new controller. If a controller already exists provides a reference to it. /proc/create_ctf_game(game_id) - if(GLOB.ctf_games[game_id]) - return GLOB.ctf_games[game_id] - var/datum/ctf_controller/CTF = new(game_id) - return CTF + return GLOB.ctf_games[game_id] || new /datum/ctf_controller(game_id) #undef CTF_DEFAULT_RESPAWN #undef CTF_INSTAGIB_RESPAWN diff --git a/code/modules/capture_the_flag/ctf_game.dm b/code/modules/capture_the_flag/ctf_game.dm index 2b164277abaa..e7316c12ec67 100644 --- a/code/modules/capture_the_flag/ctf_game.dm +++ b/code/modules/capture_the_flag/ctf_game.dm @@ -20,9 +20,7 @@ /obj/machinery/ctf/Initialize(mapload) . = ..() - ctf_game = GLOB.ctf_games[game_id] - if(isnull(ctf_game)) - ctf_game = create_ctf_game(game_id) + ctf_game = create_ctf_game(game_id) ///A spawn point for CTF, ghosts can interact with this to vote for CTF or spawn in if a game is running. /obj/machinery/ctf/spawner @@ -447,10 +445,10 @@ /obj/effect/ctf/dead_barricade/Initialize(mapload) . = ..() ctf_game = GLOB.ctf_games[game_id] - ctf_game.barricades += src + ctf_game?.barricades += src /obj/effect/ctf/dead_barricade/Destroy() - ctf_game.barricades -= src + ctf_game?.barricades -= src return ..() /obj/effect/ctf/dead_barricade/proc/respawn() @@ -473,10 +471,8 @@ ///Proc that handles toggling and unloading CTF. /proc/toggle_id_ctf(user, activated_id, automated = FALSE, unload = FALSE, area/ctf_area = /area/centcom/ctf) var/static/loading = CTF_LOADING_UNLOADED - var/datum/ctf_controller/ctf_controller = GLOB.ctf_games[activated_id] - if(isnull(ctf_controller)) - ctf_controller = create_ctf_game(activated_id) - if(unload == TRUE) + var/datum/ctf_controller/ctf_controller = create_ctf_game(activated_id) + if(unload) log_admin("[key_name_admin(user)] is attempting to unload CTF.") message_admins("[key_name_admin(user)] is attempting to unload CTF.") if(loading == CTF_LOADING_UNLOADED) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index d53bd596bfad..ebaff3921fc7 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -39,7 +39,7 @@ /// *Only* run the test provided within the parentheses /// This is useful for debugging when you want to reduce noise, but should never be pushed /// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)` -#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; } +#define TEST_FOCUS(test_path) ##test_path { test_flags = UNIT_TEST_FOCUS; } /// Run the test provided within the parentheses run_count times /// Useful for debugging flaky tests that only fail sometimes @@ -67,6 +67,16 @@ */ #define TEST_AFTER_CREATE_AND_DESTROY INFINITY +// Unit test bitflags + +/// If any unit test has this bitflag, only unit tests with UNIT_TEST_FOCUS will run. +#define UNIT_TEST_FOCUS (1<<0) +/// This unit test only runs on specially designated unit test maps (Should only ever be one). +#define UNIT_TEST_DEBUG_MAP_ONLY (1<<1) + +#define UNIT_TEST_BASIC (UNIT_TEST_DEBUG_MAP_ONLY) +#define UNIT_TEST_MAP_TEST (NONE) + /// Change color to red on ANSI terminal output, if enabled with -DANSICOLORS. #ifdef ANSICOLORS #define TEST_OUTPUT_RED(text) "\x1B\x5B1;31m[text]\x1B\x5B0m" diff --git a/code/modules/unit_tests/area_contents.dm b/code/modules/unit_tests/area_contents.dm index dea3c0a30574..0b5c792561db 100644 --- a/code/modules/unit_tests/area_contents.dm +++ b/code/modules/unit_tests/area_contents.dm @@ -1,6 +1,7 @@ /// Verifies that an area's perception of their "turfs" is correct, and no other area overlaps with them /// Quite slow, but needed /datum/unit_test/maptest_area_contents + test_flags = UNIT_TEST_MAP_TEST priority = TEST_LONGER /datum/unit_test/maptest_area_contents/Run() diff --git a/code/modules/unit_tests/atmospherics_sanity.dm b/code/modules/unit_tests/atmospherics_sanity.dm index d7348a096d37..f3efb4d0a137 100644 --- a/code/modules/unit_tests/atmospherics_sanity.dm +++ b/code/modules/unit_tests/atmospherics_sanity.dm @@ -2,6 +2,7 @@ * This test checks that all areas are connected to their distribution loops */ /datum/unit_test/atmospherics_sanity + test_flags = UNIT_TEST_MAP_TEST priority = TEST_LONGER // we iterate over all atmospherics devices on the starting networks /// List of areas to start crawling from diff --git a/code/modules/unit_tests/cable_powernets.dm b/code/modules/unit_tests/cable_powernets.dm index f462dcf5bc96..24da321f5851 100644 --- a/code/modules/unit_tests/cable_powernets.dm +++ b/code/modules/unit_tests/cable_powernets.dm @@ -1,5 +1,6 @@ ///Checking all powernets to see if they are properly connected and powered. /datum/unit_test/cable_powernets + test_flags = UNIT_TEST_MAP_TEST /datum/unit_test/cable_powernets/Run() for(var/datum/powernet/powernets as anything in SSmachines.powernets) diff --git a/code/modules/unit_tests/cargo_dep_order_locations.dm b/code/modules/unit_tests/cargo_dep_order_locations.dm index 106a0eb19a76..aa262628d2d7 100644 --- a/code/modules/unit_tests/cargo_dep_order_locations.dm +++ b/code/modules/unit_tests/cargo_dep_order_locations.dm @@ -1,4 +1,5 @@ /datum/unit_test/cargo_dep_order_locations + test_flags = UNIT_TEST_MAP_TEST /datum/unit_test/cargo_dep_order_locations/Run() for(var/datum/job_department/department as anything in SSjob.joinable_departments) diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm index 366ebc29c138..76feb7b8b01c 100644 --- a/code/modules/unit_tests/create_and_destroy.dm +++ b/code/modules/unit_tests/create_and_destroy.dm @@ -1,5 +1,7 @@ ///Delete one of every type, sleep a while, then check to see if anything has gone fucky /datum/unit_test/create_and_destroy + // Since this unit test takes so damn long, we split it up across all runners + test_flags = parent_type::test_flags & ~UNIT_TEST_DEBUG_MAP_ONLY //You absolutely must run after (almost) everything else priority = TEST_CREATE_AND_DESTROY @@ -15,6 +17,30 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) GLOB.running_create_and_destroy = TRUE var/list/type_paths_to_check = (valid_typesof(/atom/movable) + valid_typesof(/turf)) - uncreatables // No areas please + + // This code is responsible for splitting up create & destroy across multiple integration tests. + var/total_amount_to_check = length(type_paths_to_check) + var/runner_count = length(config.maplist) + + var/split_up_amount = floor(total_amount_to_check / runner_count) + + var/what_map_index_are_we = 1 + for(var/map_name, _map_config in config.maplist) + var/datum/map_config/map_config = _map_config + if(SSmapping.current_map.map_name == map_config.map_name) + break + what_map_index_are_we++ + + var/start_index = (what_map_index_are_we - 1) * split_up_amount + // Instead of super trying to make it an equal split, we just give the remainder tests to the final runner + var/end_index = (what_map_index_are_we == runner_count) ? total_amount_to_check : start_index + split_up_amount + + // +1 because byond's list.Copy() implementation is weird + type_paths_to_check = type_paths_to_check.Copy(start_index, end_index + 1) + + log_world("Running create and destroy on [length(type_paths_to_check)] atoms out of the [total_amount_to_check] total") + log_world("([start_index] [type_paths_to_check[1]]) - ([end_index] [type_paths_to_check[length(type_paths_to_check)]])") + for(var/type_path in type_paths_to_check) if(ispath(type_path, /turf)) spawn_at.ChangeTurf(type_path) @@ -28,6 +54,8 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) else var/atom/creation = new type_path(spawn_at) if(QDELETED(creation)) + // Same as below + creation = null continue //Go all in qdel(creation, force = TRUE) @@ -50,7 +78,7 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) var/list/queues_we_care_about = list() // All of em, I want hard deletes too, since we rely on the debug info from them - for(var/i in 1 to GC_QUEUE_HARDDELETE) + for(var/i in GC_QUEUE_FILTER to GC_QUEUE_HARDDELETE) queues_we_care_about += i //Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about diff --git a/code/modules/unit_tests/dcs_check_list_arguments.dm b/code/modules/unit_tests/dcs_check_list_arguments.dm index 769574cf95f2..0358ecfc2b90 100644 --- a/code/modules/unit_tests/dcs_check_list_arguments.dm +++ b/code/modules/unit_tests/dcs_check_list_arguments.dm @@ -25,6 +25,7 @@ * This unit test requires every (unless ignored) atom to have been created at least once * for a more accurate search, which is why it's run after create_and_destroy is done running. */ + test_flags = parent_type::test_flags & ~UNIT_TEST_DEBUG_MAP_ONLY priority = TEST_AFTER_CREATE_AND_DESTROY /datum/unit_test/dcs_check_list_arguments/Run() diff --git a/code/modules/unit_tests/firedoor_regions.dm b/code/modules/unit_tests/firedoor_regions.dm index 6b64a4a72608..0e2dbfc32efe 100644 --- a/code/modules/unit_tests/firedoor_regions.dm +++ b/code/modules/unit_tests/firedoor_regions.dm @@ -7,6 +7,7 @@ * Then, checks if every non-ignored region has a fire alarm in it */ /datum/unit_test/firedoor_regions + test_flags = UNIT_TEST_MAP_TEST priority = TEST_LONGER /datum/unit_test/firedoor_regions/Run() diff --git a/code/modules/unit_tests/map_landmarks.dm b/code/modules/unit_tests/map_landmarks.dm index f81b67aa6672..a454ed447fc4 100644 --- a/code/modules/unit_tests/map_landmarks.dm +++ b/code/modules/unit_tests/map_landmarks.dm @@ -1,5 +1,6 @@ /// Tests that [/datum/job/proc/get_default_roundstart_spawn_point] returns a landmark from all joinable jobs. /datum/unit_test/maptest_job_roundstart_spawnpoints + test_flags = UNIT_TEST_MAP_TEST /datum/unit_test/maptest_job_roundstart_spawnpoints/Run() for(var/datum/job/job as anything in SSjob.joinable_occupations) diff --git a/code/modules/unit_tests/mapload_space_verification.dm b/code/modules/unit_tests/mapload_space_verification.dm index c7c9845956aa..c51ac0ba549d 100644 --- a/code/modules/unit_tests/mapload_space_verification.dm +++ b/code/modules/unit_tests/mapload_space_verification.dm @@ -1,6 +1,7 @@ /// Verifies that there are no space turfs inside a station area, or on any planetary z-level. Sometimes, these are introduced during the load of the map and are not present in the DMM itself. /// Let's just make sure that we have a stop-gap measure in place to catch these if they pop up so we don't put it onto production servers should something errant come up. /datum/unit_test/maptest_mapload_space_verification + test_flags = UNIT_TEST_MAP_TEST // This test is quite taxing time-wise, so let's run it later than other faster tests. priority = TEST_LONGER diff --git a/code/modules/unit_tests/mapping.dm b/code/modules/unit_tests/mapping.dm index 6b1509ba64e2..ffdd66e2f678 100644 --- a/code/modules/unit_tests/mapping.dm +++ b/code/modules/unit_tests/mapping.dm @@ -1,5 +1,6 @@ /// Conveys all log_mapping messages as unit test failures, as they all indicate mapping problems. /datum/unit_test/maptest_log_mapping + test_flags = UNIT_TEST_MAP_TEST // Happen before all other tests, to make sure we only capture normal mapping logs. priority = TEST_PRE diff --git a/code/modules/unit_tests/mapping_nearstation_test.dm b/code/modules/unit_tests/mapping_nearstation_test.dm index 39898432fa03..6cdfe9be0783 100644 --- a/code/modules/unit_tests/mapping_nearstation_test.dm +++ b/code/modules/unit_tests/mapping_nearstation_test.dm @@ -1,5 +1,6 @@ ///Detects movables that may have been accidentally placed in space, as well as movables which do not have the proper nearspace area (meaning they aren't lit properly.) /datum/unit_test/maptest_mapping_nearstation_test + test_flags = UNIT_TEST_MAP_TEST priority = TEST_PRE /datum/unit_test/maptest_mapping_nearstation_test/Run() diff --git a/code/modules/unit_tests/orphaned_genturf.dm b/code/modules/unit_tests/orphaned_genturf.dm index 289b883d2def..ce1d97faec3f 100644 --- a/code/modules/unit_tests/orphaned_genturf.dm +++ b/code/modules/unit_tests/orphaned_genturf.dm @@ -1,6 +1,7 @@ /// Ensures we do not leave genturfs sitting around post work /// They serve as notice to the mapper and have no functionality, but it's good to make note of it here /datum/unit_test/orphaned_genturf + test_flags = UNIT_TEST_MAP_TEST /datum/unit_test/orphaned_genturf/Run() for(var/turf/open/genturf/orphaned in ALL_TURFS()) diff --git a/code/modules/unit_tests/required_map_items.dm b/code/modules/unit_tests/required_map_items.dm index a1b1f516d7f6..4d65d7651e18 100644 --- a/code/modules/unit_tests/required_map_items.dm +++ b/code/modules/unit_tests/required_map_items.dm @@ -6,6 +6,7 @@ * - In the type's initialize, REGISTER_REQUIRED_MAP_ITEM() a minimum and maximum */ /datum/unit_test/maptest_required_map_items + test_flags = UNIT_TEST_MAP_TEST /// A list of all typepaths that we expect to be in the required items list var/list/expected_types = list() diff --git a/code/modules/unit_tests/subsystem_init.dm b/code/modules/unit_tests/subsystem_init.dm index f5168079a907..3f49b5069e09 100644 --- a/code/modules/unit_tests/subsystem_init.dm +++ b/code/modules/unit_tests/subsystem_init.dm @@ -1,5 +1,6 @@ /// Tests that all subsystems that need to properly initialize. /datum/unit_test/subsystem_init + test_flags = UNIT_TEST_MAP_TEST /datum/unit_test/subsystem_init/Run() for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems) diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index 74f41bf10884..0b922c77e011 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -27,35 +27,37 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) /proc/focused_tests() var/list/focused_tests = list() for (var/datum/unit_test/unit_test as anything in subtypesof(/datum/unit_test)) - if (initial(unit_test.focus)) + if (unit_test::test_flags & UNIT_TEST_FOCUS) focused_tests += unit_test - return focused_tests.len > 0 ? focused_tests : null + return length(focused_tests) ? focused_tests : null /datum/unit_test - /// Do not instantiate if type matches this abstract_type = /datum/unit_test - //Bit of metadata for the future maybe - var/list/procs_tested + /// Behavior flags for this unit test + var/test_flags = UNIT_TEST_BASIC + /// The priority of the test, the larger it is the later it fires + var/priority = TEST_DEFAULT + /// How many times this unit test will run. Use the TEST_REPEAT() macro + var/times_to_run = 1 + // internal shit + /// If this test has passed or not + var/succeeded = TRUE /// The bottom left floor turf of the testing zone var/turf/run_loc_floor_bottom_left - /// The top right floor turf of the testing zone var/turf/run_loc_floor_top_right - ///The priority of the test, the larger it is the later it fires - var/priority = TEST_DEFAULT - //internal shit - var/focus = FALSE - var/succeeded = TRUE + /// A list of instances created by this unit test. Use allocate() var/list/allocated + /// Lazy list of why this unit test failed. var/list/fail_reasons - var/times_to_run = 1 - /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. Stored on the base datum for usability in other relevant tests that need this data. + /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. + /// Stored on the base datum for usability in other relevant tests that need this data. var/static/list/uncreatables = null - + /// Reference to the blank z-level containing our testing enviroment var/static/datum/space_level/reservation /proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b) @@ -66,10 +68,9 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) var/datum/map_template/unit_tests/template = new reservation = template.load_new_z() - if (isnull(uncreatables)) - uncreatables = build_list_of_uncreatables() + uncreatables ||= build_list_of_uncreatables() - allocated = new + allocated = list() run_loc_floor_bottom_left = get_turf(locate(/obj/effect/landmark/unit_test_bottom_left) in GLOB.landmarks_list) run_loc_floor_top_right = get_turf(locate(/obj/effect/landmark/unit_test_top_right) in GLOB.landmarks_list) @@ -367,15 +368,37 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) /proc/RunUnitTests() CHECK_TICK - var/list/tests_to_run = subtypesof(/datum/unit_test) + // Find our primary unit test map & find out if we are the secondary + var/datum/map_config/primary_unit_test_map + var/is_secondary_unit_test_map = FALSE + var/found_secondary_unit_test_map = FALSE + for(var/map_name, _map_config in config.maplist) + var/datum/map_config/map_config = _map_config + if(map_config.is_unit_test_map) + primary_unit_test_map = map_config + if(!LAZYLEN(map_config.skipped_tests) && !found_secondary_unit_test_map) + found_secondary_unit_test_map = TRUE + if(SSmapping.current_map.map_name == map_config.map_name) + is_secondary_unit_test_map = TRUE + + var/list/tests_to_run = list() var/list/focused_tests = list() - for (var/_test_to_run in tests_to_run) - var/datum/unit_test/test_to_run = _test_to_run - if (initial(test_to_run.focus)) - focused_tests += test_to_run + for (var/datum/unit_test/potential_test as anything in subtypesof(/datum/unit_test)) + // If the test has [UNIT_TEST_DEBUG_MAP_ONLY] and we aren't the primary unit test map, skip it. + // HOWEVER, some unit tests are incompatible with the primary testing map, so we must offload them a secondary one with no blacklisted tests. + if((potential_test::test_flags & UNIT_TEST_DEBUG_MAP_ONLY) && !SSmapping.current_map.is_unit_test_map && \ + !(primary_unit_test_map.skipped_tests?.Find(potential_test) && is_secondary_unit_test_map) \ + ) + continue + if (potential_test::test_flags & UNIT_TEST_FOCUS) + focused_tests += potential_test + continue + tests_to_run += potential_test if(length(focused_tests)) tests_to_run = focused_tests + primary_unit_test_map = null // I'm paranoid + sortTim(tests_to_run, GLOBAL_PROC_REF(cmp_unit_test_priority)) var/list/test_results = list() diff --git a/config/maps.txt b/config/maps.txt index 56771ade99a2..d9d967b5477a 100644 --- a/config/maps.txt +++ b/config/maps.txt @@ -15,6 +15,8 @@ Format: webmap_url (link to the a webmap to see the map in the user's browser) endmap +# When adding or removing maps be sure to also add/remove from ci_config_maps.txt + # Production-level maps. map deltastation @@ -65,3 +67,6 @@ endmap map runtimestation endmap + +map runtimestation_minimal +endmap diff --git a/tools/ci/ci_config_maps.txt b/tools/ci/ci_config_maps.txt new file mode 100644 index 000000000000..b9dc69d4e686 --- /dev/null +++ b/tools/ci/ci_config_maps.txt @@ -0,0 +1,32 @@ +map deltastation +endmap + +map icebox +endmap + +map catwalkstation +endmap + +map metastation +endmap + +map tramstation +endmap + +map nebulastation +endmap + +map wawastation +endmap + +map gateway_test +endmap + +map multiz_debug +endmap + +map runtimestation +endmap + +map runtimestation_minimal +endmap diff --git a/tools/ci/run_server.sh b/tools/ci/run_server.sh index 178bd2c5b5d2..e55b9a987ee9 100644 --- a/tools/ci/run_server.sh +++ b/tools/ci/run_server.sh @@ -11,6 +11,7 @@ mkdir -p ci_test/data #test config cp tools/ci/ci_config.txt ci_test/config/config.txt +cp tools/ci/ci_config_maps.txt ci_test/config/maps.txt #set the map cp _maps/$MAP.json ci_test/data/next_map.json @@ -20,8 +21,11 @@ DreamDaemon tgstation.dmb -close -trusted -verbose -params "log-directory=ci" cd .. -mkdir -p data/screenshots_new -cp -r ci_test/data/screenshots_new data/screenshots_new +mkdir -p data +if [ -d "ci_test/data/screenshots_new" ]; then + mkdir -p data/screenshots_new + cp -r ci_test/data/screenshots_new data/screenshots_new +fi cp ci_test/data/unit_tests.json data/unit_tests.json cat ci_test/data/logs/ci/clean_run.lk From 516e13d836bbf1e2a42cd90c922710d41f7acf01 Mon Sep 17 00:00:00 2001 From: mcbalaam <104003807+mcbalaam@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:47:03 +0700 Subject: [PATCH 044/101] fix: misc Big Manipulator bug fixes (#96374) --- .../big_manipulator/big_manipulator.dm | 22 +++++++-- .../big_manipulator_interactions.dm | 39 ++++++++------- .../big_manipulator/manipulator_tasks.dm | 20 +++++++- .../game/machinery/big_manipulator/tasking.dm | 28 ++++++----- .../tgui/interfaces/BigManipulator/index.tsx | 48 +++++++++++-------- .../tgui/interfaces/BigManipulator/types.ts | 3 +- 6 files changed, 105 insertions(+), 55 deletions(-) diff --git a/code/game/machinery/big_manipulator/big_manipulator.dm b/code/game/machinery/big_manipulator/big_manipulator.dm index 83ab74f9a034..eb78f7708a62 100644 --- a/code/game/machinery/big_manipulator/big_manipulator.dm +++ b/code/game/machinery/big_manipulator/big_manipulator.dm @@ -500,26 +500,26 @@ var/datum/manipulator_task/cargo/dropoff_base/use/t = task td["turf"] = "[t.offset_dx],[t.offset_dy]" td["filters_status"] = t.should_use_filters - td["filtering_mode"] = t.filtering_mode td["item_filters"] = _collect_filter_names(t.atom_filters) td["settings_list"] = _collect_priorities(t.interaction_priorities) td["worker_interaction"] = t.worker_interaction td["use_post_interaction"] = t.use_post_interaction td["worker_use_rmb"] = t.worker_use_rmb td["worker_combat_mode"] = t.worker_combat_mode + td["skip_anchored"] = t.skip_anchored else if(istype(task, /datum/manipulator_task/cargo/interact)) td["task_type"] = TASK_TYPE_INTERACT var/datum/manipulator_task/cargo/interact/t = task td["turf"] = "[t.offset_dx],[t.offset_dy]" td["filters_status"] = t.should_use_filters - td["filtering_mode"] = t.filtering_mode td["item_filters"] = _collect_filter_names(t.atom_filters) td["settings_list"] = _collect_priorities(t.interaction_priorities) td["worker_interaction"] = t.worker_interaction td["use_post_interaction"] = t.use_post_interaction td["worker_use_rmb"] = t.worker_use_rmb td["worker_combat_mode"] = t.worker_combat_mode + td["skip_anchored"] = t.skip_anchored else if(istype(task, /datum/manipulator_task/simple/wait)) td["task_type"] = TASK_TYPE_WAIT @@ -587,7 +587,10 @@ return TRUE if("unbuckle") - unbuckle_all_mobs() + var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker?.resolve() + if(poor_monkey) + poor_monkey.drop_all_held_items() + poor_monkey.forceMove(get_turf(src)) monkey_worker = null return TRUE @@ -812,6 +815,8 @@ return TRUE if("cycle_filtering_mode") + if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use) || istype(target_task, /datum/manipulator_task/cargo/interact)) + return FALSE if(!istype(target_task, /datum/manipulator_task/cargo)) return FALSE var/datum/manipulator_task/cargo/ct = target_task @@ -897,6 +902,17 @@ return TRUE return FALSE + if("toggle_skip_anchored") + if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use)) + var/datum/manipulator_task/cargo/dropoff_base/use/unanchor_target = target_task + unanchor_target.skip_anchored = !unanchor_target.skip_anchored + return TRUE + if(istype(target_task, /datum/manipulator_task/cargo/interact)) + var/datum/manipulator_task/cargo/interact/unanchor_target = target_task + unanchor_target.skip_anchored = !unanchor_target.skip_anchored + return TRUE + return FALSE + /// Cycles the given value in the given list. /obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values) var/current_index = possible_values.Find(current_value) diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm index 01500efdd5aa..e5f7c361330d 100644 --- a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm +++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm @@ -137,14 +137,18 @@ return FALSE var/obj/item/held_item = obj_resolve - var/atom/type_to_use = destination_task.find_type_priority() + var/atom/type_to_use = destination_task.find_type_priority(destination_task.skip_anchored) if(isnull(type_to_use)) - check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point) + drop_held_after_use(destination_task) return FALSE if(isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use)) - check_for_cycle_end_drop(destination_task, FALSE, work_done_at_point) + drop_held_after_use(destination_task) + return FALSE + + if(istype(destination_task, /datum/manipulator_task/cargo/interact) && destination_task.should_use_filters && isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use)) + drop_held_after_use(destination_task) return FALSE var/original_loc = held_item.loc @@ -153,9 +157,10 @@ if(held_item.GetComponent(/datum/component/two_handed)) held_item.attack_self(monkey_resolve) + var/old_combat_mode = monkey_resolve.combat_mode monkey_resolve.combat_mode = destination_task.worker_combat_mode held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE)) - monkey_resolve.combat_mode = FALSE + monkey_resolve.combat_mode = old_combat_mode do_attack_animation(destination_turf) manipulator_arm.do_attack_animation(destination_turf) @@ -217,7 +222,7 @@ obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src)) finish_manipulation() else - schedule_next_cycle() + addtimer(CALLBACK(src, PROC_REF(try_use_thing), destination_task, TRUE), BASE_INTERACTION_TIME * 2) /obj/machinery/big_manipulator/proc/throw_thing(datum/manipulator_task/cargo/dropoff_base/throw/throw_task) var/drop_turf = throw_task.interaction_turf @@ -241,21 +246,19 @@ finish_manipulation() return - var/atom/type_to_use = destination_task.find_type_priority() + var/atom/type_to_use = destination_task.find_type_priority(destination_task.skip_anchored) if(isnull(type_to_use)) check_end_of_use_for_use_with_empty_hand(destination_task, FALSE) return - if(isitem(type_to_use)) - var/obj/item/interact_with_item = type_to_use - var/resolve_loc = interact_with_item.loc - monkey_resolve.put_in_active_hand(interact_with_item) - interact_with_item.attack_self(monkey_resolve) - interact_with_item.forceMove(resolve_loc) - else - monkey_resolve.combat_mode = destination_task.worker_combat_mode - monkey_resolve.UnarmedAttack(type_to_use) - monkey_resolve.combat_mode = FALSE + if(destination_task.should_use_filters && isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use)) + check_end_of_use_for_use_with_empty_hand(destination_task, FALSE) + return + + var/old_combat_mode = monkey_resolve.combat_mode + monkey_resolve.combat_mode = destination_task.worker_combat_mode + monkey_resolve.UnarmedAttack(type_to_use, modifiers = list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE)) + monkey_resolve.combat_mode = old_combat_mode var/turf/dest_turf = destination_task.interaction_turf if(dest_turf) @@ -277,6 +280,10 @@ /// Completes the current manipulation action and schedules the next step. /obj/machinery/big_manipulator/proc/finish_manipulation() + if(held_object) + var/obj/resolved = held_object.resolve() + if(resolved && resolved.loc == src) + resolved.forceMove(drop_location()) held_object = null manipulator_arm.update_claw(null) current_task = null diff --git a/code/game/machinery/big_manipulator/manipulator_tasks.dm b/code/game/machinery/big_manipulator/manipulator_tasks.dm index 48a136ff255f..00fe12715770 100644 --- a/code/game/machinery/big_manipulator/manipulator_tasks.dm +++ b/code/game/machinery/big_manipulator/manipulator_tasks.dm @@ -100,7 +100,7 @@ /datum/manipulator_task/cargo/proc/fill_priority_list(manipulator_tier) return list() -/datum/manipulator_task/cargo/proc/find_type_priority() +/datum/manipulator_task/cargo/proc/find_type_priority(skip_anchored = FALSE) var/atom/movable/best_candidate = null var/best_priority_index = INFINITY @@ -117,6 +117,9 @@ if(!istype(thing, prio.atom_typepath)) continue + if(skip_anchored && thing.anchored) + continue + if(isliving(thing)) var/mob/living/living_mob = thing if(living_mob.stat == DEAD) @@ -299,12 +302,16 @@ return FALSE return TRUE -/datum/manipulator_task/cargo/dropoff_base/can_run(obj/machinery/big_manipulator/manipulator) +/datum/manipulator_task/cargo/dropoff_base/use/can_run(obj/machinery/big_manipulator/manipulator) if(!..()) return FALSE var/atom/movable/target = manipulator.held_object?.resolve() if(!target) return FALSE + if(!manipulator.monkey_worker?.resolve()) + return FALSE + if(!find_type_priority(skip_anchored)) + return FALSE return can_accept(target) /datum/manipulator_task/cargo/dropoff_base/run_task(obj/machinery/big_manipulator/manipulator) @@ -409,6 +416,7 @@ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT var/worker_combat_mode = FALSE var/worker_use_rmb = FALSE + var/skip_anchored = FALSE /datum/manipulator_task/cargo/dropoff_base/use/fill_priority_list(manipulator_tier) var/list/priorities = list( @@ -434,6 +442,7 @@ data["use_post_interaction"] = use_post_interaction data["worker_combat_mode"] = worker_combat_mode data["worker_use_rmb"] = worker_use_rmb + data["skip_anchored"] = skip_anchored return data /datum/manipulator_task/cargo/dropoff_base/use/New(turf/new_turf, manipulator_tier, serialized_data) @@ -443,6 +452,7 @@ use_post_interaction = serialized_data["use_post_interaction"] worker_combat_mode = !!serialized_data["worker_combat_mode"] worker_use_rmb = !!serialized_data["worker_use_rmb"] + skip_anchored = !!serialized_data["skip_anchored"] return /datum/manipulator_task/cargo/dropoff_base/use/do_dropoff(obj/machinery/big_manipulator/manipulator) @@ -456,6 +466,7 @@ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT var/worker_combat_mode = FALSE var/worker_use_rmb = FALSE + var/skip_anchored = FALSE /datum/manipulator_task/cargo/interact/fill_priority_list(manipulator_tier) var/list/priorities = list( @@ -482,6 +493,7 @@ data["use_post_interaction"] = use_post_interaction data["worker_combat_mode"] = worker_combat_mode data["worker_use_rmb"] = worker_use_rmb + data["skip_anchored"] = skip_anchored return data /datum/manipulator_task/cargo/interact/New(turf/new_turf, manipulator_tier, serialized_data) @@ -491,11 +503,15 @@ use_post_interaction = serialized_data["use_post_interaction"] worker_combat_mode = !!serialized_data["worker_combat_mode"] worker_use_rmb = !!serialized_data["worker_use_rmb"] + skip_anchored = !!serialized_data["skip_anchored"] return /datum/manipulator_task/cargo/interact/proc/try_interact(obj/machinery/big_manipulator/manipulator) var/atom/movable/held = manipulator.held_object?.resolve() if(held) + if(!manipulator.monkey_worker?.resolve()) + manipulator.nothing_ever_happens() + return manipulator.try_use_thing(src) else manipulator.use_thing_with_empty_hand(src) diff --git a/code/game/machinery/big_manipulator/tasking.dm b/code/game/machinery/big_manipulator/tasking.dm index cc91434363e5..65d355c0b23a 100644 --- a/code/game/machinery/big_manipulator/tasking.dm +++ b/code/game/machinery/big_manipulator/tasking.dm @@ -13,6 +13,8 @@ // Moves through the list, skipping tasks that can't run. /datum/tasking_strategy/sequential + /// Separate index for candidate selection to avoid corrupting the task index. + var/candidate_index = 1 /datum/tasking_strategy/sequential/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator) if(!length(tasks)) @@ -33,16 +35,18 @@ /datum/tasking_strategy/sequential/get_next_candidate(list/candidates) if(!length(candidates)) return null - if(current_index < 1 || current_index > length(candidates)) - current_index = 1 - var/candidate = candidates[current_index] - current_index++ - if(current_index > length(candidates)) - current_index = 1 + if(candidate_index < 1 || candidate_index > length(candidates)) + candidate_index = 1 + var/candidate = candidates[candidate_index] + candidate_index++ + if(candidate_index > length(candidates)) + candidate_index = 1 return candidate // Stays on the current task until it can run. /datum/tasking_strategy/strict + /// Separate index for candidate selection to avoid corrupting the task index. + var/candidate_index = 1 /datum/tasking_strategy/strict/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator) if(!length(tasks)) @@ -60,10 +64,10 @@ /datum/tasking_strategy/strict/get_next_candidate(list/candidates) if(!length(candidates)) return null - if(current_index < 1 || current_index > length(candidates)) - current_index = 1 - var/candidate = candidates[current_index] - current_index++ - if(current_index > length(candidates)) - current_index = 1 + if(candidate_index < 1 || candidate_index > length(candidates)) + candidate_index = 1 + var/candidate = candidates[candidate_index] + candidate_index++ + if(candidate_index > length(candidates)) + candidate_index = 1 return candidate diff --git a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx index 144e9d3e94c9..e56d1dd431f8 100644 --- a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx +++ b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx @@ -173,7 +173,7 @@ function TaskEditModal(props: TaskEditModalProps) { const isCargo = !!task.turf; const isPickup = task.task_type.includes('pickup'); - const isDropoff = task.task_type.includes('dropoff'); + const isDropoff = task.task_type === 'drop' || task.task_type === 'throw' || task.task_type === 'use'; const isInteract = task.task_type.includes('interact'); const currentButton = task.turf @@ -236,12 +236,16 @@ function TaskEditModal(props: TaskEditModalProps) { - adjust('cycle_filtering_mode')} - tooltip="Cycle object category" - /> + {(task.task_type === 'pickup' || + task.task_type === 'drop' || + task.task_type === 'throw') && ( + adjust('cycle_filtering_mode')} + tooltip="Cycle object category" + /> + )} - adjust('cycle_interaction_mode')} - tooltip="Drop / Throw / Use" - /> - adjust('cycle_overflow_status')} - tooltip="Cycle overflow behaviour" - /> - {task.interaction_mode?.toUpperCase() === 'THROW' && ( + {task.task_type === 'drop' && ( + adjust('cycle_overflow_status')} + tooltip="Cycle overflow behaviour" + /> + )} + {task.task_type === 'throw' && ( )} - {(isDropoff || isInteract) && task.interaction_mode?.toUpperCase() !== 'THROW' && ( + {(task.task_type === 'use' || isInteract) && ( <> adjust('toggle_worker_combat')} tooltip="Use combat mode during interaction" /> + adjust('toggle_skip_anchored')} + tooltip="Skip anchored objects when looking for interaction targets" + /> Date: Tue, 9 Jun 2026 11:47:28 +0000 Subject: [PATCH 045/101] Automatic changelog for PR #96374 [ci skip] --- html/changelogs/AutoChangeLog-pr-96374.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96374.yml diff --git a/html/changelogs/AutoChangeLog-pr-96374.yml b/html/changelogs/AutoChangeLog-pr-96374.yml new file mode 100644 index 000000000000..d4fb40ff65cd --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96374.yml @@ -0,0 +1,4 @@ +author: "mcbalaam" +delete-after: True +changes: + - bugfix: "Fixed some of the Big Manipulator behavior such as not working in strict mode, putting items inside itself and incorrectly targeting items due to misconfigured filters." \ No newline at end of file From cd3f6184bd61d775aeec31c561e2b41ea66d96cd Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:00:25 +0000 Subject: [PATCH 046/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96374.yml | 4 ---- html/changelogs/archive/2026-06.yml | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96374.yml diff --git a/html/changelogs/AutoChangeLog-pr-96374.yml b/html/changelogs/AutoChangeLog-pr-96374.yml deleted file mode 100644 index d4fb40ff65cd..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96374.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "mcbalaam" -delete-after: True -changes: - - bugfix: "Fixed some of the Big Manipulator behavior such as not working in strict mode, putting items inside itself and incorrectly targeting items due to misconfigured filters." \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index 8205762fdd27..df4091ff3738 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -219,3 +219,8 @@ - balance: Objects with the walking aid reduce limbless slowdown by 40%, while dedicated medical crutches reduce it by 60%. - refactor: Refactor cane and crutch code logic into a modular walking aid component +2026-06-09: + mcbalaam: + - bugfix: Fixed some of the Big Manipulator behavior such as not working in strict + mode, putting items inside itself and incorrectly targeting items due to misconfigured + filters. From 0bd4b0820f63355bc942b5efdac2557b9f1d4587 Mon Sep 17 00:00:00 2001 From: EnterTheJake <102721711+EnterTheJake@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:38:00 +0200 Subject: [PATCH 047/101] Adds TGMC Tactical maps to the game and to Nuclear Operatives! (#96068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## About The Pull Request Brought to you by Team Powercrèpe.(same group of people that delivered the Heretic Overhaul) This PR ports the TGMC Tacmaps and grants em to our NukeOps. Code by Me, @Xander3359, @Arturlang, @Absolucy. Mapping by @RipGrayson Sprites by @KillerOrcaCora Writing bits by @necromanceranne Tacmaps are essentially dynamically generated minimaps, Tac map examples Tac-Maps Icons As you can see from the screenshots above, departments are classified by color, hovering your cursor over an area or a team mate, will display their name, the system does allow the user to switch between Multi-Z, and lastly you can draw and add labels to the map, although the last option is only available to the NukeOps Leader and OW agents. The functionality is tied to the Syndicate implants nukies start with, Cayenne, Syndie borgs and Mechs also have the ability to toggle the map. Likewise, the tacmaps will display the positioning of the following in real time! - Nuke ops agents - Syndie borgs and Mechs - Cayenne - Location of the nuclear fission device (and beer nuke) - Location of the Nuclear Authentication Disk. SkyBridge OFF SkyBridge ON Holo table done https://github.com/user-attachments/assets/caf38538-ef04-4330-992c-3137a67ea0d0 Lastly The tactiical map implant cannot be be used on the nuke ops base,If the team wishes to interact with the map they'll have to Interact with the "Reconnaisance Platform" (Sprite by Orcacora) in the briefing room, which has seen some mapping changes (Brought to you by TheLastGrayson). ## Why It's Good For The Game Originally this feature was planned for the Upcoming Contractor Rework our team is currently working on, but given the massive size of it all, we decided to atomize it. But without that consideration in mind, I think it makes sense for Nuclear operatives of all antagonists to have the means to Recon and plot an assault plan onto the station and actually be able to know the whereabouts of their teammates. Obviously this feature has a lot of other potential applications, we have wanted to have functional Maps for a very long time, if someone wants to give it a go once the feature is merged, by all means. ## Changelog :cl: add: Tactical Maps have been added to the game! add: Nuclear Operatives implants now grant the ability to open a Minimap, this map will display and show the names of various areas of the station, and the positioning of the Nukeops team,borgs,mechs,Cayenne and the nuke disk in real time! add: Nukie leaders and OW agents can also draw and apply labels to the map. add: the "Recoinassance Platform" has been added to the Nukie Base, it allows displaying of the Tactical maps while on the Syndicate Base Z level. map: The briefing room in the NukeOps base has been remapped to better accomodate the new Holographic table. map: Changed the front shutters on the Syndicate Infiltrator to the more fitting Syndicate Shutters. /:cl: --------- Co-authored-by: Xander3359 <66163761+Xander3359@users.noreply.github.com> Co-authored-by: Artur Lang <24881678+Arturlang@users.noreply.github.com> Co-authored-by: Lucy Co-authored-by: Copilot Co-authored-by: RipGrayson <49290523+RipGrayson@users.noreply.github.com> Co-authored-by: necromanceranne <40847847+necromanceranne@users.noreply.github.com> --- _maps/shuttles/infiltrator_basic.dmm | 19 +- _maps/templates/lazy_templates/nukie_base.dmm | 1293 +++++++++-------- code/__DEFINES/hud.dm | 13 + code/__DEFINES/layers.dm | 4 + code/__DEFINES/logging.dm | 1 + code/__DEFINES/minimap.dm | 62 + code/__DEFINES/traits/declarations.dm | 2 + code/__HELPERS/logging/game.dm | 4 + code/__HELPERS/type2type.dm | 9 + code/_globalvars/traits/_traits.dm | 1 + code/_onclick/click.dm | 7 + .../hud/screen_objects/screen_objects.dm | 2 +- .../configuration/entries/general.dm | 3 + .../proximity_monitor/proximity_monitor.dm | 5 +- code/game/area/areas.dm | 4 + code/game/area/areas/away_content.dm | 1 + code/game/area/areas/mining.dm | 1 + code/game/area/areas/misc.dm | 1 + code/game/area/areas/ruins/_ruins.dm | 1 + code/game/area/areas/station/cargo.dm | 1 + code/game/area/areas/station/command.dm | 1 + code/game/area/areas/station/common.dm | 1 + code/game/area/areas/station/engineering.dm | 2 + code/game/area/areas/station/hallway.dm | 1 + code/game/area/areas/station/maintenance.dm | 2 + code/game/area/areas/station/medical.dm | 2 + code/game/area/areas/station/science.dm | 2 + code/game/area/areas/station/security.dm | 1 + code/game/area/areas/station/service.dm | 8 + code/game/area/areas/station/solars.dm | 1 + code/game/area/areas/station/telecomm.dm | 1 + code/game/atom/_atom.dm | 7 + code/game/machinery/doors/door.dm | 2 + .../machinery/porta_turret/portable_turret.dm | 13 + .../objects/items/implants/implant_tacmap.dm | 120 ++ .../game/objects/items/storage/uplink_kits.dm | 2 + code/game/objects/structures/fence.dm | 1 + code/game/objects/structures/ladders.dm | 8 + code/game/objects/structures/stairs.dm | 48 + code/game/objects/structures/window.dm | 1 + code/game/turfs/closed/_closed.dm | 1 + code/game/turfs/open/asteroid.dm | 2 + code/game/turfs/open/floor/iron_floor.dm | 1 + code/game/turfs/open/floor/misc_floor.dm | 6 + code/game/turfs/open/ice.dm | 1 + code/game/turfs/open/space/space.dm | 1 + code/game/turfs/turf.dm | 3 + code/modules/antagonists/clown_ops/outfits.dm | 3 +- .../equipment/nuclear_authentication_disk.dm | 1 + .../equipment/nuclear_bomb/_nuclear_bomb.dm | 10 + .../equipment/nuclear_bomb/beer_nuke.dm | 1 + code/modules/antagonists/nukeop/outfits.dm | 21 + .../logging/categories/log_category_game.dm | 4 + code/modules/minimap/_minimap.dm | 45 + code/modules/minimap/hud/minimal_z_level.dm | 68 + code/modules/minimap/hud/minimap_dimmer.dm | 7 + code/modules/minimap/hud/minimap_display.dm | 585 ++++++++ code/modules/minimap/hud/minimap_drawing.dm | 59 + code/modules/minimap/hud/minimap_element.dm | 12 + code/modules/minimap/hud/minimap_label.dm | 6 + code/modules/minimap/hud/minimap_toolbar.dm | 198 +++ .../minimap/hud/minimap_tracking_blip.dm | 90 ++ code/modules/minimap/minimap.dm | 84 ++ code/modules/minimap/minimap_action.dm | 141 ++ code/modules/minimap/minimap_table.dm | 225 +++ .../mob/living/basic/bots/medbot/medbot.dm | 3 + .../mob/living/basic/space_fauna/carp/carp.dm | 2 + .../mob/living/silicon/robot/robot_model.dm | 17 + .../research/xenobiology/xenobiology.dm | 2 + code/modules/uplink/uplink_items/nukeops.dm | 2 +- code/modules/vehicles/mecha/combat/gygax.dm | 4 + code/modules/vehicles/mecha/combat/honker.dm | 4 + .../modules/vehicles/mecha/combat/marauder.dm | 4 + config/logging.txt | 3 + icons/hud/actions.dmi | Bin 10089 -> 9680 bytes icons/hud/implants.dmi | Bin 8488 -> 3879 bytes icons/obj/machines/minimap_table.dmi | Bin 0 -> 991 bytes icons/obj/machines/minimap_table_hologram.dmi | Bin 0 -> 18543 bytes icons/ui_icons/minimap/draw_tools.dmi | Bin 0 -> 603 bytes icons/ui_icons/minimap/map_blips.dmi | Bin 0 -> 886 bytes icons/ui_icons/minimap/map_blips_large.dmi | Bin 0 -> 818 bytes icons/ui_icons/minimap/minimap.dmi | Bin 0 -> 246 bytes icons/ui_icons/minimap/minimap_buttons.dmi | Bin 0 -> 793 bytes .../minimap/minimap_mouse/draw_blue.dmi | Bin 0 -> 253 bytes .../minimap/minimap_mouse/draw_erase.dmi | Bin 0 -> 250 bytes .../minimap/minimap_mouse/draw_purple.dmi | Bin 0 -> 271 bytes .../minimap/minimap_mouse/draw_red.dmi | Bin 0 -> 256 bytes .../minimap/minimap_mouse/draw_yellow.dmi | Bin 0 -> 252 bytes .../ui_icons/minimap/minimap_mouse/label.dmi | Bin 0 -> 243 bytes tgstation.dme | 14 + 90 files changed, 2675 insertions(+), 613 deletions(-) create mode 100644 code/__DEFINES/minimap.dm create mode 100644 code/game/objects/items/implants/implant_tacmap.dm create mode 100644 code/modules/minimap/_minimap.dm create mode 100644 code/modules/minimap/hud/minimal_z_level.dm create mode 100644 code/modules/minimap/hud/minimap_dimmer.dm create mode 100644 code/modules/minimap/hud/minimap_display.dm create mode 100644 code/modules/minimap/hud/minimap_drawing.dm create mode 100644 code/modules/minimap/hud/minimap_element.dm create mode 100644 code/modules/minimap/hud/minimap_label.dm create mode 100644 code/modules/minimap/hud/minimap_toolbar.dm create mode 100644 code/modules/minimap/hud/minimap_tracking_blip.dm create mode 100644 code/modules/minimap/minimap.dm create mode 100644 code/modules/minimap/minimap_action.dm create mode 100644 code/modules/minimap/minimap_table.dm create mode 100644 icons/obj/machines/minimap_table.dmi create mode 100644 icons/obj/machines/minimap_table_hologram.dmi create mode 100644 icons/ui_icons/minimap/draw_tools.dmi create mode 100644 icons/ui_icons/minimap/map_blips.dmi create mode 100644 icons/ui_icons/minimap/map_blips_large.dmi create mode 100644 icons/ui_icons/minimap/minimap.dmi create mode 100644 icons/ui_icons/minimap/minimap_buttons.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/draw_red.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi create mode 100644 icons/ui_icons/minimap/minimap_mouse/label.dmi diff --git a/_maps/shuttles/infiltrator_basic.dmm b/_maps/shuttles/infiltrator_basic.dmm index 61f8ea231df2..468a10b865b7 100644 --- a/_maps/shuttles/infiltrator_basic.dmm +++ b/_maps/shuttles/infiltrator_basic.dmm @@ -19,9 +19,9 @@ /area/shuttle/syndicate/hallway) "ae" = ( /obj/effect/spawner/structure/window/reinforced/plasma/plastitanium, -/obj/machinery/door/poddoor/shutters{ - id = "syndieshutters"; - name = "Blast Shutters" +/obj/machinery/door/poddoor/shutters/syndicate{ + name = "Blast Shutters"; + id = "syndieshutters" }, /turf/open/floor/plating, /area/shuttle/syndicate/bridge) @@ -191,10 +191,6 @@ }, /area/shuttle/syndicate/airlock) "aY" = ( -/obj/machinery/door/poddoor{ - id = "smindicate"; - name = "Outer Blast Door" - }, /obj/docking_port/mobile/infiltrator, /obj/structure/fans/tiny, /obj/machinery/button/door/directional/north{ @@ -205,6 +201,11 @@ /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 }, +/obj/machinery/door/poddoor/shutters/syndicate/indestructible{ + id = "smindicate"; + name = "Outer Blast Door"; + dir = 4 + }, /turf/open/floor/plating, /area/shuttle/syndicate/airlock) "aZ" = ( @@ -601,9 +602,7 @@ /obj/item/storage/box/zipties, /obj/machinery/computer/security/telescreen/entertainment/directional/west, /obj/structure/table/reinforced/plastitaniumglass, -/turf/open/floor/iron/dark/smooth_corner{ - dir = 2 - }, +/turf/open/floor/iron/dark/smooth_corner, /area/shuttle/syndicate/airlock) "iE" = ( /obj/structure/closet/syndicate/nuclear, diff --git a/_maps/templates/lazy_templates/nukie_base.dmm b/_maps/templates/lazy_templates/nukie_base.dmm index 707e8a251f31..c3c6268f4006 100644 --- a/_maps/templates/lazy_templates/nukie_base.dmm +++ b/_maps/templates/lazy_templates/nukie_base.dmm @@ -7,14 +7,6 @@ "ae" = ( /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/expansion_bombthreat) -"ah" = ( -/obj/effect/turf_decal/stripes/end{ - dir = 4 - }, -/obj/machinery/mech_bay_recharge_port, -/obj/machinery/light/cold/directional/south, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "al" = ( /obj/effect/turf_decal/siding/wideplating/dark{ dir = 8 @@ -22,15 +14,6 @@ /obj/machinery/light/small/directional/north, /turf/open/floor/wood/tile, /area/centcom/syndicate_mothership/control) -"as" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/structure/sign/poster/contraband/cybersun_six_hundred/directional/east, -/obj/item/kirbyplants/random, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) -"au" = ( -/turf/open/floor/circuit/red, -/area/centcom/syndicate_mothership/control) "ay" = ( /obj/structure/table/reinforced, /obj/item/papercutter, @@ -54,6 +37,13 @@ /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"aS" = ( +/obj/effect/turf_decal/siding/red/corner, +/obj/item/folder/red, +/obj/item/pen/red, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "aX" = ( /obj/machinery/shower/directional/south, /turf/open/floor/iron/freezer, @@ -77,10 +67,6 @@ /obj/structure/mirror/directional/east, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) -"bp" = ( -/obj/machinery/light/cold/directional/north, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "bB" = ( /obj/structure/railing{ dir = 4 @@ -123,6 +109,10 @@ }, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_bombthreat) +"bS" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark, +/turf/closed/indestructible/syndicate, +/area/centcom/syndicate_mothership/control) "bT" = ( /obj/structure/flora/rock/pile/style_random, /turf/open/misc/asteroid/snow/airless, @@ -162,6 +152,15 @@ /obj/structure/cable, /turf/open/floor/iron/smooth, /area/centcom/syndicate_mothership/control) +"cC" = ( +/obj/machinery/vending/cola, +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 4 + }, +/turf/open/floor/iron/white/textured_large{ + color = "#b51026" + }, +/area/centcom/syndicate_mothership/control) "cF" = ( /obj/effect/baseturf_helper/asteroid/snow, /turf/closed/indestructible/syndicate, @@ -219,6 +218,20 @@ /obj/structure/flora/grass/both/style_random, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"cW" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark, +/obj/machinery/light/red/dim/directional/north, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) +"cY" = ( +/obj/machinery/light/cold/directional/east, +/obj/machinery/vending/snack/teal, +/obj/structure/cable, +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/open/floor/catwalk_floor/iron_dark, +/area/centcom/syndicate_mothership/control) "cZ" = ( /obj/structure/mop_bucket/janitorialcart{ dir = 4 @@ -233,15 +246,6 @@ /obj/machinery/shower/directional/south, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) -"dn" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 1 - }, -/obj/machinery/camera/autoname/directional/west{ - network = list("nukie") - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "dq" = ( /obj/machinery/light/small/directional/north, /turf/open/floor/iron/smooth_half, @@ -252,22 +256,16 @@ }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"du" = ( -/obj/structure/chair/stool/directional/north, -/obj/effect/landmark/start/nukeop_base, -/obj/structure/sign/poster/contraband/donk_co/directional/south, -/turf/open/floor/wood/tile, -/area/centcom/syndicate_mothership/control) -"dw" = ( -/obj/structure/table/reinforced, -/obj/machinery/recharger, -/turf/open/floor/carpet, -/area/centcom/syndicate_mothership/control) "dx" = ( /obj/machinery/shower/directional/east, /obj/effect/turf_decal/stripes/box, /turf/open/floor/mineral/titanium/tiled, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) +"dB" = ( +/obj/machinery/recharger, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/carpet, +/area/centcom/syndicate_mothership/control) "dF" = ( /obj/machinery/light/cold/directional/west, /obj/structure/table/glass/plasmaglass, @@ -467,6 +465,12 @@ /obj/effect/mapping_helpers/airlock/access/all/syndicate/general, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) +"eM" = ( +/obj/effect/turf_decal/siding/red, +/obj/machinery/recharger, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "fa" = ( /obj/effect/turf_decal/stripes/line, /obj/effect/turf_decal/stripes/line{ @@ -478,15 +482,6 @@ }, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) -"fb" = ( -/obj/structure/chair/sofa/bench/left{ - dir = 4 - }, -/obj/machinery/camera/autoname/directional/north{ - network = list("nukie") - }, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "fk" = ( /turf/open/floor/circuit/red/off, /area/centcom/syndicate_mothership/expansion_bioterrorism) @@ -497,6 +492,15 @@ /obj/machinery/portable_atmospherics/canister/plasma, /turf/open/floor/plating, /area/centcom/syndicate_mothership/expansion_bombthreat) +"fr" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 10 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/obj/item/folder/white, +/obj/item/pen/red, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) "fu" = ( /obj/machinery/shower/directional/south, /obj/machinery/light/floor, @@ -546,6 +550,14 @@ /obj/machinery/hydroponics/constructable, /turf/open/floor/mineral/titanium/tiled, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"fP" = ( +/obj/effect/turf_decal/siding/red, +/obj/effect/turf_decal/tile/red/half{ + dir = 1 + }, +/obj/machinery/light/red/dim/directional/north, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "fR" = ( /obj/structure/lattice/catwalk, /obj/machinery/atmospherics/pipe/heat_exchanging/junction, @@ -554,6 +566,11 @@ "fT" = ( /turf/open/floor/engine/bz, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"fU" = ( +/obj/structure/extinguisher_cabinet/directional/west, +/obj/effect/turf_decal/tile/red/half, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "fV" = ( /obj/structure/frame/computer{ dir = 1 @@ -579,36 +596,24 @@ /obj/machinery/atmospherics/pipe/heat_exchanging/junction/layer2, /turf/closed/indestructible/syndicate, /area/centcom/syndicate_mothership/expansion_bombthreat) -"go" = ( -/obj/machinery/light/cold/directional/east, -/obj/machinery/vending/snack/teal, +"gs" = ( +/obj/structure/extinguisher_cabinet/directional/west, +/turf/open/floor/mineral/titanium, +/area/centcom/syndicate_mothership/control) +"gu" = ( +/obj/structure/chair/sofa/bench/right{ + dir = 4 + }, /obj/structure/cable, /obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, /obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/turf/open/floor/catwalk_floor/titanium, -/area/centcom/syndicate_mothership/control) -"gs" = ( -/obj/structure/extinguisher_cabinet/directional/west, -/turf/open/floor/mineral/titanium, +/turf/open/floor/catwalk_floor/iron_dark, /area/centcom/syndicate_mothership/control) "gw" = ( /obj/machinery/light/small/directional/west, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) -"gB" = ( -/obj/effect/turf_decal/siding/wideplating/dark{ - dir = 8 - }, -/obj/effect/turf_decal/siding/wideplating/dark{ - dir = 4 - }, -/obj/machinery/door/airlock/public/glass{ - name = "War Room" - }, -/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, -/turf/open/floor/iron/textured_large, -/area/centcom/syndicate_mothership/control) "gD" = ( /obj/structure/flora/tree/dead/style_random, /obj/structure/flora/grass/both/style_random, @@ -789,21 +794,13 @@ dir = 4 }, /area/centcom/syndicate_mothership/expansion_fridgerummage) -"ij" = ( -/obj/structure/chair/sofa/bench/left{ - dir = 8 +"ih" = ( +/obj/machinery/camera/autoname/directional/west{ + network = list("nukie") }, -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/turf/open/floor/catwalk_floor/titanium, -/area/centcom/syndicate_mothership/control) -"il" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ +/obj/effect/turf_decal/tile/red/half{ dir = 1 }, -/obj/effect/turf_decal/siding/red/corner, /turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "is" = ( @@ -812,6 +809,13 @@ /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"iz" = ( +/obj/effect/turf_decal/siding/red/corner{ + dir = 1 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "iA" = ( /obj/effect/turf_decal/siding/thinplating{ dir = 1 @@ -842,9 +846,11 @@ }, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) -"iV" = ( -/obj/structure/chair/stool/directional/north, -/turf/open/floor/iron/dark/textured_large, +"iU" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 6 + }, +/turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/control) "iX" = ( /obj/machinery/vending/dinnerware, @@ -856,6 +862,16 @@ /obj/machinery/light/cold/directional/west, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"jd" = ( +/obj/structure/chair/stool/directional/south, +/obj/structure/sign/map/left{ + desc = "A framed picture of the station. Clockwise from security at the top (red), you see engineering (yellow), science (purple), escape (red and white), medbay (green), arrivals (blue and white), and finally cargo (brown)."; + icon_state = "map-left-MS"; + pixel_y = 32 + }, +/obj/effect/landmark/start/nukeop_base, +/turf/open/floor/wood/tile, +/area/centcom/syndicate_mothership/control) "je" = ( /obj/effect/turf_decal/siding/wideplating{ dir = 8 @@ -917,6 +933,15 @@ /obj/structure/noticeboard/directional/north, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"kf" = ( +/obj/effect/turf_decal/siding/red/corner{ + dir = 8 + }, +/obj/item/folder/red, +/obj/item/pen/red, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "kg" = ( /turf/open/misc/ice/icemoon, /area/centcom/syndicate_mothership/control) @@ -1022,12 +1047,6 @@ }, /turf/open/lava/plasma/ice_moon, /area/centcom/syndicate_mothership/control) -"lc" = ( -/obj/effect/turf_decal/siding/red/corner{ - dir = 4 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "ld" = ( /turf/open/floor/iron/smooth_edge{ dir = 8 @@ -1047,13 +1066,6 @@ /obj/structure/flora/grass/both/style_random, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"lo" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/machinery/camera/autoname/directional/south{ - network = list("nukie") - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "lt" = ( /obj/effect/turf_decal/siding/thinplating_new/dark, /turf/open/floor/mineral/plastitanium, @@ -1125,18 +1137,6 @@ /obj/machinery/chem_dispenser/mutagensaltpeter, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) -"my" = ( -/obj/effect/landmark/start/nukeop_base/leader, -/obj/effect/turf_decal/tile/bar/opposingcorners, -/turf/open/floor/iron, -/area/centcom/syndicate_mothership/control) -"mz" = ( -/obj/machinery/vending/cola, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 4 - }, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "mA" = ( /obj/effect/turf_decal/stripes/line{ dir = 10 @@ -1152,6 +1152,10 @@ }, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bombthreat) +"mC" = ( +/obj/effect/turf_decal/tile/red/half, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "mJ" = ( /obj/structure/sign/poster/contraband/free_key, /turf/closed/indestructible/syndicate, @@ -1176,19 +1180,6 @@ }, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) -"ng" = ( -/obj/effect/turf_decal/delivery, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 1 - }, -/obj/structure/fans/tiny, -/obj/machinery/door/poddoor/shutters/syndicate{ - id = "FBBZ1"; - name = "Security Shutters"; - dir = 1 - }, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "nk" = ( /obj/structure/flora/tree/dead/style_random, /obj/structure/flora/grass/both/style_random, @@ -1217,14 +1208,14 @@ /obj/structure/flora/tree/pine/style_random, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"nD" = ( -/obj/effect/turf_decal/stripes/full, -/turf/open/floor/mineral/titanium/yellow, -/area/centcom/syndicate_mothership/control) "nF" = ( /obj/effect/turf_decal/syndicateemblem/top/middle, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/control) +"nG" = ( +/obj/machinery/vending/cigarette/syndicate, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "nH" = ( /obj/effect/turf_decal/siding/thinplating_new/dark, /obj/structure/closet/syndicate/personal, @@ -1234,6 +1225,10 @@ dir = 8 }, /area/centcom/syndicate_mothership/control) +"nI" = ( +/obj/machinery/minimap_table, +/turf/open/floor/circuit/red/no_power, +/area/centcom/syndicate_mothership/control) "nL" = ( /obj/structure/chair/office/light{ dir = 1 @@ -1255,6 +1250,12 @@ /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"nV" = ( +/obj/structure/chair/stool/directional/north, +/obj/effect/landmark/start/nukeop_base, +/obj/structure/sign/poster/contraband/donk_co/directional/south, +/turf/open/floor/wood/tile, +/area/centcom/syndicate_mothership/control) "oc" = ( /obj/structure/fence{ dir = 4 @@ -1270,13 +1271,6 @@ }, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bombthreat) -"oh" = ( -/obj/structure/table/reinforced, -/obj/item/syndicatedetonator{ - desc = "This gaudy button can be used to instantly detonate syndicate bombs that have been activated on the station. It is also fun to press." - }, -/turf/open/floor/carpet, -/area/centcom/syndicate_mothership/control) "oi" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 10 @@ -1288,16 +1282,12 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, /turf/open/floor/catwalk_floor/iron_dark, /area/centcom/syndicate_mothership/control) -"or" = ( -/obj/effect/turf_decal/siding/wideplating/dark{ - dir = 1 - }, -/obj/effect/turf_decal/siding/wideplating/dark, -/obj/machinery/door/airlock/hatch{ - name = "Gangway" +"om" = ( +/obj/structure/chair/sofa/bench/left{ + dir = 4 }, -/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, -/turf/open/floor/iron, +/obj/structure/sign/poster/contraband/smoke/directional/north, +/turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "os" = ( /obj/effect/turf_decal/stripes/box, @@ -1314,10 +1304,19 @@ }, /turf/open/floor/engine/vacuum, /area/centcom/syndicate_mothership/expansion_bombthreat) -"oD" = ( +"oA" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 10 + }, +/turf/open/floor/mineral/plastitanium/red, +/area/centcom/syndicate_mothership/control) +"oH" = ( /obj/effect/turf_decal/siding/red{ dir = 1 }, +/obj/item/folder/red, +/obj/item/pen/red, +/obj/structure/table/reinforced/plastitaniumglass, /turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "oK" = ( @@ -1356,6 +1355,14 @@ }, /turf/open/floor/engine, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"oU" = ( +/obj/effect/turf_decal/siding/red{ + dir = 1 + }, +/obj/machinery/recharger, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "oW" = ( /obj/effect/turf_decal/siding/thinplating{ dir = 9 @@ -1402,11 +1409,11 @@ /obj/structure/flora/rock/icy/style_random, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"pr" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ +"pp" = ( +/obj/effect/turf_decal/siding/red, +/obj/effect/turf_decal/tile/red/half{ dir = 1 }, -/obj/effect/turf_decal/siding/red, /turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "ps" = ( @@ -1438,13 +1445,6 @@ /obj/machinery/vending/tool, /turf/open/floor/mineral/titanium/yellow, /area/centcom/syndicate_mothership/control) -"pS" = ( -/obj/structure/chair/greyscale{ - dir = 4 - }, -/obj/effect/landmark/start/nukeop_base/overwatch, -/turf/open/floor/mineral/plastitanium, -/area/centcom/syndicate_mothership) "pU" = ( /obj/structure/chair/office/light{ dir = 8 @@ -1515,10 +1515,6 @@ }, /turf/open/floor/plastic, /area/centcom/syndicate_mothership/expansion_fridgerummage) -"qv" = ( -/obj/item/kirbyplants/random, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "qw" = ( /turf/open/floor/plating, /area/centcom/syndicate_mothership/control) @@ -1583,25 +1579,28 @@ }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"qU" = ( -/obj/structure/flora/rock/icy/style_random{ - pixel_x = -7 +"qR" = ( +/obj/effect/turf_decal/siding/wideplating{ + dir = 1 + }, +/obj/effect/turf_decal/siding/wideplating, +/obj/machinery/door/poddoor/shutters/syndicate/indestructible{ + name = "Base Lift"; + dir = 1; + id_tag = "nukiespawnlift"; + id = "nukiespawnlift" + }, +/turf/open/floor/iron/dark/textured_half, +/area/centcom/syndicate_mothership/control) +"qU" = ( +/obj/structure/flora/rock/icy/style_random{ + pixel_x = -7 }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) "qX" = ( /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) -"rb" = ( -/obj/effect/turf_decal/siding/red, -/obj/structure/table/reinforced, -/obj/machinery/recharger, -/obj/item/stack/spacecash/c10{ - pixel_x = -19; - pixel_y = 10 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "rf" = ( /turf/open/floor/iron/stairs/old, /area/centcom/syndicate_mothership/control) @@ -1623,25 +1622,6 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /turf/open/floor/iron/smooth, /area/centcom/syndicate_mothership/control) -"ru" = ( -/obj/effect/turf_decal/siding/wideplating{ - dir = 1 - }, -/obj/effect/turf_decal/siding/wideplating, -/obj/machinery/door/poddoor/shutters/syndicate/indestructible{ - name = "Base Lift"; - dir = 1; - id_tag = "nukiespawnlift"; - id = "nukiespawnlift" - }, -/turf/open/floor/iron/dark/textured_half, -/area/centcom/syndicate_mothership/control) -"rw" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 6 - }, -/turf/open/floor/mineral/plastitanium/red, -/area/centcom/syndicate_mothership/control) "rA" = ( /obj/effect/turf_decal/siding/purple{ dir = 1 @@ -1681,6 +1661,14 @@ /obj/structure/fence/door/opened, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"rO" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark/corner{ + dir = 8 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/obj/item/folder/red, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) "rS" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 5 @@ -1838,20 +1826,6 @@ }, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_bombthreat) -"tu" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red/corner{ - dir = 1 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) -"tv" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red/corner, -/obj/item/folder/red, -/obj/item/pen/red, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "tz" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 @@ -1922,6 +1896,13 @@ /obj/structure/statue/uranium/nuke, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"ux" = ( +/obj/structure/cable, +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/open/floor/catwalk_floor/iron_dark, +/area/centcom/syndicate_mothership/control) "uK" = ( /obj/effect/turf_decal/siding/wideplating{ dir = 1 @@ -1954,6 +1935,14 @@ /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"uY" = ( +/obj/effect/turf_decal/siding/red{ + dir = 1 + }, +/obj/effect/turf_decal/tile/red/half, +/obj/machinery/light/red/dim/directional/south, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "ve" = ( /obj/effect/turf_decal/syndicateemblem/top/middle{ dir = 1 @@ -1974,26 +1963,17 @@ /obj/machinery/portable_atmospherics/canister/carbon_dioxide, /turf/open/floor/plating, /area/centcom/syndicate_mothership/expansion_bombthreat) -"vv" = ( -/obj/structure/table/reinforced, -/obj/item/paper/fluff/stations/centcom/disk_memo{ - pixel_x = -6; - pixel_y = -7 - }, -/obj/item/taperecorder{ - pixel_y = 15 +"vn" = ( +/obj/effect/turf_decal/tile/red{ + dir = 1 }, -/obj/item/stack/spacecash/c50, -/turf/open/floor/carpet, +/turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) -"vx" = ( -/obj/machinery/door/airlock/external/ruin, -/obj/effect/mapping_helpers/airlock/cyclelink_helper{ - dir = 8 +"vB" = ( +/obj/effect/turf_decal/tile/red/anticorner{ + dir = 1 }, -/obj/structure/fans/tiny, -/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, -/turf/open/floor/plating, +/turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "vI" = ( /obj/structure/railing, @@ -2038,16 +2018,6 @@ /obj/structure/filingcabinet/medical, /turf/open/floor/carpet, /area/centcom/syndicate_mothership/control) -"wc" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) -"we" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 5 - }, -/turf/open/floor/mineral/plastitanium/red, -/area/centcom/syndicate_mothership/control) "wg" = ( /obj/structure/fence/corner{ dir = 6 @@ -2077,6 +2047,16 @@ }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) +"ww" = ( +/obj/structure/chair/sofa/bench/left{ + dir = 8 + }, +/obj/structure/cable, +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/open/floor/catwalk_floor/iron_dark, +/area/centcom/syndicate_mothership/control) "wC" = ( /obj/structure/chair/sofa/bench/right{ dir = 4 @@ -2084,6 +2064,15 @@ /obj/structure/sign/poster/contraband/rebels_unite/directional/south, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) +"wD" = ( +/obj/structure/chair/sofa/bench/left{ + dir = 4 + }, +/obj/machinery/camera/autoname/directional/north{ + network = list("nukie") + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "wG" = ( /turf/open/floor/iron/smooth_half{ dir = 1 @@ -2112,10 +2101,6 @@ dir = 8 }, /area/centcom/syndicate_mothership/control) -"wS" = ( -/obj/effect/landmark/nukeop_elevator/exterior, -/turf/closed/indestructible/syndicate, -/area/centcom/syndicate_mothership/control) "wW" = ( /obj/structure/flora/rock/pile/style_random, /obj/effect/light_emitter{ @@ -2124,6 +2109,10 @@ }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) +"wX" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark/corner, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) "wY" = ( /obj/machinery/atmospherics/components/unary/outlet_injector/monitored{ desc = "Has a valve and pump attached to it. Slightly more menacing than Nanotrasen's standard."; @@ -2150,11 +2139,18 @@ /obj/effect/turf_decal/siding/thinplating_new/dark/corner, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) -"xj" = ( -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/turf/open/floor/mineral/titanium, +"xt" = ( +/obj/structure/table/reinforced/plastitaniumglass, +/obj/effect/turf_decal/delivery, +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 1 + }, +/obj/structure/fans/tiny, +/obj/effect/turf_decal/tile/bar/opposingcorners, +/obj/machinery/door/poddoor/shutters/syndicate{ + dir = 1 + }, +/turf/open/floor/iron, /area/centcom/syndicate_mothership/control) "xu" = ( /turf/closed/indestructible/syndicate, @@ -2177,6 +2173,12 @@ "ya" = ( /turf/open/floor/carpet, /area/centcom/syndicate_mothership/control) +"yb" = ( +/obj/effect/turf_decal/tile/red/half{ + dir = 1 + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "ye" = ( /obj/effect/turf_decal/weather/snow/corner{ dir = 10 @@ -2260,6 +2262,10 @@ /obj/structure/chair/office, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) +"zl" = ( +/obj/machinery/light/small/red/directional/west, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "zo" = ( /obj/item/stack/sheet/mineral/sandbags, /turf/open/floor/catwalk_floor/iron_dark, @@ -2290,11 +2296,14 @@ }, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) -"zH" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/item/kirbyplants/random, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) +"zI" = ( +/obj/effect/light_emitter{ + set_cap = 1; + set_luminosity = 4 + }, +/obj/structure/flora/tree/dead/style_random, +/turf/closed/indestructible/rock/snow, +/area/centcom/syndicate_mothership) "zL" = ( /obj/structure/rack, /obj/item/restraints/handcuffs/cable/pink, @@ -2395,6 +2404,10 @@ }, /turf/open/floor/plating, /area/centcom/syndicate_mothership/expansion_bombthreat) +"AE" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) "AL" = ( /obj/machinery/light/cold/directional/south, /turf/open/floor/iron/smooth_half{ @@ -2404,29 +2417,6 @@ "AM" = ( /turf/closed/indestructible/iron, /area/centcom/syndicate_mothership/control) -"AN" = ( -/obj/structure/table/reinforced/plastitaniumglass, -/obj/effect/turf_decal/delivery, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 1 - }, -/obj/structure/fans/tiny, -/obj/effect/turf_decal/tile/bar/opposingcorners, -/obj/machinery/door/poddoor/shutters/syndicate{ - dir = 1 - }, -/turf/open/floor/iron, -/area/centcom/syndicate_mothership/control) -"AO" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red/corner{ - dir = 4 - }, -/obj/item/stack/spacecash/c1{ - pixel_y = 12 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "AR" = ( /obj/effect/baseturf_helper/asteroid/snow, /turf/closed/indestructible/syndicate, @@ -2436,16 +2426,6 @@ /obj/structure/flora/grass/both/style_random, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) -"AV" = ( -/obj/structure/chair/stool/directional/south, -/obj/structure/sign/map/left{ - desc = "A framed picture of the station. Clockwise from security at the top (red), you see engineering (yellow), science (purple), escape (red and white), medbay (green), arrivals (blue and white), and finally cargo (brown)."; - icon_state = "map-left-MS"; - pixel_y = 32 - }, -/obj/effect/landmark/start/nukeop_base, -/turf/open/floor/wood/tile, -/area/centcom/syndicate_mothership/control) "AW" = ( /obj/machinery/chem_master, /turf/open/floor/mineral/titanium/tiled/yellow, @@ -2577,6 +2557,17 @@ }, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) +"Cn" = ( +/obj/structure/chair/sofa/bench{ + dir = 4 + }, +/obj/machinery/light/cold/directional/west, +/obj/structure/cable, +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/open/floor/catwalk_floor/iron_dark, +/area/centcom/syndicate_mothership/control) "Ct" = ( /obj/machinery/door/airlock/hatch{ name = "Closet" @@ -2631,6 +2622,15 @@ }, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) +"CW" = ( +/obj/effect/turf_decal/siding/red{ + dir = 4 + }, +/obj/item/paper_bin, +/obj/item/pen, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "CX" = ( /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership) @@ -2648,6 +2648,16 @@ /obj/structure/sign/poster/contraband/gorlex_recruitment/directional/west, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) +"Dt" = ( +/obj/effect/turf_decal/siding/red/corner{ + dir = 4 + }, +/obj/item/stack/spacecash/c1{ + pixel_y = 12 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Du" = ( /obj/docking_port/stationary{ area_type = /area/centcom/syndicate_mothership; @@ -2685,6 +2695,10 @@ }, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) +"DQ" = ( +/obj/effect/landmark/nukeop_elevator/exterior, +/turf/closed/indestructible/syndicate, +/area/centcom/syndicate_mothership/control) "DV" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 @@ -2729,13 +2743,6 @@ "DZ" = ( /turf/closed/indestructible/syndicate, /area/centcom/syndicate_mothership/control) -"Ed" = ( -/obj/structure/chair/sofa/bench/left{ - dir = 4 - }, -/obj/structure/sign/poster/contraband/smoke/directional/north, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "Ef" = ( /obj/structure/fence, /turf/open/misc/asteroid/snow/airless, @@ -2799,23 +2806,16 @@ /obj/effect/turf_decal/siding/purple, /turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) -"Fm" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red/corner{ - dir = 8 - }, -/obj/item/folder/red, -/obj/item/pen/red, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) -"Fp" = ( -/obj/machinery/vending/cigarette/syndicate, -/turf/open/floor/mineral/titanium, +"Fl" = ( +/obj/structure/chair/stool/directional/west, +/obj/effect/landmark/start/nukeop_base, +/turf/open/floor/wood/tile, /area/centcom/syndicate_mothership/control) -"Fq" = ( -/obj/structure/table/reinforced, -/obj/item/flashlight/lamp, -/turf/open/floor/carpet, +"Fo" = ( +/obj/machinery/light/small/red/directional/north{ + allow_break_on_init = 0 + }, +/turf/open/floor/mineral/plastitanium, /area/centcom/syndicate_mothership/control) "FB" = ( /obj/machinery/atmospherics/components/unary/portables_connector/visible{ @@ -2826,6 +2826,25 @@ }, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_bombthreat) +"FC" = ( +/obj/item/paper/fluff/stations/centcom/disk_memo{ + pixel_x = -6; + pixel_y = -7 + }, +/obj/item/taperecorder{ + pixel_y = 15 + }, +/obj/item/stack/spacecash/c50, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/carpet, +/area/centcom/syndicate_mothership/control) +"FE" = ( +/obj/item/syndicatedetonator{ + desc = "This gaudy button can be used to instantly detonate syndicate bombs that have been activated on the station. It is also fun to press." + }, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/carpet, +/area/centcom/syndicate_mothership/control) "FG" = ( /obj/effect/turf_decal/stripes/corner, /obj/effect/turf_decal/stripes/corner{ @@ -2847,6 +2866,12 @@ /obj/machinery/atmospherics/pipe/heat_exchanging/simple, /turf/closed/indestructible/syndicate, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"FQ" = ( +/obj/effect/turf_decal/tile/red{ + dir = 4 + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "FR" = ( /obj/machinery/light/floor, /turf/open/floor/iron/dark/textured_half{ @@ -2991,17 +3016,6 @@ dir = 8 }, /area/centcom/syndicate_mothership/control) -"GI" = ( -/obj/structure/chair/sofa/bench{ - dir = 4 - }, -/obj/machinery/light/cold/directional/west, -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/turf/open/floor/catwalk_floor/titanium, -/area/centcom/syndicate_mothership/control) "GL" = ( /obj/structure/lattice/catwalk, /obj/effect/turf_decal/stripes/line{ @@ -3057,10 +3071,6 @@ }, /turf/open/misc/ice/icemoon, /area/centcom/syndicate_mothership/control) -"Hs" = ( -/obj/machinery/shuttle_manipulator, -/turf/open/floor/circuit/red, -/area/centcom/syndicate_mothership/control) "Ht" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 @@ -3114,12 +3124,26 @@ /obj/item/stack/sheet/mineral/plasma, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"HE" = ( +/obj/effect/landmark/start/nukeop_base/leader, +/obj/effect/turf_decal/tile/bar/opposingcorners, +/turf/open/floor/iron, +/area/centcom/syndicate_mothership/control) "HJ" = ( /obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) +"HN" = ( +/obj/machinery/vending/coffee, +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 4 + }, +/turf/open/floor/iron/white/textured_large{ + color = "#b51026" + }, +/area/centcom/syndicate_mothership/control) "HW" = ( /obj/effect/light_emitter{ set_cap = 1; @@ -3140,6 +3164,17 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, /turf/open/floor/catwalk_floor/iron_dark, /area/centcom/syndicate_mothership/control) +"Ic" = ( +/obj/effect/mapping_helpers/airlock/cyclelink_helper{ + dir = 8 + }, +/obj/structure/fans/tiny, +/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, +/obj/machinery/door/airlock/hatch{ + name = "Security Checkpoint" + }, +/turf/open/floor/plating, +/area/centcom/syndicate_mothership/control) "Id" = ( /obj/structure/closet/crate/freezer{ name = "meat freezer" @@ -3204,6 +3239,11 @@ }, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"IA" = ( +/obj/item/flashlight/lamp, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/carpet, +/area/centcom/syndicate_mothership/control) "IC" = ( /obj/structure/chair/stool/bar/directional/west, /obj/effect/turf_decal/siding/thinplating/dark{ @@ -3212,41 +3252,12 @@ /obj/effect/turf_decal/tile/bar/opposingcorners, /turf/open/floor/iron, /area/centcom/syndicate_mothership/control) -"IL" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red, -/obj/item/toy/nuke{ - pixel_x = -5; - pixel_y = 1 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) -"IM" = ( -/obj/structure/chair/sofa/bench/right{ - dir = 4 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark, -/turf/open/floor/mineral/titanium, -/area/centcom/syndicate_mothership/control) "IQ" = ( /obj/structure/cable, /obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) -"IV" = ( -/obj/structure/chair/stool/directional/south, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 1 - }, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) -"Ja" = ( -/obj/machinery/light/small/red/directional/north{ - allow_break_on_init = 0 - }, -/turf/open/floor/mineral/plastitanium, -/area/centcom/syndicate_mothership/control) "Jc" = ( /obj/structure/table/wood, /obj/machinery/chem_dispenser/drinks/beer{ @@ -3282,6 +3293,10 @@ }, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"Jr" = ( +/obj/effect/turf_decal/stripes/full, +/turf/open/floor/mineral/titanium, +/area/centcom/syndicate_mothership/control) "Js" = ( /obj/structure/table/wood, /obj/item/rag, @@ -3298,6 +3313,19 @@ }, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_bombthreat) +"JE" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 9 + }, +/turf/open/floor/mineral/plastitanium/red, +/area/centcom/syndicate_mothership/control) +"JG" = ( +/obj/structure/chair/comfy/black{ + dir = 8 + }, +/obj/effect/turf_decal/tile/red/full, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "JJ" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 5 @@ -3341,6 +3369,13 @@ /obj/effect/turf_decal/syndicateemblem/top/left, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/control) +"JY" = ( +/obj/effect/turf_decal/siding/red/corner, +/obj/effect/turf_decal/tile/red/half{ + dir = 1 + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Ka" = ( /obj/structure/chair/sofa/bench/right{ dir = 4 @@ -3361,6 +3396,12 @@ /obj/structure/flora/grass/both/style_random, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"KB" = ( +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/open/floor/iron/smooth_large, +/area/centcom/syndicate_mothership/control) "KD" = ( /obj/effect/turf_decal/trimline/red, /obj/effect/turf_decal/trimline/red, @@ -3410,6 +3451,14 @@ }, /turf/open/lava/plasma/ice_moon, /area/centcom/syndicate_mothership/control) +"KT" = ( +/obj/effect/turf_decal/stripes/end{ + dir = 4 + }, +/obj/machinery/mech_bay_recharge_port, +/obj/machinery/light/red/dim/directional/south, +/turf/open/floor/mineral/titanium, +/area/centcom/syndicate_mothership/control) "KU" = ( /obj/effect/light_emitter{ set_cap = 1; @@ -3426,18 +3475,24 @@ }, /turf/open/floor/iron/smooth_half, /area/centcom/syndicate_mothership/control) +"KX" = ( +/obj/effect/turf_decal/siding/red{ + dir = 8 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Ld" = ( /obj/effect/turf_decal/siding/thinplating_new/dark, /obj/structure/closet/syndicate/personal, /obj/effect/turf_decal/tile/red/full, /turf/open/floor/iron/dark/textured_half, /area/centcom/syndicate_mothership/control) -"Lk" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red{ - dir = 8 +"Le" = ( +/obj/structure/chair/comfy/black{ + dir = 4 }, -/obj/item/storage/fancy/donut_box, +/obj/effect/turf_decal/tile/red/full, /turf/open/floor/iron/dark/textured_large, /area/centcom/syndicate_mothership/control) "Lu" = ( @@ -3473,15 +3528,6 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /turf/open/floor/stone, /area/centcom/syndicate_mothership/control) -"LB" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red{ - dir = 1 - }, -/obj/item/folder/red, -/obj/item/pen/red, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "LH" = ( /obj/effect/turf_decal/stripes/line, /obj/machinery/atmospherics/components/binary/pump/on, @@ -3530,13 +3576,6 @@ }, /turf/open/floor/plating/snowed/icemoon, /area/centcom/syndicate_mothership/control) -"Me" = ( -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/turf/open/floor/catwalk_floor/titanium, -/area/centcom/syndicate_mothership/control) "Mh" = ( /obj/structure/sign/poster/contraband/lamarr/directional/south, /turf/open/floor/iron/smooth_half{ @@ -3608,6 +3647,12 @@ /obj/effect/mapping_helpers/airlock/access/all/syndicate/general, /turf/open/floor/catwalk_floor/iron, /area/centcom/syndicate_mothership/control) +"MF" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 4 + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "MH" = ( /obj/machinery/hydroponics/constructable, /obj/machinery/light/cold/directional/west, @@ -3632,14 +3677,6 @@ dir = 8 }, /area/centcom/syndicate_mothership/control) -"MU" = ( -/obj/effect/turf_decal/siding/wideplating/dark{ - dir = 8 - }, -/obj/structure/chair/stool/directional/east, -/obj/effect/landmark/start/nukeop_base, -/turf/open/floor/wood/tile, -/area/centcom/syndicate_mothership/control) "Nb" = ( /obj/structure/chair/sofa/right/brown{ dir = 4 @@ -3653,6 +3690,13 @@ name = "Tac-Com" }, /area/centcom/syndicate_mothership/control) +"Ne" = ( +/obj/effect/turf_decal/siding/red/corner{ + dir = 4 + }, +/obj/effect/turf_decal/tile/red/half, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Ni" = ( /obj/machinery/light/small/directional/north, /obj/machinery/computer/slot_machine/syndicate, @@ -3734,6 +3778,13 @@ /obj/item/melee/powerfist, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bombthreat) +"Og" = ( +/obj/structure/chair/sofa/bench/right{ + dir = 4 + }, +/obj/effect/turf_decal/siding/thinplating_new/dark, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Oi" = ( /obj/structure/cable, /turf/closed/indestructible/syndicate, @@ -3858,6 +3909,24 @@ "Ph" = ( /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"Pp" = ( +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/turf/open/floor/iron/smooth_large, +/area/centcom/syndicate_mothership/control) +"Ps" = ( +/obj/effect/turf_decal/delivery, +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 1 + }, +/obj/structure/fans/tiny, +/obj/machinery/door/poddoor/shutters/syndicate{ + id = "FBBZ1"; + name = "Security Shutters"; + dir = 1 + }, +/turf/open/floor/mineral/titanium, +/area/centcom/syndicate_mothership/control) "Pu" = ( /obj/effect/turf_decal/stripes/line{ dir = 9 @@ -4057,6 +4126,13 @@ /obj/structure/cable, /turf/open/floor/catwalk_floor/iron, /area/centcom/syndicate_mothership/control) +"Re" = ( +/obj/effect/turf_decal/siding/red{ + dir = 1 + }, +/obj/effect/turf_decal/tile/red/half, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Rn" = ( /obj/item/circuitboard/computer/syndicate_shuttle{ pixel_x = 2; @@ -4132,6 +4208,14 @@ /obj/effect/baseturf_helper/asteroid/snow, /turf/closed/indestructible/syndicate, /area/centcom/syndicate_mothership/expansion_bioterrorism) +"RV" = ( +/obj/effect/turf_decal/siding/wideplating/dark{ + dir = 8 + }, +/obj/structure/chair/stool/directional/east, +/obj/effect/landmark/start/nukeop_base, +/turf/open/floor/wood/tile, +/area/centcom/syndicate_mothership/control) "Sc" = ( /obj/structure/railing, /turf/open/floor/iron/stairs/old{ @@ -4139,18 +4223,6 @@ initial_gas_mix = "TEMP=2.7" }, /area/centcom/syndicate_mothership) -"Se" = ( -/obj/machinery/door/airlock/external/ruin, -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/obj/effect/mapping_helpers/airlock/cyclelink_helper{ - dir = 4 - }, -/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, -/turf/open/floor/plating, -/area/centcom/syndicate_mothership/control) "Sg" = ( /obj/machinery/camera/autoname/directional/east{ network = list("nukie") @@ -4210,6 +4282,11 @@ /obj/item/mop, /turf/open/floor/catwalk_floor/iron_smooth, /area/centcom/syndicate_mothership/control) +"Sz" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark, +/obj/structure/sign/poster/contraband/cybersun_six_hundred/directional/east, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "SD" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 @@ -4217,6 +4294,12 @@ /obj/machinery/portable_atmospherics/pump, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/expansion_bombthreat) +"SH" = ( +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, +/turf/closed/indestructible/opsglass, +/area/centcom/syndicate_mothership/control) "SJ" = ( /obj/machinery/atmospherics/components/trinary/filter{ dir = 8 @@ -4242,6 +4325,12 @@ /obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /turf/closed/indestructible/opsglass, /area/centcom/syndicate_mothership/control) +"SN" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 6 + }, +/turf/open/floor/iron/smooth_half, +/area/centcom/syndicate_mothership/control) "SR" = ( /obj/structure/railing, /obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, @@ -4352,12 +4441,6 @@ }, /turf/open/floor/wood/tile, /area/centcom/syndicate_mothership/control) -"TH" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 10 - }, -/turf/open/floor/mineral/plastitanium/red, -/area/centcom/syndicate_mothership/control) "TK" = ( /obj/structure/barricade/sandbags, /obj/effect/light_emitter{ @@ -4405,6 +4488,12 @@ }, /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/control) +"Uc" = ( +/obj/effect/turf_decal/tile/red/anticorner{ + dir = 4 + }, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Um" = ( /obj/machinery/button/door/directional/south{ id = "syn_ordmix_vent"; @@ -4457,10 +4546,28 @@ }, /turf/open/floor/plating/icemoon, /area/centcom/syndicate_mothership/control) +"UC" = ( +/obj/structure/chair/greyscale{ + dir = 4 + }, +/obj/effect/landmark/start/nukeop_base/overwatch, +/turf/open/floor/mineral/plastitanium, +/area/centcom/syndicate_mothership) "UE" = ( /obj/machinery/light/cold/directional/east, /turf/open/floor/plating, /area/centcom/syndicate_mothership/control) +"UH" = ( +/obj/effect/turf_decal/siding/wideplating/dark{ + dir = 1 + }, +/obj/effect/turf_decal/siding/wideplating/dark, +/obj/machinery/door/airlock/hatch{ + name = "Gangway" + }, +/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, +/turf/open/floor/mineral/plastitanium, +/area/centcom/syndicate_mothership/control) "Va" = ( /obj/effect/turf_decal/syndicateemblem/middle/left{ dir = 8 @@ -4481,6 +4588,13 @@ /obj/structure/flora/rock/pile/style_random, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) +"Vq" = ( +/obj/structure/chair/sofa/bench/right{ + dir = 8 + }, +/obj/structure/sign/poster/contraband/syndicate_pistol/directional/north, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "Vr" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 5 @@ -4545,12 +4659,15 @@ /obj/item/stack/sheet/mineral/silver/fifty, /turf/open/floor/mineral/titanium/tiled/yellow, /area/centcom/syndicate_mothership/expansion_chemicalwarfare) -"Wc" = ( -/obj/machinery/vending/coffee, +"Wb" = ( +/obj/structure/cable, +/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, +/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, +/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 4 }, -/turf/open/floor/mineral/titanium, +/turf/open/floor/catwalk_floor/iron_dark, /area/centcom/syndicate_mothership/control) "Wo" = ( /obj/effect/light_emitter{ @@ -4562,16 +4679,6 @@ "Wp" = ( /turf/open/floor/iron/smooth, /area/centcom/syndicate_mothership/control) -"Wr" = ( -/obj/structure/chair/sofa/bench/right{ - dir = 4 - }, -/obj/structure/cable, -/obj/machinery/atmospherics/pipe/smart/manifold4w/general/hidden, -/obj/machinery/atmospherics/pipe/smart/manifold4w/yellow/hidden/layer2, -/obj/machinery/atmospherics/pipe/smart/manifold4w/orange/hidden/layer5, -/turf/open/floor/catwalk_floor/titanium, -/area/centcom/syndicate_mothership/control) "Ws" = ( /obj/structure/flora/tree/pine/style_random, /obj/effect/light_emitter{ @@ -4580,9 +4687,16 @@ }, /turf/open/misc/asteroid/snow/airless, /area/centcom/syndicate_mothership) -"Wu" = ( -/obj/structure/extinguisher_cabinet/directional/west, -/turf/open/floor/iron/dark/textured_large, +"Wt" = ( +/obj/structure/cable, +/obj/effect/mapping_helpers/airlock/cyclelink_helper{ + dir = 4 + }, +/obj/effect/mapping_helpers/airlock/access/all/syndicate/general, +/obj/machinery/door/airlock/hatch{ + name = "Security Checkpoint" + }, +/turf/open/floor/plating, /area/centcom/syndicate_mothership/control) "Wz" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ @@ -4609,25 +4723,18 @@ }, /turf/open/misc/asteroid/snow/icemoon, /area/centcom/syndicate_mothership/control) -"WG" = ( -/obj/structure/chair/sofa/bench/right{ - dir = 8 +"WD" = ( +/obj/effect/turf_decal/siding/thinplating_new/dark{ + dir = 5 }, -/obj/structure/sign/poster/contraband/syndicate_pistol/directional/north, -/turf/open/floor/mineral/titanium, +/turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership/control) "WI" = ( /obj/structure/sign/poster/contraband/revolver/directional/south, /turf/open/floor/mineral/titanium, /area/centcom/syndicate_mothership/control) -"WR" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red{ - dir = 4 - }, -/obj/item/paper_bin, -/obj/item/pen, -/turf/open/floor/iron/dark/textured_large, +"WQ" = ( +/turf/open/floor/circuit/red/no_power, /area/centcom/syndicate_mothership/control) "WS" = ( /obj/structure/sign/poster/contraband/energy_swords/directional/north, @@ -4652,22 +4759,9 @@ /obj/structure/cable, /turf/open/floor/catwalk_floor/iron_dark, /area/centcom/syndicate_mothership/control) -"Xj" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/red{ - dir = 1 - }, -/obj/machinery/recharger, -/turf/open/floor/iron/dark/textured_large, -/area/centcom/syndicate_mothership/control) "Xk" = ( /turf/open/floor/mineral/plastitanium/red, /area/centcom/syndicate_mothership) -"Xu" = ( -/obj/structure/chair/stool/directional/west, -/obj/effect/landmark/start/nukeop_base, -/turf/open/floor/wood/tile, -/area/centcom/syndicate_mothership/control) "Xv" = ( /obj/structure/table/reinforced/plastitaniumglass, /obj/effect/turf_decal/siding/thinplating_new/dark{ @@ -4748,12 +4842,6 @@ dir = 1 }, /area/centcom/syndicate_mothership/control) -"XZ" = ( -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 9 - }, -/turf/open/floor/mineral/plastitanium/red, -/area/centcom/syndicate_mothership/control) "Yd" = ( /obj/machinery/camera/autoname/directional/south{ network = list("nukie") @@ -4795,6 +4883,15 @@ /obj/structure/cable, /turf/open/floor/catwalk_floor/iron_smooth, /area/centcom/syndicate_mothership/control) +"YM" = ( +/obj/effect/turf_decal/siding/red, +/obj/item/toy/nuke{ + pixel_x = -5; + pixel_y = 1 + }, +/obj/structure/table/reinforced/plastitaniumglass, +/turf/open/floor/iron/dark/textured_large, +/area/centcom/syndicate_mothership/control) "YO" = ( /obj/effect/turf_decal/siding/wideplating/dark{ dir = 8 @@ -6512,7 +6609,7 @@ HW wW Kq sv -pS +UC fV Kq CX @@ -6939,7 +7036,7 @@ Pc ZZ DZ gE -my +HE uX Jc ek @@ -7552,7 +7649,7 @@ bF DZ al Gd -MU +RV YO DZ tL @@ -7652,7 +7749,7 @@ VK BD sO DZ -AV +jd mK er TS @@ -7757,7 +7854,7 @@ DZ OS TG jT -du +nV DZ Dy Wp @@ -7857,7 +7954,7 @@ VK GE DZ Ni -Xu +Fl mL BH DZ @@ -9382,16 +9479,16 @@ bW qw nw MP -ng +Ps DZ VK VK VK VK DZ -Ed -GI -IM +om +Cn +Og wG wG Te @@ -9484,16 +9581,16 @@ bW qw qw qw -ng +Ps hb TC FR TC TC hb -ZZ -Me -wc +qN +ux +ov wG AL DZ @@ -9586,20 +9683,20 @@ bW qw qw qw -ng +Ps qJ TC TC FR TC qJ -ZZ -Me -wc +qN +ux +ov wG wG DZ -Ja +Fo Ov DZ DZ @@ -9695,14 +9792,14 @@ Xv KE VK DZ -WG -ij -zH +Vq +ww +OF wG wG -ru -XZ -TH +qR +JE +oA Ov DZ Ox @@ -9797,14 +9894,14 @@ so uX zL DZ -fb -Wr -zH +wD +gu +OF wG wG -ru -we -rw +qR +WD +iU Ov DZ Ox @@ -9892,20 +9989,20 @@ bW qw qw qw -AN +xt uX uX uX uX uX RA -ZZ -Me -wc +qN +ux +ov wG lE DZ -Ja +Fo Ov DZ DZ @@ -9994,19 +10091,19 @@ bW qw qw qw -AN +xt LS Mn uX ZH LN DZ -Fp -go -as +nG +cY +Sz wG wG -wS +DQ DZ DZ DZ @@ -10106,12 +10203,11 @@ DZ DZ qp DZ -gB -gB -DZ -mz -Wc +NY +NY DZ +cC +HN DZ DZ DZ @@ -10119,6 +10215,7 @@ DZ DZ VK VK +VK Ov DZ XC @@ -10208,20 +10305,20 @@ DZ pK pg VK -ZZ -ZZ -dn +wX +SN +ih qN qN -qN -Wu -ov -or +zl +fU +UH zE sl Ov Ov Ov +Ov DZ Ox Ox @@ -10310,14 +10407,14 @@ DZ CM FN VK -ZZ -ZZ -IV -tv -WR -AO -iV -ov +AE +vB +vn +Le +Le +Le +mC +DZ DZ DZ DZ @@ -10412,14 +10509,13 @@ DZ DZ eg tL -bp -ZZ -IV -IL -Hs -Xj -iV -ov +cW +yb +qN +aS +CW +Dt +mC Cg DZ Ox @@ -10428,6 +10524,7 @@ Ox Ox Ox Ox +Ox sq Ox sU @@ -10511,20 +10608,20 @@ Ld ol zo VK -ZZ -ZZ -Me -ZZ -ZZ -IV -rb -au -LB -iV -ov -ah +qN +qN +ux +AE +yb +qN +YM +nI +oU +mC +KT DZ Ox +Ox sq sq Ox @@ -10613,18 +10710,17 @@ Tc Ib Ib en -xj -xj -Me -ZZ -ZZ -IV -Fm -Lk -tu -iV -ov -nD +Pp +Pp +KB +AE +yb +qN +eM +WQ +oH +mC +Jr DZ Ox Ox @@ -10633,6 +10729,7 @@ Ox Ox Ox Ox +Ox sU sU sU @@ -10715,21 +10812,21 @@ Ld XL RQ VK -ZZ -ZZ -Me -ZZ -ZZ -il -dV -dV -dV -lc -lo +MF +MF +Wb +AE +yb +qN +kf +KX +iz +mC ec DZ DZ Ox +Ox sU sU sU @@ -10817,21 +10914,21 @@ DZ DZ DZ DZ -DZ -VK -Se VK -ZZ -pr -oh -vv -Fq -oD -ov +Wt +SH +cW +Uc +FQ +JG +JG +JG +mC Nd gf DZ Ox +Ox sU sU sU @@ -10919,21 +11016,21 @@ DZ tJ HW HW -Zt VK qw VK -qv -pr -ya -PF -ya -oD -OF +rO +fr +JY +dV +dV +dV +Ne DZ DZ DZ Ox +Ox sU sU sU @@ -11021,21 +11118,21 @@ QM HW HW lj -HW VK qw VK DZ -VK -wb -wq -dw -VK -DZ +bS +fP +FE +FC +IA +uY DZ Ox Ox Ox +Ox sU sU sU @@ -11123,20 +11220,20 @@ HW HW HW HW -tJ VK Hv VK -Ho -VK -VK -VK -VK -VK -Zt -KU +uT +bS +pp +ya +PF +ya +Re +DZ Ye uT +Ox Ye sU sU @@ -11225,20 +11322,20 @@ nz Zt HW HW -HW VK qw VK -HW -QM -HW -Zt -HW -HW -lj -KU +uT +DZ +VK +wb +wq +dB +VK +DZ Ye uT +Ox Ye sU sU @@ -11327,18 +11424,18 @@ HW HW HW QM -Zt VK qw VK +uT Zt +VK +VK +VK +VK +VK HW -HW -HW -tJ -HW -HW -KU +zI KU Ye Ye @@ -11429,10 +11526,10 @@ uT uT uT uT -HW VK -vx +Ic VK +uT HW tJ HW @@ -11532,11 +11629,11 @@ uT uT uT uT -uT Du uT uT uT +uT HW QM HW diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm index bed1a6b24ace..1b71b003872b 100644 --- a/code/__DEFINES/hud.dm +++ b/code/__DEFINES/hud.dm @@ -169,6 +169,19 @@ #define HUD_XENOBIO_CONSOLE "xenobio_console" +#define HUD_TAC_MINIMAP "tac_minimap" +#define HUD_TAC_MINIMAP_DIMMER "tac_minimap_dimmer" +#define HUD_TAC_MINIMAP_Z_INDICATOR "tac_minimap_z_indicator" +#define HUD_TAC_MINIMAP_Z_INDICATOR_UP "tac_minimap_z_up" +#define HUD_TAC_MINIMAP_Z_INDICATOR_DOWN "tac_minimap_z_down" +#define HUD_TAC_MINIMAP_TOOL_RED "tac_minimap_tool_red" +#define HUD_TAC_MINIMAP_TOOL_YELLOW "tac_minimap_tool_yellow" +#define HUD_TAC_MINIMAP_TOOL_PURPLE "tac_minimap_tool_purple" +#define HUD_TAC_MINIMAP_TOOL_BLUE "tac_minimap_tool_blue" +#define HUD_TAC_MINIMAP_TOOL_ERASE "tac_minimap_tool_erase" +#define HUD_TAC_MINIMAP_TOOL_LABEL "tac_minimap_tool_label" +#define HUD_TAC_MINIMAP_TOOL_CLEAR "tac_minimap_tool_clear" + /* These defines specificy screen locations. For more information, see the byond documentation on the screen_loc var. diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 6bc5b96e0e00..4d7a26858a2b 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -313,6 +313,10 @@ #define CURSE_LAYER 6 #define ECHO_LAYER 7 #define PARRY_LAYER 8 +#define MINIMAP_IMAGE_LAYER 9 +#define MINIMAP_BLIPS_LAYER 10 +#define MINIMAP_LOCATOR_LAYER 11 +#define MINIMAP_LABELS_LAYER 12 #define FOV_EFFECT_LAYER 100 diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 7feed196c755..449a0f7f5f47 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -144,6 +144,7 @@ #define LOG_CATEGORY_GAME_ACCESS "game-access" #define LOG_CATEGORY_GAME_BLOOD_WORM "game-blood-worm" #define LOG_CATEGORY_GAME_EMOTE "game-emote" +#define LOG_CATEGORY_GAME_MINIMAP_DRAWING "game-minimap-drawing" #define LOG_CATEGORY_GAME_INTERNET_REQUEST "game-internet-request" #define LOG_CATEGORY_GAME_OOC "game-ooc" #define LOG_CATEGORY_GAME_PRAYER "game-prayer" diff --git a/code/__DEFINES/minimap.dm b/code/__DEFINES/minimap.dm new file mode 100644 index 000000000000..05629ef5d0b9 --- /dev/null +++ b/code/__DEFINES/minimap.dm @@ -0,0 +1,62 @@ +///Converts the overworld x and y to minimap x and y values +#define MINIMAP_PIXEL_FROM_WORLD(val) (val*2-3) + +//actual size of a users screen in pixels +#define SCREEN_PIXEL_SIZE 480 + +//Drawing tool colors +#define TACMAP_DRAWING_RED "#ff0000" +#define TACMAP_DRAWING_YELLOW "#FFFF00" +#define TACMAP_DRAWING_PURPLE "#A020F0" +#define TACMAP_DRAWING_BLUE "#0000FF" + + +//Turf colours +#define TACMAP_BLACK "#111111d0" +#define TACMAP_SOLID "#ebe5e5ee" +#define TACMAP_DOOR "#451e5eee" +#define TACMAP_WINDOW "#525252d0" +#define TACMAP_FENCE "#8c2294ee" +#define TACMAP_LAVA "#db4206d0" +#define TACMAP_DIRT "#9c906dd0" +#define TACMAP_SHALE "#706955d0" +#define TACMAP_SNOW "#c4e3e9d0" +#define TACMAP_MARS_DIRT "#aa5f44d0" +#define TACMAP_ICE "#93cae0d0" +#define TACMAP_WATER "#94b0d59c" //lower opacity as its really bright + +//Area colours +//Departments +#define TACMAP_AREA_COMMAND COLOR_COMMAND_BLUE +#define TACMAP_AREA_CARGO COLOR_CARGO_BROWN +#define TACMAP_AREA_ENGINEERING COLOR_ENGINEERING_ORANGE +#define TACMAP_AREA_MEDICAL COLOR_MEDICAL_BLUE +#define TACMAP_AREA_SCIENCE COLOR_SCIENCE_PINK +#define TACMAP_AREA_SECURITY COLOR_SECURITY_RED +#define TACMAP_AREA_SERVICE COLOR_SERVICE_LIME +//General +#define TACMAP_AREA_MAINTENANCE COLOR_WEBSAFE_DARK_GRAY + +/// How much we multiply the drawn image by, and as a result the pixel coordinates +#define MINIMAP_PIXEL_MULTIPLIER 2 +/// Converts an icon pixel coordinate (from ICON_X/ICON_Y modifiers) to a world tile coordinate. +#define MINIMAP_ICON_TO_WORLD(icon_coord, minimap_min) ((minimap_min) + floor(((icon_coord) - 1) / MINIMAP_PIXEL_MULTIPLIER)) +/// Converts a world tile coordinate to a pixel_w/pixel_z offset for placing a blip on the minimap display. +#define MINIMAP_WORLD_TO_PIXEL(world_coord, minimap_min, half_size) (((world_coord) - (minimap_min)) * MINIMAP_PIXEL_MULTIPLIER + 1 - (half_size)) +#define COMSIG_MINIMAP_ADD(blip_tag) "minimap_add_" + blip_tag +#define COMSIG_MINIMAP_REMOVE(blip_tag) "minimap_remove_" + blip_tag +// sends a index of how much to change by +#define COMSIG_MINIMAP_CHANGE_Z_LEVEL "minimap_z_change" +#define COMSIG_MINIMAP_ACTION_TRIGGER "minimap_action_trigger" + #define COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL (1<<0) + + +#define MINIMAP_BOMB_BLIP "nuke" +#define MINIMAP_NUKEDISK_BLIP "nuke_disk" +#define MINIMAP_NUKEOP_BLIP "nukeop" +#define MINIMAP_NUKEOP_BORG_BLIP "nukeop_borg" +#define MINIMAP_SYNDICATE_MECH_BLIP "syndicate_mech" +#define MINIMAP_SYNDIE_TURRET_BLIP "syndie_turret" +#define MINIMAP_LADDER_BLIP "ladder" +#define MINIMAP_STAIR_BLIP "stair" +#define MINIMAP_ANNOTATION_TAG_NUCLEAR "nuclear_ops" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 317b17c2d7e6..59b12502d1c7 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -501,6 +501,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_CLIFF_WALKER "cliff_walker" /// This means the user is currently holding/wearing a "tactical camouflage" item (like a potted plant). #define TRAIT_TACTICALLY_CAMOUFLAGED "tactically_camouflaged" +/// This means the user is allowed to draw on minimap holotables. +#define TRAIT_MINIMAP_TABLE_DRAW "minimap_table_draw" /// Gets double arcade prizes #define TRAIT_GAMERGOD "gamer-god" #define TRAIT_GIANT "giant" diff --git a/code/__HELPERS/logging/game.dm b/code/__HELPERS/logging/game.dm index 0affd00228b6..52c1a2254f3c 100644 --- a/code/__HELPERS/logging/game.dm +++ b/code/__HELPERS/logging/game.dm @@ -32,3 +32,7 @@ /proc/log_ghost_poll(text, list/data) logger.Log(LOG_CATEGORY_GAME_GHOST_POLLS, text, data) + +/// Logging for drawing on minimap +/proc/log_minimap_drawing(text, list/data) + logger.Log(LOG_CATEGORY_GAME_MINIMAP_DRAWING, text, data) diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm index ccb1945a9248..fcd5753f6139 100644 --- a/code/__HELPERS/type2type.dm +++ b/code/__HELPERS/type2type.dm @@ -320,6 +320,15 @@ GLOBAL_LIST_INIT(modulo_angle_to_dir, list(NORTH,NORTHEAST,EAST,SOUTHEAST,SOUTH, if(var_source.vars.Find(A)) . += A +/// Converts a screen loc param to a x,y coordinate pixel on the screen. +/proc/params2screenpixel(scr_loc) + var/list/x_and_y = splittext(scr_loc, ",") + var/list/x_dirty = splittext(x_and_y[1], ":") + var/list/y_dirty = splittext(x_and_y[2], ":") + var/x = (text2num(x_dirty[1]) - 1) * ICON_SIZE_X + text2num(x_dirty[2]) + var/y = (text2num(y_dirty[1]) - 1) * ICON_SIZE_Y + text2num(y_dirty[2]) + return list(x, y) + //word of warning: using a matrix like this as a color value will simplify it back to a string after being set /proc/color_hex2color_matrix(string) var/length = length(string) diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 24127005b6ee..61a0255dfaa2 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -651,6 +651,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_XRAY_VISION" = TRAIT_XRAY_VISION, "TRAIT_NOGRAV_ALWAYS_DRIFT" = TRAIT_NOGRAV_ALWAYS_DRIFT, "TRAIT_SPEECH_BOOSTER" = TRAIT_SPEECH_BOOSTER, + "TRAIT_MINIMAP_TABLE_DRAW" = TRAIT_MINIMAP_TABLE_DRAW, "TRAIT_MINING_PARRYING" = TRAIT_MINING_PARRYING, "TRAIT_MINING_AOE_IMMUNE" = TRAIT_MINING_AOE_IMMUNE, "TRAIT_IGNORE_FIRE_PROTECTION" = TRAIT_IGNORE_FIRE_PROTECTION, diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index ee1aa3633d11..d25020206dab 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -354,6 +354,13 @@ /mob/proc/RangedAttack(atom/A, modifiers) if(SEND_SIGNAL(src, COMSIG_MOB_ATTACK_RANGED, A, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN) return TRUE + A.RangedAttackOn(src, modifiers) + +/** + * Atom's version of RangedAttack, for when you want to do something when a mob clicks on this with more sanity than just Click() + */ +/atom/proc/RangedAttackOn(mob/attacker, list/modifiers) + return null /** * Ranged secondary attack diff --git a/code/_onclick/hud/screen_objects/screen_objects.dm b/code/_onclick/hud/screen_objects/screen_objects.dm index 11accd552f52..00341f671881 100644 --- a/code/_onclick/hud/screen_objects/screen_objects.dm +++ b/code/_onclick/hud/screen_objects/screen_objects.dm @@ -94,7 +94,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen) // I hate this place RegisterSignal(hud, COMSIG_QDELETING, PROC_REF(on_hud_delete)) /// Returns the mob this is being displayed to, if any -/atom/movable/screen/proc/get_mob() +/atom/movable/screen/proc/get_mob() as /mob return hud?.mymob /atom/movable/screen/proc/on_hud_delete(datum/source) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 114988a0f86c..71516fdd24f1 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -101,6 +101,9 @@ /// log game events /datum/config_entry/flag/log_game +/// log minimap drawing events +/datum/config_entry/flag/log_minimap_drawing + /// log mech data /datum/config_entry/flag/log_mecha diff --git a/code/datums/proximity_monitor/proximity_monitor.dm b/code/datums/proximity_monitor/proximity_monitor.dm index ec77ce2145a1..f5e63733765f 100644 --- a/code/datums/proximity_monitor/proximity_monitor.dm +++ b/code/datums/proximity_monitor/proximity_monitor.dm @@ -74,9 +74,10 @@ //Update the ignore_if_not_on_turf AddComponent(/datum/component/connect_range, host, loc_connections, current_range, ignore_if_not_on_turf) -/datum/proximity_monitor/proc/on_uncrossed() +/datum/proximity_monitor/proc/on_uncrossed(atom/source, atom/movable/gone, direction) //Used by the advanced subtype for effect fields. SIGNAL_HANDLER - return //Used by the advanced subtype for effect fields. + if(source != host) + hasprox_receiver?.OnProximityExit(gone) /datum/proximity_monitor/proc/on_entered(atom/source, atom/movable/arrived, turf/old_loc) SIGNAL_HANDLER diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 87104b8cd473..fd0f586b98d5 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -12,6 +12,7 @@ plane = AREA_PLANE mouse_opacity = MOUSE_OPACITY_TRANSPARENT invisibility = INVISIBILITY_LIGHTING + tacmap_color = null /// List of all turfs currently inside this area as nested lists indexed by zlevel. /// Acts as a filtered version of area.contents For faster lookup @@ -137,6 +138,9 @@ /// Are shuttles allowed to dock in this area var/allow_shuttle_docking = FALSE + /// If TRUE, then this area will be skipped entirely by minimap rendering. + var/skip_minimap_rendering = FALSE + /** * A list of teleport locations * diff --git a/code/game/area/areas/away_content.dm b/code/game/area/areas/away_content.dm index 39d7ebce1dbe..7194a8494e4a 100644 --- a/code/game/area/areas/away_content.dm +++ b/code/game/area/areas/away_content.dm @@ -11,6 +11,7 @@ Unused icons for new areas are "awaycontent1" ~ "awaycontent30" default_gravity = STANDARD_GRAVITY ambience_index = AMBIENCE_AWAY sound_environment = SOUND_ENVIRONMENT_ROOM + skip_minimap_rendering = TRUE /area/awaymission/museum name = "Nanotrasen Museum" diff --git a/code/game/area/areas/mining.dm b/code/game/area/areas/mining.dm index af047a00698d..51f054029739 100644 --- a/code/game/area/areas/mining.dm +++ b/code/game/area/areas/mining.dm @@ -196,6 +196,7 @@ sound_environment = SOUND_AREA_ICEMOON ambient_buzz = 'sound/ambience/lavaland/magma.ogg' allow_shuttle_docking = TRUE + skip_minimap_rendering = TRUE /area/icemoon/surface name = "Icemoon" diff --git a/code/game/area/areas/misc.dm b/code/game/area/areas/misc.dm index ac66943d5d1c..a922a1e147ab 100644 --- a/code/game/area/areas/misc.dm +++ b/code/game/area/areas/misc.dm @@ -17,6 +17,7 @@ sound_environment = SOUND_AREA_SPACE ambient_buzz = null //Space is deafeningly quiet allow_shuttle_docking = TRUE + skip_minimap_rendering = TRUE /area/space/nearstation icon_state = "space_near" diff --git a/code/game/area/areas/ruins/_ruins.dm b/code/game/area/areas/ruins/_ruins.dm index 92fff19386da..f0adbe7eb6bc 100644 --- a/code/game/area/areas/ruins/_ruins.dm +++ b/code/game/area/areas/ruins/_ruins.dm @@ -9,6 +9,7 @@ ambience_index = AMBIENCE_RUINS flags_1 = CAN_BE_DIRTY_1 sound_environment = SOUND_ENVIRONMENT_STONEROOM + skip_minimap_rendering = TRUE /area/ruin/unpowered always_unpowered = TRUE diff --git a/code/game/area/areas/station/cargo.dm b/code/game/area/areas/station/cargo.dm index 8e892efda95c..430122cc769b 100644 --- a/code/game/area/areas/station/cargo.dm +++ b/code/game/area/areas/station/cargo.dm @@ -3,6 +3,7 @@ icon_state = "quart" airlock_wires = /datum/wires/airlock/cargo sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = COLOR_CARGO_BROWN /area/station/cargo/sorting name = "\improper Delivery Office" diff --git a/code/game/area/areas/station/command.dm b/code/game/area/areas/station/command.dm index 30f126dc42cb..6cc80002aab1 100644 --- a/code/game/area/areas/station/command.dm +++ b/code/game/area/areas/station/command.dm @@ -6,6 +6,7 @@ ) airlock_wires = /datum/wires/airlock/command sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = TACMAP_AREA_COMMAND /area/station/command/bridge name = "\improper Bridge" diff --git a/code/game/area/areas/station/common.dm b/code/game/area/areas/station/common.dm index 1b6eba4dc25e..641c43dfd417 100644 --- a/code/game/area/areas/station/common.dm +++ b/code/game/area/areas/station/common.dm @@ -87,6 +87,7 @@ mood_message = "I love being in the bar!" mood_trait = TRAIT_EXTROVERT sound_environment = SOUND_AREA_SMALL_SOFTFLOOR + tacmap_color = TACMAP_AREA_SERVICE /area/station/commons/fitness name = "\improper Fitness Room" diff --git a/code/game/area/areas/station/engineering.dm b/code/game/area/areas/station/engineering.dm index 3f0464453857..04e27d738c4e 100644 --- a/code/game/area/areas/station/engineering.dm +++ b/code/game/area/areas/station/engineering.dm @@ -3,6 +3,7 @@ ambience_index = AMBIENCE_ENGI airlock_wires = /datum/wires/airlock/engineering sound_environment = SOUND_AREA_LARGE_ENCLOSED + tacmap_color = TACMAP_AREA_ENGINEERING /area/station/engineering/circuit_workshop name = "\improper Circuit Workshop" @@ -150,6 +151,7 @@ icon_state = "construction" ambience_index = AMBIENCE_ENGI sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = TACMAP_AREA_ENGINEERING /area/station/construction/mining/aux_base name = "Auxiliary Base Construction" diff --git a/code/game/area/areas/station/hallway.dm b/code/game/area/areas/station/hallway.dm index 12a5ba6816b4..3c82c9b1beea 100644 --- a/code/game/area/areas/station/hallway.dm +++ b/code/game/area/areas/station/hallway.dm @@ -92,6 +92,7 @@ /area/station/hallway/secondary/service name = "\improper Service Hallway" icon_state = "hall_service" + tacmap_color = TACMAP_AREA_SERVICE /area/station/hallway/secondary/spacebridge name = "\improper Space Bridge" diff --git a/code/game/area/areas/station/maintenance.dm b/code/game/area/areas/station/maintenance.dm index a6fa76cd80e9..84ce4032eb62 100644 --- a/code/game/area/areas/station/maintenance.dm +++ b/code/game/area/areas/station/maintenance.dm @@ -7,6 +7,7 @@ forced_ambience = TRUE ambient_buzz = 'sound/ambience/maintenance/source_corridor2.ogg' ambient_buzz_vol = 20 + tacmap_color = TACMAP_AREA_MAINTENANCE /* * Departmental Maintenance @@ -248,6 +249,7 @@ /area/station/maintenance/disposal/incinerator name = "\improper Incinerator" icon_state = "incinerator" + tacmap_color = TACMAP_AREA_ENGINEERING /area/station/maintenance/space_hut name = "\improper Space Hut" diff --git a/code/game/area/areas/station/medical.dm b/code/game/area/areas/station/medical.dm index 03bdc8eb5bf1..a02e014a0c00 100644 --- a/code/game/area/areas/station/medical.dm +++ b/code/game/area/areas/station/medical.dm @@ -4,6 +4,7 @@ ambience_index = AMBIENCE_MEDICAL airlock_wires = /datum/wires/airlock/medbay sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = TACMAP_AREA_MEDICAL /area/station/medical/abandoned name = "\improper Abandoned Medbay" @@ -12,6 +13,7 @@ 'sound/ambience/misc/signal.ogg', ) sound_environment = SOUND_AREA_SMALL_ENCLOSED + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/medical/medbay/central name = "Medbay Central" diff --git a/code/game/area/areas/station/science.dm b/code/game/area/areas/station/science.dm index b64e7efe75a4..9954a500673b 100644 --- a/code/game/area/areas/station/science.dm +++ b/code/game/area/areas/station/science.dm @@ -3,6 +3,7 @@ icon_state = "science" airlock_wires = /datum/wires/airlock/science sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = TACMAP_AREA_SCIENCE /area/station/science/lobby name = "\improper Science Lobby" @@ -131,6 +132,7 @@ name = "\improper Ordnance Bomb Site" icon_state = "ord_boom" area_flags = BLOBS_ALLOWED | CULT_PERMITTED | NO_GRAVITY + skip_minimap_rendering = TRUE /area/station/science/ordnance/bomb/planet area_flags = /area/station/science/ordnance/bomb::area_flags & ~NO_GRAVITY diff --git a/code/game/area/areas/station/security.dm b/code/game/area/areas/station/security.dm index 3166f7733d95..22d3bb0de85c 100644 --- a/code/game/area/areas/station/security.dm +++ b/code/game/area/areas/station/security.dm @@ -6,6 +6,7 @@ ambience_index = AMBIENCE_DANGER airlock_wires = /datum/wires/airlock/security sound_environment = SOUND_AREA_STANDARD_STATION + tacmap_color = TACMAP_AREA_SECURITY /area/station/security/office name = "\improper Security Office" diff --git a/code/game/area/areas/station/service.dm b/code/game/area/areas/station/service.dm index 355406563264..71e9421e3a5b 100644 --- a/code/game/area/areas/station/service.dm +++ b/code/game/area/areas/station/service.dm @@ -1,5 +1,6 @@ /area/station/service airlock_wires = /datum/wires/airlock/service + tacmap_color = TACMAP_AREA_SERVICE /* * Bar/Kitchen Areas @@ -199,27 +200,34 @@ name = "\improper Abandoned Garden" icon_state = "abandoned_garden" sound_environment = SOUND_AREA_SMALL_ENCLOSED + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/kitchen/abandoned name = "\improper Abandoned Kitchen" icon_state = "abandoned_kitchen" + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/electronic_marketing_den name = "\improper Electronic Marketing Den" icon_state = "abandoned_marketing_den" + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/abandoned_gambling_den name = "\improper Abandoned Gambling Den" icon_state = "abandoned_gambling_den" + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/abandoned_gambling_den/gaming name = "\improper Abandoned Gaming Den" icon_state = "abandoned_gaming_den" + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/theater/abandoned name = "\improper Abandoned Theater" icon_state = "abandoned_theatre" + tacmap_color = TACMAP_AREA_MAINTENANCE /area/station/service/library/abandoned name = "\improper Abandoned Library" icon_state = "abandoned_library" + tacmap_color = TACMAP_AREA_MAINTENANCE diff --git a/code/game/area/areas/station/solars.dm b/code/game/area/areas/station/solars.dm index 569a7f0a169e..d7765c6ee9b6 100644 --- a/code/game/area/areas/station/solars.dm +++ b/code/game/area/areas/station/solars.dm @@ -77,6 +77,7 @@ /area/station/maintenance/solars name = "Solar Maintenance" icon_state = "yellow" + tacmap_color = TACMAP_AREA_ENGINEERING /area/station/maintenance/solars/port name = "Port Solar Maintenance" diff --git a/code/game/area/areas/station/telecomm.dm b/code/game/area/areas/station/telecomm.dm index adb4670b44b3..46c644acfebe 100644 --- a/code/game/area/areas/station/telecomm.dm +++ b/code/game/area/areas/station/telecomm.dm @@ -15,6 +15,7 @@ 'sound/ambience/misc/ambimystery.ogg', ) airlock_wires = /datum/wires/airlock/engineering + tacmap_color = TACMAP_AREA_ENGINEERING /area/station/tcommsat/maints name = "\improper Telecomms Maintenance Room" diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm index ad246f5ff825..d59f478a2dba 100644 --- a/code/game/atom/_atom.dm +++ b/code/game/atom/_atom.dm @@ -147,6 +147,9 @@ /// Generally for niche objects, atoms blacklisted can spawn if enabled by spawner. var/spawn_blacklisted = FALSE + /// What color this shows up as on the tactical map + var/tacmap_color = TACMAP_SOLID + /** * Top level of the destroy chain for most atoms * @@ -438,6 +441,10 @@ /atom/proc/HasProximity(atom/movable/proximity_check_mob as mob|obj) return +/// has a previously nearby atom moved away +/atom/proc/OnProximityExit(atom/movable/proximity_check_mob as mob|obj) + return + /// Sets the wire datum of an atom /atom/proc/set_wires(datum/wires/new_wires) wires = new_wires diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 57dffa377e5f..6bbab50fa790 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -28,6 +28,8 @@ idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.1 active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 0.2 + tacmap_color = TACMAP_DOOR + /// The animation we're currently playing, if any var/animation var/visible = TRUE diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index 801ed06603cd..f682219a06bd 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -782,6 +782,19 @@ DEFINE_BITFIELD(turret_flags, list( . = ..() AddElement(/datum/element/empprotection, EMP_PROTECT_SELF | EMP_PROTECT_WIRES) AddElement(/datum/element/nav_computer_icon, 'icons/effects/nav_computer_indicators.dmi', "turret", FALSE) + add_minimap_blip(src, MINIMAP_SYNDIE_TURRET_BLIP, "sentry_passive") + +/obj/machinery/porta_turret/syndicate/proc/update_turret_minimap_icon(new_icon_state) + var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(MINIMAP_SYNDIE_TURRET_BLIP, src) + if(isnull(blip)) + return + blip.icon_state = new_icon_state + +/obj/machinery/porta_turret/syndicate/shootAt(atom/movable/target) + . = ..() + if(raised && (obj_flags & EMAGGED || last_fired == world.time)) + update_turret_minimap_icon("sentry_firing") + addtimer(CALLBACK(src, PROC_REF(update_turret_minimap_icon), "sentry_passive"), shot_delay + 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE) /obj/machinery/porta_turret/syndicate/setup() return diff --git a/code/game/objects/items/implants/implant_tacmap.dm b/code/game/objects/items/implants/implant_tacmap.dm new file mode 100644 index 000000000000..33ebbe88c08e --- /dev/null +++ b/code/game/objects/items/implants/implant_tacmap.dm @@ -0,0 +1,120 @@ +/obj/item/implant/tacmap + name = "tactical map implant" + desc = "provides you with a map" + actions_types = list(/datum/action/minimap) + var/wearer_icon_state = null + /// Optional z-trait that resolves to the first z-level and locks the minimap there. + var/minimap_fixed_z_trait + /// Whether this implant allows drawing/labeling on the personal minimap HUD. + var/can_draw_on_personal_minimap = FALSE + var/static/list/minimap_refresh_signals = list( + COMSIG_MOB_STATCHANGE, + COMSIG_LIVING_REVIVE, + COMSIG_MOB_GHOSTIZED, + ) + +/obj/item/implant/tacmap/implant(mob/living/target, mob/user, silent, force) + . = ..() + if(.) + configure_minimap_action() + RegisterSignals(target, minimap_refresh_signals, PROC_REF(refresh_minimap_icon)) + addtimer(CALLBACK(src, PROC_REF(update_minimap_icon), target), 0.1 SECONDS) // Mobs are spawned inside nullspace sometimes so this avoids that hijinks. + +/obj/item/implant/tacmap/removed(mob/living/source, silent, special) + UnregisterSignal(source, minimap_refresh_signals) + remove_minimap(source) + return ..() + +///Remove all action of type minimap from the wearer, and make him disappear from the minimap +/obj/item/implant/tacmap/proc/remove_minimap(mob/user) + remove_minimap_blip(MINIMAP_NUKEOP_BLIP, user) + +/obj/item/implant/tacmap/proc/refresh_minimap_icon() + SIGNAL_HANDLER + if(imp_in) + update_minimap_icon(imp_in) + +/obj/item/implant/tacmap/proc/resolve_fixed_minimap_z_level() + if(!isnull(minimap_fixed_z_trait)) + var/list/trait_levels = SSmapping.levels_by_trait(minimap_fixed_z_trait) + if(length(trait_levels)) + return trait_levels[1] + return null + +/obj/item/implant/tacmap/proc/configure_minimap_action() + var/datum/action/minimap/minimap_action = locate(/datum/action/minimap) in actions + if(isnull(minimap_action)) + return + minimap_action.fixed_z_level = resolve_fixed_minimap_z_level() + minimap_action.can_draw = can_draw_on_personal_minimap + +/obj/item/implant/tacmap/proc/get_minimap_icon_state(mob/living/wearer) + return wearer_icon_state + +///Updates the wearer's minimap icon +/obj/item/implant/tacmap/proc/update_minimap_icon(mob/wearer) + SIGNAL_HANDLER + remove_minimap_blip(MINIMAP_NUKEOP_BLIP, wearer) + add_minimap_blip(wearer, MINIMAP_NUKEOP_BLIP, get_minimap_icon_state(wearer)) + +/obj/item/implant/tacmap/nuclear // Nukie subtype, map shows you nuke disk, operatives, cayenne and the nuke + actions_types = list(/datum/action/minimap/nuclear) + wearer_icon_state = "syndicate" + +/obj/item/implant/tacmap/nuclear/implant(mob/living/target, mob/user, silent, force) + . = ..() + if(.) + RegisterSignal(target, COMSIG_MINIMAP_ACTION_TRIGGER, PROC_REF(deny_nukie_base_open)) + +/obj/item/implant/tacmap/nuclear/get_minimap_icon_state(mob/living/wearer) + if(wearer.stat != DEAD && istype(wearer, /mob/living/basic/carp/pet/cayenne)) + return "cayenne" + . = ..() + +/obj/item/implant/tacmap/nuclear/removed(mob/living/source, silent, special) + UnregisterSignal(source, COMSIG_MINIMAP_ACTION_TRIGGER) + return ..() + +/obj/item/implant/tacmap/nuclear/proc/deny_nukie_base_open(mob/living/user) + var/turf/user_turf = get_turf(user) + if(user_turf.onSyndieBase()) + user.balloon_alert(user, "can't use implant in the base, go to the holotable!") + return COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL + +/obj/item/implant/tacmap/nuclear/cayenne // subtype used for cayenne and syndie sentience potions in general + wearer_icon_state = "cayenne" + +/obj/item/implant/tacmap/nuclear/leader // Leader subtype lets him draw on the map + actions_types = list(/datum/action/minimap/nuclear) + can_draw_on_personal_minimap = TRUE + wearer_icon_state = "syndicate_leader" + +/obj/item/implant/tacmap/nuclear/leader/implant(mob/living/target, mob/user, silent, force) + . = ..() + if(.) + ADD_TRAIT(target, TRAIT_MINIMAP_TABLE_DRAW, REF(src)) + +/obj/item/implant/tacmap/nuclear/leader/removed(mob/living/source, silent, special) + REMOVE_TRAIT(source, TRAIT_MINIMAP_TABLE_DRAW, REF(src)) + return ..() + +// Subtype that just lets them open it off-base. +/obj/item/implant/tacmap/nuclear/offbase + minimap_fixed_z_trait = ZTRAIT_STATION + can_draw_on_personal_minimap = TRUE + +/obj/item/implant/tacmap/nuclear/offbase/deny_nukie_base_open(mob/living/user) + return + +/obj/item/implanter/tacmap + name = "implanter (minimap)" + imp_type = /obj/item/implant/tacmap + +/obj/item/implanter/tacmap/nuclear + name = "implanter (operative minimap)" + imp_type = /obj/item/implant/tacmap/nuclear + +/obj/item/implantcase/tacmap + name = "implant case - 'Tactical Map'" + desc = "A glass case containing an implant with a virtual map." + imp_type = /obj/item/implant/tacmap diff --git a/code/game/objects/items/storage/uplink_kits.dm b/code/game/objects/items/storage/uplink_kits.dm index aff0ccc4e800..cd4baf7f1c99 100644 --- a/code/game/objects/items/storage/uplink_kits.dm +++ b/code/game/objects/items/storage/uplink_kits.dm @@ -764,6 +764,8 @@ new /obj/item/book/manual/nuclear(src) // Very important // The most important part of the kit, the implant that gives them the syndicate faction. new /obj/item/implanter/induction_implant(src) + // Tactical map implant so they can see the minimap with the rest of the team. + new /obj/item/implanter/tacmap/nuclear(src) // All in all, 6+3+3+2+5+2+4 = ~25 TC of 'miscellaneous' items. // This is a lot of value for 10 TC, but you have to keep in mind that you NEED someone to get this stuff station-side. // Pretty much all of it is a bad deal for reinforcements or yourself as they already have similar or good-enough alternatives. diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm index 95c725868ca1..f2f9d138df80 100644 --- a/code/game/objects/structures/fence.dm +++ b/code/game/objects/structures/fence.dm @@ -18,6 +18,7 @@ icon = 'icons/obj/fence.dmi' icon_state = "straight" + tacmap_color = TACMAP_FENCE var/cuttable = TRUE var/hole_size= NO_HOLE diff --git a/code/game/objects/structures/ladders.dm b/code/game/objects/structures/ladders.dm index 5d15c1af89f0..88423eb182c5 100644 --- a/code/game/objects/structures/ladders.dm +++ b/code/game/objects/structures/ladders.dm @@ -165,6 +165,7 @@ down.up = null down.update_appearance(UPDATE_ICON_STATE) + down.update_minimap_blip() down = null update_appearance(UPDATE_ICON_STATE) clear_base_transparency() @@ -188,6 +189,7 @@ up.down = null up.clear_base_transparency() up.update_appearance(UPDATE_ICON_STATE) + up.update_minimap_blip() up = null update_appearance(UPDATE_ICON_STATE) @@ -196,6 +198,11 @@ unlink_down() unlink_up() +/obj/structure/ladder/proc/update_minimap_blip() + remove_minimap_blip(MINIMAP_LADDER_BLIP, src) + if(up || down) + add_minimap_blip(src, MINIMAP_LADDER_BLIP, "ladder") + /obj/structure/ladder/LateInitialize() // By default, discover ladders above and below us vertically var/turf/base = get_turf(src) @@ -213,6 +220,7 @@ // Linking updates our icon, so if we failed both links we need a manual update if(isnull(down) && isnull(up)) update_appearance(UPDATE_ICON_STATE) + update_minimap_blip() /obj/structure/ladder/update_icon_state() icon_state = "[base_icon_state][!!up][!!down]" diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm index 8d7e1fc84e12..0f037fec4d5c 100644 --- a/code/game/objects/structures/stairs.dm +++ b/code/game/objects/structures/stairs.dm @@ -4,6 +4,8 @@ /// Range within which stair indicators will appear for approaching mobs #define STAIR_INDICATOR_RANGE 3 +/// Minimum tile spacing between stair minimap blips +#define STAIR_BLIP_MIN_DISTANCE 2 // dir determines the direction of travel to go upwards // stairs require /turf/open/openspace as the tile above them to work, unless your stairs have 'force_open_above' set to TRUE @@ -27,6 +29,8 @@ VAR_FINAL/turf/directly_above /// If TRUE, we have left/middle/right sprites. var/has_merged_sprites = TRUE + /// Current atoms used as this stair's minimap blip targets. + var/list/minimap_blip_targets /// Lazyassoc list of weakef to mob viewing stair indicators to their images VAR_PRIVATE/list/mob_to_image @@ -63,6 +67,7 @@ force_open_above() build_signal_listener() update_surrounding() + update_minimap_blip() var/static/list/exit_connections = list( COMSIG_ATOM_EXIT = PROC_REF(on_exit_stairs), @@ -78,6 +83,7 @@ /obj/structure/stairs/Destroy() + clear_minimap_blips() if(directly_above) UnregisterSignal(directly_above, COMSIG_TURF_MULTIZ_NEW) directly_above = null @@ -104,6 +110,47 @@ for(var/obj/structure/stairs/stair in get_step(src, turn(dir, -90))) stair.update_appearance() + update_minimap_blip() + +/obj/structure/stairs/proc/update_minimap_blip() + var/bottom_state = isTerminator() ? "stairs_up" : "stairs_down" + var/top_state = (bottom_state == "stairs_up") ? "stairs_down" : "stairs_up" + var/turf/current_turf = get_turf(src) + + clear_minimap_blips() + if(isnull(current_turf)) + return + + add_minimap_blip_if_valid(current_turf, bottom_state) + add_minimap_blip_if_valid(get_step_multiz(current_turf, UP), top_state) + +/obj/structure/stairs/proc/clear_minimap_blips() + if(!islist(minimap_blip_targets)) + return + for(var/atom/target as anything in minimap_blip_targets) + remove_minimap_blip(MINIMAP_STAIR_BLIP, target) + LAZYCLEARLIST(minimap_blip_targets) + +/obj/structure/stairs/proc/add_minimap_blip_if_valid(atom/target, state) + if(isnull(target)) + return + if(!should_place_minimap_blip(target)) + return + + var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(MINIMAP_STAIR_BLIP, target) + if(!isnull(blip)) + blip.icon_state = state + else + add_minimap_blip(target, MINIMAP_STAIR_BLIP, state) + LAZYADD(minimap_blip_targets, target) + +/obj/structure/stairs/proc/should_place_minimap_blip(atom/target) + var/turf/target_turf = get_turf(target) + if(isnull(target_turf)) + return FALSE + if(length(get_minimap_blips_in_area(MINIMAP_STAIR_BLIP, target_turf, STAIR_BLIP_MIN_DISTANCE))) + return FALSE + return TRUE /obj/structure/stairs/update_icon_state() . = ..() @@ -420,3 +467,4 @@ #undef STAIR_TERMINATOR_YES #undef STAIR_INDICATOR_RANGE +#undef STAIR_BLIP_MIN_DISTANCE diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index cd54bd380f2f..6ca6b22836c9 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -18,6 +18,7 @@ flags_ricochet = RICOCHET_HARD receive_ricochet_chance_mod = 0.5 custom_materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT) + tacmap_color = TACMAP_WINDOW var/state = WINDOW_OUT_OF_FRAME var/reinf = FALSE var/heat_resistance = 800 diff --git a/code/game/turfs/closed/_closed.dm b/code/game/turfs/closed/_closed.dm index c29772afc5b4..09bd513be8e3 100644 --- a/code/game/turfs/closed/_closed.dm +++ b/code/game/turfs/closed/_closed.dm @@ -9,6 +9,7 @@ init_air = FALSE rad_insulation = RAD_MEDIUM_INSULATION pass_flags_self = PASSCLOSEDTURF + tacmap_color = TACMAP_BLACK /turf/closed/AfterChange() . = ..() diff --git a/code/game/turfs/open/asteroid.dm b/code/game/turfs/open/asteroid.dm index c441871caa82..b8128820f8ec 100644 --- a/code/game/turfs/open/asteroid.dm +++ b/code/game/turfs/open/asteroid.dm @@ -182,6 +182,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt) initial_gas_mix = LAVALAND_DEFAULT_ATMOS planetary_atmos = TRUE baseturfs = /turf/open/lava/smooth/lava_land_surface + skip_minimap_rendering = TRUE /// Used for the lavaland icemoon ruin. /turf/open/misc/asteroid/basalt/lava_land_surface/no_ruins @@ -393,6 +394,7 @@ GLOBAL_LIST_EMPTY(dug_up_basalt) baseturfs = /turf/open/openspace/icemoon initial_gas_mix = ICEMOON_DEFAULT_ATMOS slowdown = 0 + skip_minimap_rendering = TRUE /// Exact subtype as parent, just used in ruins to prevent other ruins/chasms from spawning on top of it. /turf/open/misc/asteroid/snow/icemoon/do_not_chasm diff --git a/code/game/turfs/open/floor/iron_floor.dm b/code/game/turfs/open/floor/iron_floor.dm index 6f0489acea5e..7f7a5fa28c9b 100644 --- a/code/game/turfs/open/floor/iron_floor.dm +++ b/code/game/turfs/open/floor/iron_floor.dm @@ -414,6 +414,7 @@ icon_state = "solarpanel" base_icon_state = "solarpanel" floor_tile = /obj/item/stack/tile/iron/solarpanel + skip_minimap_rendering = TRUE /turf/open/floor/iron/solarpanel/airless initial_gas_mix = AIRLESS_ATMOS diff --git a/code/game/turfs/open/floor/misc_floor.dm b/code/game/turfs/open/floor/misc_floor.dm index 61f2c76bc797..ae5ae6c3e9dd 100644 --- a/code/game/turfs/open/floor/misc_floor.dm +++ b/code/game/turfs/open/floor/misc_floor.dm @@ -75,6 +75,9 @@ icon_state = "bcircuitoff" always_off = TRUE +/turf/open/floor/circuit/no_light + always_off = TRUE + /turf/open/floor/circuit/airless initial_gas_mix = AIRLESS_ATMOS @@ -120,6 +123,9 @@ icon_state = "rcircuitoff" always_off = TRUE +/turf/open/floor/circuit/red/no_power + always_off = TRUE + /turf/open/floor/circuit/red/anim icon_state = "rcircuitanim" floor_tile = /obj/item/stack/tile/circuit/red/anim diff --git a/code/game/turfs/open/ice.dm b/code/game/turfs/open/ice.dm index a966e8e4d7e6..1a227d2a0340 100644 --- a/code/game/turfs/open/ice.dm +++ b/code/game/turfs/open/ice.dm @@ -82,6 +82,7 @@ baseturfs = /turf/open/openspace/icemoon initial_gas_mix = ICEMOON_DEFAULT_ATMOS slowdown = 0 + skip_minimap_rendering = TRUE /turf/open/misc/ice/icemoon/no_planet_atmos planetary_atmos = FALSE diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm index dcc735819f3b..e4c447008da7 100644 --- a/code/game/turfs/open/space/space.dm +++ b/code/game/turfs/open/space/space.dm @@ -71,6 +71,7 @@ GLOBAL_LIST_EMPTY(starlight) vis_flags = VIS_INHERIT_ID //when this be added to vis_contents of something it be associated with something on clicking, important for visualisation of turf in openspace and interraction with openspace that show you turf. force_no_gravity = TRUE + skip_minimap_rendering = TRUE /turf/open/space/basic icon_state = MAP_SWITCH("space", "space_basic_map") diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index b6a0b8c38678..4a98f911a040 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -113,6 +113,9 @@ GLOBAL_LIST_EMPTY(station_turfs) ///The typepath we use for lazy fishing on turfs, to save on world init time. var/fish_source + /// If TRUE, then this turf will be skipped entirely by minimap rendering. + var/skip_minimap_rendering = FALSE + /turf/vv_edit_var(var_name, new_value) var/static/list/banned_edits = list(NAMEOF_STATIC(src, x), NAMEOF_STATIC(src, y), NAMEOF_STATIC(src, z)) diff --git a/code/modules/antagonists/clown_ops/outfits.dm b/code/modules/antagonists/clown_ops/outfits.dm index 18016da7c4a5..b66b2212445b 100644 --- a/code/modules/antagonists/clown_ops/outfits.dm +++ b/code/modules/antagonists/clown_ops/outfits.dm @@ -18,7 +18,7 @@ /obj/item/mod/skin_applier/honkerative = 1, ) box = /obj/item/storage/box/survival/syndie - implants = list(/obj/item/implant/sad_trombone) + implants = list(/obj/item/implant/sad_trombone, /obj/item/implant/tacmap/nuclear) uplink_type = /obj/item/uplink/clownop @@ -33,3 +33,4 @@ command_radio = TRUE id_trim = /datum/id_trim/chameleon/operative/clown_leader + implants = list(/obj/item/implant/sad_trombone, /obj/item/implant/tacmap/nuclear/leader) diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm index 7e9aa464142e..06a95d809d00 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm @@ -25,6 +25,7 @@ AddComponent(/datum/component/stationloving, !fake) AddComponent(/datum/component/keep_me_secure, CALLBACK(src, PROC_REF(secured_process)), CALLBACK(src, PROC_REF(unsecured_process)), 10) SSpoints_of_interest.make_point_of_interest(src) + add_minimap_blip(src, MINIMAP_NUKEDISK_BLIP, "green_disk_off", 'icons/ui_icons/minimap/map_blips_large.dmi', TRUE) else // Ensure fake disks still have examine text, but dont actually do anything AddComponent(/datum/component/keep_me_secure) diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm index 807f0e6e6905..a00956e7c801 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm @@ -53,6 +53,8 @@ GLOBAL_VAR(station_nuke_source) var/proper_bomb = TRUE //Please /// A reference to the countdown that goes up over the nuke var/obj/effect/countdown/nuclearbomb/countdown + /// is this nuke on the MINIMAP_BOMB_BLIP tag minimap? + var/is_on_minimap = TRUE /obj/machinery/nuclearbomb/Initialize(mapload) . = ..() @@ -62,6 +64,7 @@ GLOBAL_VAR(station_nuke_source) update_appearance() SSpoints_of_interest.make_point_of_interest(src) previous_level = SSsecurity_level.get_current_level_as_text() + update_minimap_blip() /obj/machinery/nuclearbomb/Destroy() safety = FALSE @@ -523,6 +526,7 @@ GLOBAL_VAR(station_nuke_source) arm_nuke(usr) else disarm_nuke(usr) + update_minimap_blip() /// Arms the nuke, making it active and triggering all pinpointers to start counting down (+delta alert) /obj/machinery/nuclearbomb/proc/arm_nuke(mob/armer) @@ -565,6 +569,12 @@ GLOBAL_VAR(station_nuke_source) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NUKE_DEVICE_DISARMED, src) update_appearance() +/obj/machinery/nuclearbomb/proc/update_minimap_blip() + if(!is_on_minimap) + return + var/blip_icon = 'icons/ui_icons/minimap/map_blips_large.dmi' + add_minimap_blip(src, MINIMAP_BOMB_BLIP, "nuke_[timing ? "on" : "off"]", blip_icon, TRUE) + /// If the nuke is active, gets how much time is left until it detonates, in seconds. /// If the nuke is not active, gets how much time the nuke is set for, in seconds. /obj/machinery/nuclearbomb/proc/get_time_left() diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm index 66798ae798c6..7bef615c8067 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm @@ -3,6 +3,7 @@ name = "\improper Nanotrasen-brand nuclear fission explosive" desc = "One of the more successful achievements of the Nanotrasen Corporate Warfare Division, their nuclear fission explosives are renowned for being cheap to produce and devastatingly effective. Signs explain that though this particular device has been decommissioned, every Nanotrasen station is equipped with an equivalent one, just in case. All Captains carefully guard the disk needed to detonate them - at least, the sign says they do. There seems to be a tap on the back." proper_bomb = FALSE + is_on_minimap = FALSE /// The keg located within the beer nuke. var/obj/structure/reagent_dispensers/beerkeg/keg /// Reagent that is produced once the nuke detonates. diff --git a/code/modules/antagonists/nukeop/outfits.dm b/code/modules/antagonists/nukeop/outfits.dm index 74402a2cd066..ce7cadba7a5a 100644 --- a/code/modules/antagonists/nukeop/outfits.dm +++ b/code/modules/antagonists/nukeop/outfits.dm @@ -13,6 +13,11 @@ skillchips = list(/obj/item/skillchip/disk_verifier) box = /obj/item/storage/box/survival/syndie + implants = list( + /obj/item/implant/weapons_auth, + /obj/item/implant/explosive, + /obj/item/implant/tacmap/nuclear, + ) /// Amount of TC to automatically store in this outfit's uplink. var/tc = 25 /// Enables big voice on this outfit's headset, used for nukie leaders. @@ -33,6 +38,12 @@ name = "Syndicate Leader - Basic" command_radio = TRUE + implants = list( + /obj/item/implant/weapons_auth, + /obj/item/implant/explosive, + /obj/item/implant/tacmap/nuclear/leader, + ) + id_trim = /datum/id_trim/chameleon/operative/nuke_leader /datum/outfit/syndicate/leader/plasmaman @@ -84,6 +95,11 @@ /datum/outfit/syndicate/full/loneop name = "Syndicate Operative - Full Kit (Loneop)" uplink_type = /obj/item/uplink/loneop + implants = list( + /obj/item/implant/weapons_auth, + /obj/item/implant/explosive, + /obj/item/implant/tacmap/nuclear/leader, + ) /datum/outfit/syndicate/full/plasmaman name = "Syndicate Operative - Full Kit (Plasmaman)" @@ -213,3 +229,8 @@ command_radio = TRUE tc = 0 uplink_type = null + implants = list( + /obj/item/implant/weapons_auth, + /obj/item/implant/explosive, + /obj/item/implant/tacmap/nuclear/offbase, + ) diff --git a/code/modules/logging/categories/log_category_game.dm b/code/modules/logging/categories/log_category_game.dm index fced988c1fc4..a5be914e6d25 100644 --- a/code/modules/logging/categories/log_category_game.dm +++ b/code/modules/logging/categories/log_category_game.dm @@ -66,3 +66,7 @@ category = LOG_CATEGORY_GAME_BLOOD_WORM config_flag = /datum/config_entry/flag/log_blood_worm master_category = /datum/log_category/game + +/datum/log_category/minimap_drawing + category = LOG_CATEGORY_GAME_MINIMAP_DRAWING + master_category = /datum/log_category/game diff --git a/code/modules/minimap/_minimap.dm b/code/modules/minimap/_minimap.dm new file mode 100644 index 000000000000..e864b080f560 --- /dev/null +++ b/code/modules/minimap/_minimap.dm @@ -0,0 +1,45 @@ +GLOBAL_ALIST_EMPTY(minimap_blip_tags) +GLOBAL_ALIST_EMPTY(minimap_annotations) +GLOBAL_LIST_EMPTY(minimap_annotation_viewers) + +/// Create a minimap blip on the z-level in question, object is optional, and will tie the blip to the object in question, and will clean up itself if the object in question is deleted +/proc/add_minimap_blip(atom/object, tag, icon_state, icon = 'icons/ui_icons/minimap/map_blips.dmi', large = FALSE, layer = 12) + if(!istype(object) || !tag || !icon_state) + CRASH("Invalid params passed in to add_minimap_blip") + var/atom/movable/screen/minimap_element/blip/new_blip = new(null, null, object, icon_state, icon, large, tag) + new_blip.layer = layer + LAZYADD(GLOB.minimap_blip_tags[tag], new_blip) + SEND_GLOBAL_SIGNAL(COMSIG_MINIMAP_ADD(tag), new_blip) + +/proc/get_minimap_blip(tag, atom/object) + if(!tag || !istype(object)) + return + for(var/atom/movable/screen/minimap_element/blip/blip as anything in GLOB.minimap_blip_tags[tag]) + if(blip.track_target == object) + return blip + +/proc/remove_minimap_blip(tag, atom/object) + var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(tag, object) + if(isnull(blip)) + return + SEND_GLOBAL_SIGNAL(COMSIG_MINIMAP_REMOVE(tag), blip) + LAZYREMOVE(GLOB.minimap_blip_tags[tag], blip) + qdel(blip) + +/// Returns minimap blips matching a tag that are within `distance` tiles of `target_turf` on the same z-level. +/proc/get_minimap_blips_in_area(tag, turf/target_turf, distance) + if(!tag || isnull(target_turf) || !isnum(distance) || !islist(GLOB.minimap_blip_tags[tag])) + return list() + + var/list/nearby_blips = list() + for(var/atom/movable/screen/minimap_element/blip/blip as anything in GLOB.minimap_blip_tags[tag]) + var/turf/blip_turf = get_turf(blip.track_target) + if(isnull(blip_turf)) + continue + if(blip_turf.z != target_turf.z) + continue + if(get_dist(blip_turf, target_turf) > distance) + continue + nearby_blips += blip + + return nearby_blips diff --git a/code/modules/minimap/hud/minimal_z_level.dm b/code/modules/minimap/hud/minimal_z_level.dm new file mode 100644 index 000000000000..af6071b49dc1 --- /dev/null +++ b/code/modules/minimap/hud/minimal_z_level.dm @@ -0,0 +1,68 @@ +/atom/movable/screen/minimap_z_indicator + icon = 'icons/hud/screen_ai.dmi' + icon_state = "zindicator" + screen_loc = "BOTTOM+4,RIGHT" + +/atom/movable/screen/minimap_z_indicator/Initialize(mapload, datum/hud/hud_owner) + . = ..() + if(!isnull(hud_owner.mymob)) + RegisterSignal(hud_owner.mymob, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_level_change)) + RegisterSignal(hud_owner.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, PROC_REF(on_minimap_change_request)) + var/current_turf = get_turf(hud_owner.mymob) + on_z_level_change(null, current_turf, FALSE) + +///sets the currently indicated relative floor +/atom/movable/screen/minimap_z_indicator/proc/on_z_level_change(turf/old_turf, turf/new_turf, same_z_layer) + SIGNAL_HANDLER + var/used_z = get_displayed_z_level(new_turf) + set_floor_text(used_z) + +/atom/movable/screen/minimap_z_indicator/proc/on_minimap_change_request(mob/hud_owner, new_z_change) + SIGNAL_HANDLER + var/atom/movable/screen/minimap_display/current_display = hud?.screen_objects[HUD_TAC_MINIMAP] + if(isnull(current_display)) + var/current_turf = get_turf(hud_owner) + on_z_level_change(null, current_turf, FALSE) + return + var/current_z = current_display.get_viewed_z_level() + if(isnull(current_z)) + return + var/requested_z = current_display.get_clamped_connected_z(current_z + new_z_change, current_z) + set_floor_text(requested_z) + +/atom/movable/screen/minimap_z_indicator/proc/get_displayed_z_level(turf/current_turf) + var/atom/movable/screen/minimap_display/current_display = hud?.screen_objects[HUD_TAC_MINIMAP] + if(!isnull(current_display?.fixed_z_level)) + return current_display.minimap?.z + return current_turf?.z + +/atom/movable/screen/minimap_z_indicator/proc/set_floor_text(used_z) + if(isnull(used_z)) + return + var/bottom_z = used_z + while(SSmapping.multiz_levels?[bottom_z]?[Z_LEVEL_DOWN]) // just keep going down + bottom_z-- + var/text = "Floor
[(used_z - bottom_z) + 1]" + maptext = MAPTEXT_TINY_UNICODE("
[text]
") + +/atom/movable/screen/minimap_z_up + name = "go up" + icon = 'icons/hud/screen_ai.dmi' + icon_state = "up" + mouse_over_pointer = MOUSE_HAND_POINTER + screen_loc = "BOTTOM+4,RIGHT-1" + +/atom/movable/screen/minimap_z_up/Click(location, control, params) + flick("uppressed", src) + SEND_SIGNAL(hud.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, 1) // +1 z level + +/atom/movable/screen/minimap_z_down + name = "go down" + icon = 'icons/hud/screen_ai.dmi' + icon_state = "down" + mouse_over_pointer = MOUSE_HAND_POINTER + screen_loc = "BOTTOM+4,RIGHT-1" + +/atom/movable/screen/minimap_z_down/Click(location, control, params) + flick("downpressed", src) + SEND_SIGNAL(hud.mymob, COMSIG_MINIMAP_CHANGE_Z_LEVEL, -1) // -1 z level diff --git a/code/modules/minimap/hud/minimap_dimmer.dm b/code/modules/minimap/hud/minimap_dimmer.dm new file mode 100644 index 000000000000..bdd759d86cc5 --- /dev/null +++ b/code/modules/minimap/hud/minimap_dimmer.dm @@ -0,0 +1,7 @@ +/// Fullscreen dimmer used while the tactical minimap is open. +/// Rendered below HUD planes so minimap and normal HUD icons remain undimmed. +/atom/movable/screen/fullscreen/dimmer/minimap + alpha = 180 + plane = FULLSCREEN_PLANE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + clear_with_screen = TRUE diff --git a/code/modules/minimap/hud/minimap_display.dm b/code/modules/minimap/hud/minimap_display.dm new file mode 100644 index 000000000000..611bb51f30f0 --- /dev/null +++ b/code/modules/minimap/hud/minimap_display.dm @@ -0,0 +1,585 @@ +#define MINIMAP_LABEL_REMOVE_PIXEL_RANGE 5 + +/// Screen object that renders a [/datum/minimap] base map icon on the HUD. +/atom/movable/screen/minimap_display + name = "Minimap" + icon_state = "" + layer = MINIMAP_IMAGE_LAYER + screen_loc = "1,1" + var/list/origin_px + var/atom/movable/screen/minimap_element/drawing/drawing + /// Optional group tag. Displays with the same tag share drawings and labels. + var/annotation_share_tag + /// Unified list of currently visible minimap elements (drawings, labels, blips, screentip). + var/list/atom/movable/screen/minimap_element/visible_minimap_elements = list() + /// A reference to the minimap used for this display. + var/datum/minimap/minimap + /// Screentext in vis_contents used for the maptext. + var/atom/movable/screen/minimap_element/label/screentip + /// Named blips indexed by name (added via add_blip()). Tagged blips are rebuilt from globals on z-level change. + var/list/atom/movable/screen/minimap_element/blip/blips = list() + /// The list of minimap blip tags we're going to read from the globalist and listen for additions to + var/list/valid_minimap_blip_tags = list() + /// fixed z-level to stay on + var/fixed_z_level + /// Y-axis offset for drawing to account for mouse cursor icon positioning. + var/draw_offset_y = -3 + /// Whether this minimap instance allows drawing and labels. + var/can_draw = TRUE + /// list of signals we want to keep tied on the hud owner mob + var/list/hud_signals = list( + COMSIG_MOVABLE_Z_CHANGED = PROC_REF(on_z_level_change), + COMSIG_MINIMAP_CHANGE_Z_LEVEL = PROC_REF(z_change_request) + ) + /// Maps HUD key → button type path. Used to create/remove toolbar buttons via [/datum/hud]. + var/static/list/toolbar_button_types = list( + HUD_TAC_MINIMAP_TOOL_RED = /atom/movable/screen/minimap_toolbar_button/draw/red, + HUD_TAC_MINIMAP_TOOL_YELLOW = /atom/movable/screen/minimap_toolbar_button/draw/yellow, + HUD_TAC_MINIMAP_TOOL_PURPLE = /atom/movable/screen/minimap_toolbar_button/draw/purple, + HUD_TAC_MINIMAP_TOOL_BLUE = /atom/movable/screen/minimap_toolbar_button/draw/blue, + HUD_TAC_MINIMAP_TOOL_ERASE = /atom/movable/screen/minimap_toolbar_button/erase, + HUD_TAC_MINIMAP_TOOL_LABEL = /atom/movable/screen/minimap_toolbar_button/label, + HUD_TAC_MINIMAP_TOOL_CLEAR = /atom/movable/screen/minimap_toolbar_button/clear, + ) + /// Currently active toolbar button (the active tool). + var/atom/movable/screen/minimap_toolbar_button/active_button = null + /// string for the locator blip's tag + var/locator_blip_tag = "locator" + +/atom/movable/screen/minimap_display/Initialize(mapload, datum/hud/hud_owner, datum/minimap/minimap, list/minimap_blip_tags, initial_fixed_z_level, annotation_share_tag, can_draw = TRUE) + src.can_draw = can_draw + . = ..() + if(isnull(minimap)) + CRASH("[type] created without a minimap reference!") + src.annotation_share_tag = isnull(annotation_share_tag) ? "[type]" : annotation_share_tag + LAZYOR(GLOB.minimap_annotation_viewers[src.annotation_share_tag], src) + if(!isnull(initial_fixed_z_level)) + fixed_z_level = initial_fixed_z_level + INVOKE_ASYNC(src, PROC_REF(apply_fixed_z_minimap), initial_fixed_z_level) + if(length(minimap_blip_tags)) + valid_minimap_blip_tags = minimap_blip_tags.Copy() + set_minimap(minimap) + screentip = new + show_minimap_element(screentip) + if(length(valid_minimap_blip_tags)) + for(var/blip_tag in valid_minimap_blip_tags) + RegisterSignal(SSdcs, COMSIG_MINIMAP_ADD(blip_tag), PROC_REF(on_tagged_blip_add)) + RegisterSignal(SSdcs, COMSIG_MINIMAP_REMOVE(blip_tag), PROC_REF(on_tagged_blip_remove)) + on_z_level_change(hud_owner.mymob) + show_tagged_blips() + +/atom/movable/screen/minimap_display/proc/apply_fixed_z_minimap(target_z) + if(QDELETED(src) || isnull(target_z) || fixed_z_level != target_z) + return + var/datum/minimap/fixed_minimap = get_minimap_for_z(target_z) + if(QDELETED(src) || isnull(fixed_minimap) || fixed_z_level != target_z) + return + set_minimap(fixed_minimap) + +/atom/movable/screen/minimap_display/Destroy() + set_cursor_icon(null) + if(active_button) + active_button.on_deactivate() + active_button = null + var/mob/owner = get_mob() + if(hud) + for(var/key in toolbar_button_types) + hud.remove_screen_object(key, update = FALSE) + if(owner) + for(var/signal in hud_signals) + UnregisterSignal(owner, signal, hud_signals[signal]) + if(owner?.client) + UnregisterSignal(owner.client, COMSIG_CLIENT_MOUSEUP) + if(length(GLOB.minimap_annotation_viewers[annotation_share_tag])) + GLOB.minimap_annotation_viewers[annotation_share_tag] -= src + minimap = null + drawing = null + visible_minimap_elements.Cut() + annotation_share_tag = null + QDEL_NULL(screentip) + if(length(valid_minimap_blip_tags)) + for(var/blip_tag in valid_minimap_blip_tags) + UnregisterSignal(SSdcs, COMSIG_MINIMAP_ADD(blip_tag)) + UnregisterSignal(SSdcs, COMSIG_MINIMAP_REMOVE(blip_tag)) + remove_all_blips() + + return ..() + +/atom/movable/screen/minimap_display/set_new_hud(datum/hud/hud_owner) + var/mob/old_owner = get_mob() + if(hud) + for(var/key in toolbar_button_types) + hud.remove_screen_object(key, update = FALSE) + if(old_owner) + for(var/signal in hud_signals) + UnregisterSignal(old_owner, signal, hud_signals[signal]) + if(old_owner?.client) + UnregisterSignal(old_owner.client, COMSIG_CLIENT_MOUSEUP) + . = ..() + var/mob/new_owner = get_mob() + if(new_owner) + for(var/signal in hud_signals) + RegisterSignal(new_owner, signal, hud_signals[signal]) + if(new_owner?.client) + RegisterSignal(new_owner.client, COMSIG_CLIENT_MOUSEUP, PROC_REF(on_client_mouseup)) + if(can_draw) + for(var/hud_key, hud_type in toolbar_button_types) + var/atom/movable/screen/minimap_toolbar_button/button = new hud_type(null, hud_owner, src) + button.tool_key = hud_key + hud_owner.add_screen_object(button, hud_key, HUD_GROUP_STATIC, update_screen = FALSE) + reposition_toolbar_buttons() + hud_owner.show_hud(hud_owner.hud_version) + +/atom/movable/screen/minimap_display/Click(location, control, params) + if(..() || usr != get_mob()) + return + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X)) + var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y)) + var/right_click = LAZYACCESS(modifiers, RIGHT_CLICK) + + if(active_button && active_button.on_click(icon_x, icon_y, right_click)) + return + +/atom/movable/screen/minimap_display/proc/remove_nearest_label(icon_x, icon_y, mob/user) + var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z) + if(!length(labels_for_z)) + return + var/atom/movable/screen/minimap_element/label/nearest + var/nearest_dist_sq + for(var/atom/movable/screen/minimap_element/label/label as anything in labels_for_z) + var/dx = label.pixel_w - icon_x + var/dy = label.pixel_z - icon_y + var/dist_sq = (dx * dx) + (dy * dy) + if(dist_sq > (MINIMAP_LABEL_REMOVE_PIXEL_RANGE * MINIMAP_LABEL_REMOVE_PIXEL_RANGE)) + continue + if(isnull(nearest_dist_sq) || dist_sq < nearest_dist_sq) + nearest_dist_sq = dist_sq + nearest = label + if(isnull(nearest)) + return + labels_for_z -= nearest + hide_minimap_element(nearest) + qdel(nearest) + sync_visible_objects(minimap?.z) + log_minimap_drawing("[key_name(user)] removed a minimap label") + +/atom/movable/screen/minimap_display/MouseEntered(location, control, params) + MouseMove(location, control, params) + +/atom/movable/screen/minimap_display/MouseDrag(over_object, src_location, over_location, src_control, over_control, params) + if(usr != get_mob()) + return + if(!active_button) + return + var/list/modifiers = params2list(params) + var/list/mouse_px = params2screenpixel(LAZYACCESS(modifiers, SCREEN_LOC)) + if(length(mouse_px) != 2) + return + var/x = mouse_px[1] - origin_px[1] + 1 + var/y = mouse_px[2] - origin_px[2] + 1 + active_button.on_mouse_drag(x, y) + +/atom/movable/screen/minimap_display/proc/on_client_mouseup(client/source) + SIGNAL_HANDLER + if(active_button) + active_button.on_mouse_up() + +/atom/movable/screen/minimap_display/MouseMove(location, control, params) + if(usr != get_mob()) + return + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X)) + var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y)) + + var/x = clamp(MINIMAP_ICON_TO_WORLD(icon_x, minimap.min_x), 1, world.maxx) + var/y = clamp(MINIMAP_ICON_TO_WORLD(icon_y, minimap.min_y), 1, world.maxy) + var/hover_text = get_hover_text(x, y) + screentip.maptext = MAPTEXT_TINY_UNICODE("[hover_text]") + screentip.pixel_w = icon_x + screentip.pixel_z = icon_y + +/atom/movable/screen/minimap_display/proc/get_hover_text(x, y) + var/closest_blip_name + var/closest_blip_distance + for(var/atom/movable/screen/minimap_element/blip/blip in visible_minimap_elements) + if(isnull(blip.track_target) || blip.track_target.z != minimap.z) + continue + if(!length(blip.name)) + continue + var/hover_range = blip.large ? 2 : 1 + var/x_distance = abs(blip.track_target.x - x) + var/y_distance = abs(blip.track_target.y - y) + if(x_distance > hover_range || y_distance > hover_range) + continue + var/distance = x_distance + y_distance + if(isnull(closest_blip_distance) || distance < closest_blip_distance) + closest_blip_distance = distance + closest_blip_name = blip.name + + if(!isnull(closest_blip_name)) + return closest_blip_name + + var/area_name = minimap.map_position_to_name["[x]:[y]"] + if(isnull(area_name)) + var/turf/hovered_loc = locate(x, y, minimap.z) + area_name = "[hovered_loc?.loc?.name]" + minimap.map_position_to_name["[x]:[y]"] = area_name + return area_name + +/atom/movable/screen/minimap_display/MouseExited(location, control, params) + if(usr != get_mob()) + return + screentip.maptext = "" + if(active_button) + active_button.on_mouse_up() + +/atom/movable/screen/minimap_display/proc/on_z_level_change(mob/source) + SIGNAL_HANDLER + var/turf/mob_loc = get_turf(source) + if(!mob_loc || mob_loc.z != minimap.z) + if(isnull(fixed_z_level)) + INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), mob_loc.z) + return + remove_blip(locator_blip_tag) + return + add_blip(locator_blip_tag, "locator", mob_loc.x, mob_loc.y, layer = 15) + + +/atom/movable/screen/minimap_display/proc/resolve_and_change_z_level(new_z) + if(isnull(new_z)) + return + var/datum/minimap/new_minimap = get_minimap_for_z(new_z) + if(QDELETED(src) || isnull(new_minimap)) + return + change_z_level(new_z, new_minimap) + +/atom/movable/screen/minimap_display/proc/change_z_level(new_z, datum/minimap/new_minimap) + if(isnull(new_minimap)) + return + if(!isnull(fixed_z_level)) + fixed_z_level = new_z + set_minimap(new_minimap) + +/atom/movable/screen/minimap_display/proc/set_fixed_z_level(new_fixed_z, apply_immediately = FALSE) + fixed_z_level = new_fixed_z + if(apply_immediately) + INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), new_fixed_z) + +/atom/movable/screen/minimap_display/proc/show_tagged_blips() + if(!length(valid_minimap_blip_tags)) + return + for(var/blip_flag in valid_minimap_blip_tags) + var/blip_list = GLOB.minimap_blip_tags[blip_flag] + if(!blip_list) + continue + for(var/atom/movable/screen/minimap_element/blip/blip as anything in blip_list) + on_tagged_blip_add(null, blip) + +/atom/movable/screen/minimap_display/proc/on_tagged_blip_add(datum/source, atom/movable/screen/minimap_element/blip/blip) + SIGNAL_HANDLER + if(isnull(blip) || QDELETED(blip) || isnull(minimap)) + return + if(!(blip.blip_tag in valid_minimap_blip_tags)) + return + if(isnull(blip.track_target)) + return + var/turf/blip_turf = get_turf(blip.track_target) + if(blip_turf?.z != minimap.z) + return + blip.start_tracking_target() + show_minimap_element(blip) + +/atom/movable/screen/minimap_display/proc/on_tagged_blip_remove(datum/source, atom/movable/screen/minimap_element/blip/blip) + SIGNAL_HANDLER + if(isnull(blip)) + return + hide_minimap_element(blip) + +/atom/movable/screen/minimap_display/proc/set_minimap(datum/minimap/minimap) + if(isnull(minimap)) + return + icon = minimap.base_map + var/map_w = minimap.base_map.Width() + var/map_h = minimap.base_map.Height() + var/screen_size = get_screen_pixel_size() + var/map_offset_y = 32 + screen_loc = "1:[screen_size / 2 - map_w / 2],1:[screen_size / 2 - map_h / 2 - map_offset_y]" + origin_px = params2screenpixel(screen_loc) + src.minimap = minimap + refresh_visible_annotations() + screentip?.maptext = "" + clear_tagged_blips() + show_tagged_blips() + reposition_toolbar_buttons() + +/// adds a local blip that's not added to the global list +/atom/movable/screen/minimap_display/proc/add_blip(name, icon_state, x, y, large = FALSE, layer = 12) + if(blips[name]) + return + var/atom/movable/screen/minimap_element/blip/new_blip = new(null, null, get_mob(), icon_state, large) + new_blip.layer = layer + new_blip.start_tracking_target() + blips[name] = new_blip + show_minimap_element(new_blip) + +/atom/movable/screen/minimap_display/proc/update_blip(name, x, y) + var/atom/movable/screen/minimap_element/blip/blip = blips[name] + if(!blip) + return + var/half_size = blip.large ? 5 : 3 + blip.pixel_w = MINIMAP_WORLD_TO_PIXEL(x, minimap.min_x, half_size) + blip.pixel_z = MINIMAP_WORLD_TO_PIXEL(y, minimap.min_y, half_size) + +/atom/movable/screen/minimap_display/proc/remove_blip(name) + var/atom/movable/screen/minimap_element/blip/blip = blips[name] + if(!blip) + return + blips -= name + hide_minimap_element(blip) + qdel(blip) + +/atom/movable/screen/minimap_display/proc/remove_all_blips() + for(var/blip_name in blips) + var/atom/movable/screen/minimap_element/blip/blip = blips[blip_name] + hide_minimap_element(blip) + qdel(blip) + blips.Cut() + hide_visible_elements_by_type(/atom/movable/screen/minimap_element/blip) + +/atom/movable/screen/minimap_display/proc/z_change_request(mob/hud_owner, new_z_change) + SIGNAL_HANDLER + var/current_z = get_viewed_z_level() + var/new_z = get_clamped_connected_z(current_z + new_z_change, current_z) + INVOKE_ASYNC(src, PROC_REF(resolve_and_change_z_level), new_z) + +/atom/movable/screen/minimap_display/proc/get_viewed_z_level() + if(!isnull(fixed_z_level)) + return fixed_z_level + return minimap?.z + +/atom/movable/screen/minimap_display/proc/get_clamped_connected_z(requested_z, source_z) + if(isnull(source_z)) + return requested_z + var/list/connected_levels = SSmapping.get_connected_levels(source_z) + if(!length(connected_levels)) + return requested_z + if(requested_z in connected_levels) + return requested_z + var/closest_z = connected_levels[1] + var/closest_distance = abs(closest_z - requested_z) + for(var/connected_z in connected_levels) + var/current_distance = abs(connected_z - requested_z) + if(current_distance < closest_distance) + closest_z = connected_z + closest_distance = current_distance + return closest_z + +/// Activates a toolbar button as the active tool. +/atom/movable/screen/minimap_display/proc/activate_button(atom/movable/screen/minimap_toolbar_button/button) + if(!button) + return + + // Deselect if clicking the same button + if(active_button == button) + deactivate_button() + return + + deactivate_button() + + active_button = button + button.on_activate() + update_toolbar_button_states() + +/// Deactivates the current tool button. +/atom/movable/screen/minimap_display/proc/deactivate_button() + if(active_button) + active_button.on_deactivate() + active_button = null + update_toolbar_button_states() + +/// Sets the mouse cursor icon for the HUD client. Pass null to reset to default. +/atom/movable/screen/minimap_display/proc/set_cursor_icon(icon/cursor_icon) + var/mob/owner = get_mob() + if(owner?.client) + owner.client.mouse_pointer_icon = cursor_icon + +/// Calculates the actual screen pixel size based on the client's view +/atom/movable/screen/minimap_display/proc/get_screen_pixel_size() + var/mob/owner = get_mob() + if(!owner?.client) + return SCREEN_PIXEL_SIZE // fallback to constant if no client + var/list/view_pixels = view_to_pixels(owner.client.view_size.getView()) + // Return the maximum dimension (typically square, but handle non-square views) + return max(view_pixels[1], view_pixels[2]) + +/atom/movable/screen/minimap_display/proc/update_toolbar_button_states() + if(!hud) + return + for(var/hud_key in toolbar_button_types) + var/atom/movable/screen/minimap_toolbar_button/button = hud.screen_objects[hud_key] + if(button) + button.update_active_state() + +/atom/movable/screen/minimap_display/proc/reposition_toolbar_buttons() + if(!hud || !minimap) + return + // origin_px[1] is the minimap's left edge; place toolbar one icon-width to its left. + var/btn_x = origin_px[1] - ICON_SIZE_X - 4 + if(btn_x < 0) + btn_x = origin_px[1] + minimap.base_map.Width() + 4 // fall back to right side + // Toolbar is vertically centered relative to the minimap's current position. + var/screen_size = get_screen_pixel_size() + var/toolbar_h = length(toolbar_button_types) * ICON_SIZE_Y + var/map_center_y = origin_px[2] + minimap.base_map.Height() / 2 + var/btn_top_y = clamp(map_center_y + toolbar_h / 2, toolbar_h, screen_size) + for(var/key in toolbar_button_types) + var/atom/movable/screen/minimap_toolbar_button/button = hud.screen_objects[key] + if(button) + button.screen_loc = "1:[btn_x],1:[btn_top_y - ICON_SIZE_Y - button.button_slot * ICON_SIZE_Y]" + +/atom/movable/screen/minimap_display/proc/clear_canvas(mob/user) + if(!can_draw) + return + drawing.clear_canvas(minimap?.base_map) + log_minimap_drawing("[key_name(user)] cleared the minimap canvas on z-level [minimap?.z]") + to_chat(user, span_warning("Cleared all minimap drawings.")) + +/atom/movable/screen/minimap_display/proc/clear_all_annotations(mob/user, annotation_type = /atom/movable/screen/minimap_element/label, annotation_type_name = "label") + var/alist/annotation_store = GLOB.minimap_annotations[annotation_share_tag] + var/alist/items_by_z = annotation_store?[annotation_type] + if(isnull(items_by_z)) + return + var/current_z = get_viewed_z_level() + var/list/items = items_by_z[current_z] + if(!length(items)) + return + QDEL_LIST(items) + items_by_z[current_z] = list() + refresh_visible_annotations() + sync_visible_objects(current_z) + var/user_name = user ? key_name(user) : "System" + to_chat(user, span_warning("Cleared all [annotation_type_name] annotations on z-level [current_z].")) + log_minimap_drawing("[user_name] has cleared all [annotation_type_name] annotations on z-level [current_z]") + +/atom/movable/screen/minimap_display/proc/async_place_label(mob/user, icon_x, icon_y) + var/x = clamp(MINIMAP_ICON_TO_WORLD(icon_x, minimap.min_x), 1, world.maxx) + var/y = clamp(MINIMAP_ICON_TO_WORLD(icon_y, minimap.min_y), 1, world.maxy) + var/area_name = minimap.map_position_to_name["[x]:[y]"] + if(isnull(area_name)) + var/turf/hovered_loc = locate(x, y, minimap.z) + area_name = "[hovered_loc?.loc?.name]" + minimap.map_position_to_name["[x]:[y]"] = area_name + var/label_text = tgui_input_text(user, "What would you like the label at [area_name] to say?", "Add Label", max_length = 25) + if(!label_text || QDELETED(src)) + return + var/list/filter_result = is_ic_filtered(label_text) + if(filter_result) + to_chat(user, span_warning("That label contained a word prohibited in IC chat! Consider reviewing the server rules.\n\"[label_text]\"")) + SSblackbox.record_feedback("tally", "ic_blocked_words", 1, LOWER_TEXT(config.ic_filter_regex.match)) + REPORT_CHAT_FILTER_TO_USER(src, filter_result) + log_filter("IC", label_text, filter_result) + return + var/atom/movable/screen/minimap_element/label/new_label = new + new_label.icon = 'icons/ui_icons/minimap/map_blips.dmi' + new_label.icon_state = "label" + new_label.maptext_x = 5 + new_label.maptext_y = 5 + new_label.maptext_width = 64 + new_label.maptext = MAPTEXT_TINY_UNICODE("[label_text]") + var/icon/label_icon = icon(new_label.icon, new_label.icon_state) + var/half_width = round(label_icon.Width() / 2) + var/half_height = round(label_icon.Height() / 2) + new_label.pixel_w = MINIMAP_WORLD_TO_PIXEL(x, minimap.min_x, half_width) + new_label.pixel_z = MINIMAP_WORLD_TO_PIXEL(y, minimap.min_y, half_height) + var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z) + labels_for_z += new_label + sync_visible_objects(minimap?.z) + log_minimap_drawing("[key_name(user)] placed label '[label_text]' at [area_name]") + +/atom/movable/screen/minimap_display/proc/get_or_create_annotation_list(annotation_type, z_level) + var/alist/annotation_store = GLOB.minimap_annotations[annotation_share_tag] + if(isnull(annotation_store)) + annotation_store = alist() + GLOB.minimap_annotations[annotation_share_tag] = annotation_store + var/alist/by_z = annotation_store[annotation_type] + if(isnull(by_z)) + by_z = alist() + annotation_store[annotation_type] = by_z + var/list/items = by_z[z_level] + if(isnull(items)) + items = list() + by_z[z_level] = items + return items + +/atom/movable/screen/minimap_display/proc/refresh_visible_annotations() + if(isnull(minimap)) + return + var/list/drawings = get_or_create_annotation_list(/atom/movable/screen/minimap_element/drawing, minimap.z) + var/atom/movable/screen/minimap_element/drawing/target_drawing = length(drawings) ? drawings[1] : null + if(isnull(target_drawing)) + target_drawing = new + target_drawing.clear_canvas(minimap.base_map) + drawings += target_drawing + if(drawing != target_drawing) + if(!isnull(drawing)) + hide_minimap_element(drawing) + drawing = target_drawing + show_minimap_element(drawing) + + hide_visible_elements_by_type(/atom/movable/screen/minimap_element/label) + var/list/labels_for_z = get_or_create_annotation_list(/atom/movable/screen/minimap_element/label, minimap.z) + if(length(labels_for_z)) + show_minimap_elements(labels_for_z) + show_minimap_element(screentip) + +/atom/movable/screen/minimap_display/proc/sync_visible_objects(z_level) + if(isnull(z_level)) + return + var/list/displays_for_tag = GLOB.minimap_annotation_viewers[annotation_share_tag] + if(!length(displays_for_tag)) + return + for(var/atom/movable/screen/minimap_display/display as anything in displays_for_tag) + if(QDELETED(display) || display.minimap?.z != z_level) + continue + display.refresh_visible_annotations() + +/atom/movable/screen/minimap_display/proc/show_minimap_element(atom/movable/screen/minimap_element/element) + if(isnull(element)) + return + if(!(element in visible_minimap_elements)) + visible_minimap_elements += element + vis_contents |= element + +/atom/movable/screen/minimap_display/proc/hide_minimap_element(atom/movable/screen/minimap_element/element) + if(isnull(element)) + return + visible_minimap_elements -= element + vis_contents -= element + +/atom/movable/screen/minimap_display/proc/show_minimap_elements(list/elements) + for(var/atom/movable/screen/minimap_element/element as anything in elements) + show_minimap_element(element) + +/atom/movable/screen/minimap_display/proc/hide_minimap_elements(list/elements) + for(var/atom/movable/screen/minimap_element/element as anything in elements) + hide_minimap_element(element) + +/atom/movable/screen/minimap_display/proc/hide_visible_elements_by_type(element_type) + if(isnull(element_type)) + return + for(var/atom/movable/screen/minimap_element/element as anything in visible_minimap_elements.Copy()) + if(istype(element, element_type)) + hide_minimap_element(element) + +/atom/movable/screen/minimap_display/proc/clear_tagged_blips() + for(var/atom/movable/screen/minimap_element/blip/blip as anything in visible_minimap_elements.Copy()) + if(!length(blip.blip_tag)) + continue + hide_minimap_element(blip) + +/atom/movable/screen/minimap_display/nuclear + annotation_share_tag = MINIMAP_ANNOTATION_TAG_NUCLEAR + valid_minimap_blip_tags = list(MINIMAP_BOMB_BLIP, MINIMAP_NUKEDISK_BLIP, MINIMAP_NUKEOP_BLIP, MINIMAP_NUKEOP_BORG_BLIP, MINIMAP_SYNDICATE_MECH_BLIP, MINIMAP_SYNDIE_TURRET_BLIP, MINIMAP_LADDER_BLIP, MINIMAP_STAIR_BLIP) + +#undef MINIMAP_LABEL_REMOVE_PIXEL_RANGE diff --git a/code/modules/minimap/hud/minimap_drawing.dm b/code/modules/minimap/hud/minimap_drawing.dm new file mode 100644 index 000000000000..11c81089c85a --- /dev/null +++ b/code/modules/minimap/hud/minimap_drawing.dm @@ -0,0 +1,59 @@ + +/atom/movable/screen/minimap_element/drawing + icon = 'icons/ui_icons/minimap/minimap.dmi' + +/atom/movable/screen/minimap_element/drawing/proc/clear_canvas(icon/base_map) + var/icon/new_icon = icon('icons/ui_icons/minimap/minimap.dmi') + if(base_map) + new_icon.Scale(base_map.Width(), base_map.Height()) + icon = new_icon + +/atom/movable/screen/minimap_element/drawing/proc/draw_box(box_color, start_x, start_y, end_x, end_y, erase_pixel_range = 0, erase_padding_multiplier = 0) + var/icon/new_icon = icon(src.icon) + if(!isnull(box_color) || !erase_padding_multiplier) + new_icon.DrawBox(box_color, start_x, start_y, end_x, end_y) + else + var/padding = erase_pixel_range * erase_padding_multiplier + new_icon.DrawBox(box_color, start_x - padding, start_y - padding, end_x + padding, end_y + padding) + icon = new_icon + +// Unapologetically yoinked from /proc/get_line() +/atom/movable/screen/minimap_element/drawing/proc/draw_line(line_color, x0, y0, x1, y1, erase_pixel_range = 0, erase_padding_multiplier = 0) + var/icon/canvas = icon(src.icon) + var/current_x = x0 + var/current_y = y0 + + var/x_distance = x1 - current_x + var/y_distance = y1 - current_y + + var/abs_x_distance = abs(x_distance) + var/abs_y_distance = abs(y_distance) + + var/x_distance_sign = sign(x_distance) + var/y_distance_sign = sign(y_distance) + + var/cx = abs_x_distance >> 1 + var/cy = abs_y_distance >> 1 + + // draw the start pixel + var/padding = isnull(line_color) && erase_padding_multiplier ? erase_pixel_range * erase_padding_multiplier : 0 + canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding) + + if(abs_x_distance >= abs_y_distance) + for(var/i in 0 to (abs_x_distance - 1)) + cy += abs_y_distance + if(cy >= abs_x_distance) + cy -= abs_x_distance + current_y += y_distance_sign + current_x += x_distance_sign + canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding) + else + for(var/i in 0 to (abs_y_distance - 1)) + cx += abs_x_distance + if(cx >= abs_y_distance) + cx -= abs_y_distance + current_x += x_distance_sign + current_y += y_distance_sign + canvas.DrawBox(line_color, current_x - padding, current_y - padding, current_x + 1 + padding, current_y + 1 + padding) + + src.icon = canvas diff --git a/code/modules/minimap/hud/minimap_element.dm b/code/modules/minimap/hud/minimap_element.dm new file mode 100644 index 000000000000..f146f61bf348 --- /dev/null +++ b/code/modules/minimap/hud/minimap_element.dm @@ -0,0 +1,12 @@ +/atom/movable/screen/minimap_element + name = "unknown" + layer = MINIMAP_LABELS_LAYER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + vis_flags = VIS_INHERIT_PLANE + /// the tag this blip is associated via in it's stored globalist + var/blip_tag = "" + +/atom/movable/screen/minimap_element/Destroy() + if(blip_tag) + LAZYREMOVE(GLOB.minimap_blip_tags[blip_tag], src) + return ..() diff --git a/code/modules/minimap/hud/minimap_label.dm b/code/modules/minimap/hud/minimap_label.dm new file mode 100644 index 000000000000..e7515ab0c882 --- /dev/null +++ b/code/modules/minimap/hud/minimap_label.dm @@ -0,0 +1,6 @@ + +/atom/movable/screen/minimap_element/label + name = "label" + maptext = "" + maptext_width = 96 + maptext_height = 96 diff --git a/code/modules/minimap/hud/minimap_toolbar.dm b/code/modules/minimap/hud/minimap_toolbar.dm new file mode 100644 index 000000000000..c32ecb3979cf --- /dev/null +++ b/code/modules/minimap/hud/minimap_toolbar.dm @@ -0,0 +1,198 @@ +#define MINIMAP_TOOLBAR_ERASE_RANGE 5 + +/atom/movable/screen/minimap_toolbar_button + icon = 'icons/ui_icons/minimap/minimap_buttons.dmi' + icon_state = "draw" + mouse_over_pointer = MOUSE_HAND_POINTER + /// The minimap display this button controls. + var/atom/movable/screen/minimap_display/display + /// Vertical slot index (0 = topmost). Used by reposition_toolbar_buttons to calculate screen_loc. + var/button_slot = 0 + /// Mouse cursor icon set when this button's tool is active. null = default cursor. + var/icon/mouse_icon = null + /// Draw color this button represents. null = erase mode. + var/draw_color = null + /// HUD key for this tool button. Used to track selection state. + var/tool_key = null + /// Coordinates of the last drag position during drawing. + var/last_drag_x + var/last_drag_y + +/atom/movable/screen/minimap_toolbar_button/Initialize(mapload, datum/hud/hud_owner, atom/movable/screen/minimap_display/minimap_display) + . = ..() + display = minimap_display + +/atom/movable/screen/minimap_toolbar_button/Destroy() + display = null + return ..() + +/atom/movable/screen/minimap_toolbar_button/MouseEntered(location, control, params) + add_filter("mouseover", 1, outline_filter(1, COLOR_LIME)) + if(desc) + openToolTip(usr, tip_src = display || src, params = params, title = name, content = desc) + +/atom/movable/screen/minimap_toolbar_button/MouseExited(location, control, params) + remove_filter("mouseover") + if(desc) + closeToolTip(usr) + +/// Returns TRUE if this button should be shown as active. +/atom/movable/screen/minimap_toolbar_button/proc/is_active() + return display && display.active_button == src + +/// Updates the button's visual state to show if it's active. +/atom/movable/screen/minimap_toolbar_button/proc/update_active_state() + if(is_active()) + add_filter("active", 1, outline_filter(2, COLOR_YELLOW)) + else + remove_filter("active") + +/// Called when this button is activated as the active tool. +/atom/movable/screen/minimap_toolbar_button/proc/on_activate() + display?.set_cursor_icon(mouse_icon) + +/// Called when this button is deactivated. +/atom/movable/screen/minimap_toolbar_button/proc/on_deactivate() + display?.set_cursor_icon(null) + last_drag_x = null + last_drag_y = null + +/// Called during mouse drag on the map. Override to implement tool behavior. +/atom/movable/screen/minimap_toolbar_button/proc/on_mouse_drag(x, y) + return FALSE + +/// Called on mouse up. Override to clean up tool state. +/atom/movable/screen/minimap_toolbar_button/proc/on_mouse_up() + last_drag_x = null + last_drag_y = null + +/// Called on click. Override to implement click behavior like label placement. +/atom/movable/screen/minimap_toolbar_button/proc/on_click(icon_x, icon_y, right_click) + return FALSE + +/atom/movable/screen/minimap_toolbar_button/draw + desc = "Left-drag on the map to draw." + +/atom/movable/screen/minimap_toolbar_button/draw/on_mouse_drag(x, y) + if(isnull(display) || isnull(display.drawing)) + return FALSE + var/icon_width = display.minimap?.base_map?.Width() + var/icon_height = display.minimap?.base_map?.Height() + if(isnull(icon_width) || isnull(icon_height)) + return FALSE + if(!ISINRANGE(x, 1, icon_width) || !ISINRANGE(y, 1, icon_height)) + last_drag_x = null + last_drag_y = null + return FALSE + + if(last_drag_x && last_drag_y) + display.drawing.draw_line(draw_color, last_drag_x, last_drag_y + display.draw_offset_y, x, y + display.draw_offset_y, 0, 1) + last_drag_x = x + last_drag_y = y + else + display.drawing.draw_box(draw_color, x, y + display.draw_offset_y, x + 1, y + 1 + display.draw_offset_y, 0, 1) + last_drag_x = x + last_drag_y = y + return TRUE + +/atom/movable/screen/minimap_toolbar_button/draw/Click(location, control, params) + if(usr == get_mob()) + display?.activate_button(src) + +/atom/movable/screen/minimap_toolbar_button/draw/red + button_slot = 0 + color = TACMAP_DRAWING_RED + draw_color = TACMAP_DRAWING_RED + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_red.dmi' + +/atom/movable/screen/minimap_toolbar_button/draw/yellow + button_slot = 1 + color = TACMAP_DRAWING_YELLOW + draw_color = TACMAP_DRAWING_YELLOW + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi' + +/atom/movable/screen/minimap_toolbar_button/draw/purple + button_slot = 2 + color = TACMAP_DRAWING_PURPLE + draw_color = TACMAP_DRAWING_PURPLE + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_purple.dmi' + +/atom/movable/screen/minimap_toolbar_button/draw/blue + button_slot = 3 + color = TACMAP_DRAWING_BLUE + draw_color = TACMAP_DRAWING_BLUE + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi' + +/// Erase tool — left-drag on the map to erase a line. +/atom/movable/screen/minimap_toolbar_button/erase + icon_state = "erase" + button_slot = 4 + desc = "Left-drag on the map to erase." + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/draw_erase.dmi' + +/atom/movable/screen/minimap_toolbar_button/erase/on_mouse_drag(x, y) + if(isnull(display) || isnull(display.drawing)) + return FALSE + var/icon_width = display.minimap?.base_map?.Width() + var/icon_height = display.minimap?.base_map?.Height() + if(isnull(icon_width) || isnull(icon_height)) + return FALSE + if(!ISINRANGE(x, 1, icon_width) || !ISINRANGE(y, 1, icon_height)) + last_drag_x = null + last_drag_y = null + return FALSE + + if(last_drag_x && last_drag_y) + display.drawing.draw_line(null, last_drag_x, last_drag_y + display.draw_offset_y, x, y + display.draw_offset_y, MINIMAP_TOOLBAR_ERASE_RANGE, 1) + last_drag_x = x + last_drag_y = y + else + display.drawing.draw_box(null, x, y + display.draw_offset_y, x + 1, y + 1 + display.draw_offset_y, MINIMAP_TOOLBAR_ERASE_RANGE, 1) + last_drag_x = x + last_drag_y = y + return TRUE + +/atom/movable/screen/minimap_toolbar_button/erase/Click(location, control, params) + if(usr == get_mob()) + display?.activate_button(src) + +/// Clear tool — removes all drawings and labels. +/atom/movable/screen/minimap_toolbar_button/clear + icon_state = "clear" + button_slot = 5 + desc = "Clear all drawings." + +/atom/movable/screen/minimap_toolbar_button/clear/is_active() + return FALSE + +/atom/movable/screen/minimap_toolbar_button/clear/Click(location, control, params) + if(usr == get_mob()) + display?.clear_canvas(usr) + +/// Label tool — click the map to place labels, right-click a label to remove it, right-click this button to clear all labels. +/atom/movable/screen/minimap_toolbar_button/label + icon_state = "label" + button_slot = 6 + desc = "Toggle label mode. Click the map to place a label. Right-click a nearby label on the map to remove it. Right-click this button to clear all labels." + mouse_icon = 'icons/ui_icons/minimap/minimap_mouse/label.dmi' + +/atom/movable/screen/minimap_toolbar_button/label/on_click(icon_x, icon_y, right_click) + if(isnull(display)) + return FALSE + if(right_click) + display.remove_nearest_label(icon_x, icon_y, usr) + return TRUE + // Place label asynchronously + INVOKE_ASYNC(display, /atom/movable/screen/minimap_display/proc/async_place_label, usr, icon_x, icon_y) + return TRUE + +/atom/movable/screen/minimap_toolbar_button/label/Click(location, control, params) + if(usr != get_mob()) + return + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + display.clear_all_annotations(usr, /atom/movable/screen/minimap_element/label) + else + display?.activate_button(src) + +#undef MINIMAP_TOOLBAR_ERASE_RANGE diff --git a/code/modules/minimap/hud/minimap_tracking_blip.dm b/code/modules/minimap/hud/minimap_tracking_blip.dm new file mode 100644 index 000000000000..d7c2d19433c4 --- /dev/null +++ b/code/modules/minimap/hud/minimap_tracking_blip.dm @@ -0,0 +1,90 @@ +/atom/movable/screen/minimap_element/blip + icon = 'icons/ui_icons/minimap/map_blips.dmi' + /// Is this a large blip? causes different pixel offsets to be applied + var/large = FALSE + /// Minimap datum for the current z-level this blip is on + var/datum/minimap/minimap + /// If we are tracking our target or not, to ensure we do not re-register multiple times + var/tracking = FALSE + /// what target we're essentially owned by, and will cause this blip to cleanup if it gets deleted + var/atom/track_target + /// Weak reference to connect_containers so container movement updates this blip. + var/datum/weakref/connect_ref + +/atom/movable/screen/minimap_element/blip/Initialize(mapload, datum/hud/hud_owner, atom/track_target, icon_state, icon, large = FALSE, blip_tag) + . = ..() + src.icon_state = icon_state + src.large = large + if(icon) + src.icon = icon + if(track_target) + register_target(track_target) + if(blip_tag) + src.blip_tag = blip_tag + +/atom/movable/screen/minimap_element/blip/Destroy() + clear_tracking_signals() + return ..() + +/atom/movable/screen/minimap_element/blip/proc/register_target(atom/target) + if(!isnull(track_target)) + CRASH("[type] attempted to register [target] while already tracking [track_target].") + if(isnull(target)) + CRASH("[type] attempted to register a null track target.") + RegisterSignal(target, COMSIG_QDELETING, TYPE_PROC_REF(/datum, selfdelete), override = TRUE) + track_target = target + name = target.name + +/atom/movable/screen/minimap_element/blip/proc/clear_tracking_signals() + tracking = FALSE + if(track_target) + UnregisterSignal(track_target, list(COMSIG_QDELETING, COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOVABLE_MOVED)) + track_target.maptext = null + track_target = null + QDEL_NULL(connect_ref) + +/atom/movable/screen/minimap_element/blip/proc/start_tracking_target() + if(tracking) + return + if(isnull(track_target)) + CRASH("[type] cannot start tracking without a registered target.") + RegisterSignal(track_target, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_target_z_changed)) + RegisterSignal(track_target, COMSIG_MOVABLE_MOVED, PROC_REF(on_tracked_or_container_moved)) + if(ismovable(track_target)) + var/static/list/container_connections = list( + COMSIG_MOVABLE_MOVED = PROC_REF(on_tracked_or_container_moved), + COMSIG_MOVABLE_Z_CHANGED = PROC_REF(on_target_z_changed), + ) + connect_ref = WEAKREF(AddComponent(/datum/component/connect_containers, track_target, container_connections)) + tracking = TRUE + INVOKE_ASYNC(src, PROC_REF(update_minimap)) + +/atom/movable/screen/minimap_element/blip/proc/update_minimap() + var/turf/target_turf = get_turf(track_target) + minimap = get_minimap_for_z(target_turf.z) + update_blip() + +/atom/movable/screen/minimap_element/blip/proc/on_target_z_changed(atom/movable/source, turf/old_turf, turf/new_turf, same_z_layer) + SIGNAL_HANDLER + if(isnull(track_target)) + return + var/turf/target_turf = get_turf(track_target) + if(isnull(minimap) || minimap.z != target_turf?.z) + INVOKE_ASYNC(src, PROC_REF(update_minimap)) + else + update_blip() + +/atom/movable/screen/minimap_element/blip/proc/on_tracked_or_container_moved(atom/movable/source, atom/old_loc) + SIGNAL_HANDLER + update_blip() + +/atom/movable/screen/minimap_element/blip/proc/update_blip() + SIGNAL_HANDLER + if(isnull(track_target) || isnull(minimap)) + return + name = track_target.name + var/turf/target_turf = get_turf(track_target) + var/half_size = large ? 5 : 3 + pixel_w = MINIMAP_WORLD_TO_PIXEL(target_turf.x, minimap.min_x, half_size) + pixel_z = MINIMAP_WORLD_TO_PIXEL(target_turf.y, minimap.min_y, half_size) + diff --git a/code/modules/minimap/minimap.dm b/code/modules/minimap/minimap.dm new file mode 100644 index 000000000000..a893ae13d38a --- /dev/null +++ b/code/modules/minimap/minimap.dm @@ -0,0 +1,84 @@ +/// Assoc list of z-levels to `/datum/minimap` instances. +GLOBAL_ALIST_EMPTY(minimaps) + +/// Represents a minimap for a single Z-level. +/datum/minimap + /// The Z-level this minimap was made for. + VAR_FINAL/z + /// The icon of the base map itself. + var/icon/base_map + /// Mapping of x/y coords to area names. + var/alist/map_position_to_name = alist() + /// Minimum world X coordinate included in the cropped map icon. + var/min_x = 1 + /// Minimum world Y coordinate included in the cropped map icon. + var/min_y = 1 + +/datum/minimap/proc/load_z(z) + . = FALSE + if(!isnum(z) || z > length(SSmapping.z_list)) + CRASH("Tried to create minimap for invalid Z-level ([z])") + + base_map = icon('icons/ui_icons/minimap/minimap.dmi') + src.z = z + + var/min_x = world.maxx + var/min_y = world.maxy + var/max_x = 1 + var/max_y = 1 + + map_position_to_name.Cut() + for(var/turf/location as anything in Z_TURFS(z)) + if(location.skip_minimap_rendering || isshuttleturf(location)) + continue + var/area/arealoc = location.loc + if(arealoc.skip_minimap_rendering) + continue + var/x = location.x + var/y = location.y + min_x = min(min_x, x) + min_y = min(min_y, y) + max_x = max(max_x, x) + max_y = max(max_y, y) + if(location.density) + base_map.DrawBox(location.tacmap_color, x, y) + continue + var/atom/movable/alttarget = (locate(/obj/machinery/door) in location) || (locate(/obj/structure/window) in location) || (locate(/obj/structure/fence) in location) + if(alttarget) + base_map.DrawBox(alttarget.tacmap_color, x, y) + continue + if(arealoc.tacmap_color) + base_map.DrawBox(BlendRGB(location.tacmap_color, arealoc.tacmap_color, 0.5), x, y) + continue + if(istype(location, /turf/open/floor/engine/hull)) + var/turf/turf_below = GET_TURF_BELOW(location) + var/area/below_area = turf_below?.loc + // we'll draw the below area's color but transparent + if(below_area?.tacmap_color) + var/list/below_color = rgb2num(below_area.tacmap_color) + base_map.DrawBox(rgb(below_color[1], below_color[2], below_color[3], 64), x, y) + continue + base_map.DrawBox(location.tacmap_color, x, y) + + src.min_x = min_x + src.min_y = min_y + + base_map.Crop(min_x, min_y, max_x, max_y) + base_map.Scale(base_map.Width() * MINIMAP_PIXEL_MULTIPLIER, base_map.Height() * MINIMAP_PIXEL_MULTIPLIER) + + return TRUE + +/// Gets the `/datum/minimap` for a Z-level - generating it if it hasn't been yet. +/proc/get_minimap_for_z(z) as /datum/minimap + var/static/generating_minimap = FALSE + UNTIL(!generating_minimap) + + if(GLOB.minimaps[z]) + return GLOB.minimaps[z] + + generating_minimap = TRUE + var/datum/minimap/minimap = new + if(minimap.load_z(z)) + GLOB.minimaps[z] = minimap + . = minimap + generating_minimap = FALSE diff --git a/code/modules/minimap/minimap_action.dm b/code/modules/minimap/minimap_action.dm new file mode 100644 index 000000000000..9c25d7bc553e --- /dev/null +++ b/code/modules/minimap/minimap_action.dm @@ -0,0 +1,141 @@ + +/datum/action/minimap + name = "Toggle Minimap" + button_icon = 'icons/hud/implants.dmi' + button_icon_state = "minimap" + /// Mob currently bound to z-change signal handling for auto-hide behavior. + var/mob/tracked_owner + /// Optional fixed z-level that anchors this action's minimap stack and permission checks. + var/fixed_z_level + /// Optional minimap blip tags to render on the rewrite minimap display. + var/list/minimap_blip_tags = list() + /// Optional annotation sharing tag for minimap drawings/labels. + var/annotation_share_tag + /// Whether this action's minimap display should allow drawing tools. + var/can_draw = FALSE + /// list of hud elements we add and remove and check for when this action is triggered + var/list/huds = list( + HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap, + HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display, + HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator, + HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up, + HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down + ) + +/datum/action/minimap/Trigger(mob/clicker, trigger_flags) + . = ..() + var/datum/hud/hud = clicker.hud_used + // Toggle off if already visible. + if(has_minimap_huds(hud)) + remove_huds(hud) + to_chat(clicker, span_notice("Minimap hidden.")) + return + + if(SEND_SIGNAL(clicker, COMSIG_MINIMAP_ACTION_TRIGGER) & COMSIG_MINIMAP_ACTION_TRIGGER_CANCEL) + return + + var/anchor_z = get_anchor_z_level(clicker?.z) + if(is_forbidden_minimap_z(anchor_z)) + to_chat(clicker, span_warning("The minimap cannot be used on this z-level.")) + clicker.balloon_alert(clicker, "invalid z-level!") + return + var/display_z = get_opening_display_z_level(anchor_z, clicker?.z) + + var/datum/minimap/minimap = get_minimap_for_z(display_z) + if(isnull(minimap)) + clicker.balloon_alert(clicker, "no minimap generated") + return + add_huds(hud, minimap, isnull(fixed_z_level) ? null : display_z) + to_chat(clicker, span_notice("Minimap shown.")) + +/datum/action/minimap/Grant(mob/grant_to) + . = ..() + set_tracked_owner(grant_to) + +/datum/action/minimap/Remove(mob/remove_from) + set_tracked_owner(null) + return ..() + +/datum/action/minimap/proc/has_minimap_huds(datum/hud/hud) + for(var/element in huds) + if(hud.screen_objects[element]) + return TRUE + return FALSE + +/datum/action/minimap/proc/get_open_minimap_display(datum/hud/hud) + if(isnull(hud) || !has_minimap_huds(hud)) + return null + return hud.screen_objects[HUD_TAC_MINIMAP] + +/datum/action/minimap/proc/add_huds(datum/hud/hud, datum/minimap/minimap, initial_display_z_level) + for(var/element in huds) + var/hud_element_type = huds[element] + var/instanced = new hud_element_type(null, hud, minimap, minimap_blip_tags, initial_display_z_level, annotation_share_tag, can_draw) + hud.add_screen_object(instanced, element, HUD_GROUP_STATIC, update_screen = TRUE) + +/datum/action/minimap/proc/set_tracked_owner(mob/new_owner) + if(tracked_owner == new_owner) + return + if(!isnull(tracked_owner)) + UnregisterSignal(tracked_owner, COMSIG_MOVABLE_Z_CHANGED) + tracked_owner = new_owner + if(!isnull(tracked_owner)) + RegisterSignal(tracked_owner, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_owner_z_changed)) + +/datum/action/minimap/proc/get_anchor_z_level(current_z_level) + return isnull(fixed_z_level) ? current_z_level : fixed_z_level + + +/datum/action/minimap/proc/get_opening_display_z_level(anchor_z_level, current_z_level) + var/list/connected_levels = SSmapping.get_connected_levels(anchor_z_level) + if(length(connected_levels) && connected_levels.Find(current_z_level)) + return current_z_level + return (length(connected_levels) ? connected_levels[1] : anchor_z_level) + +/datum/action/minimap/proc/is_forbidden_minimap_z(z_level) + if(isnull(z_level)) + return FALSE + return is_centcom_level(z_level) || is_reserved_level(z_level) + +/datum/action/minimap/proc/on_owner_z_changed(atom/movable/source, turf/old_turf, turf/new_turf, same_z_layer) + SIGNAL_HANDLER + INVOKE_ASYNC(src, PROC_REF(handle_owner_z_changed), source, new_turf?.z) + +/datum/action/minimap/proc/handle_owner_z_changed(mob/owner_mob, new_z_level) + var/datum/hud/owner_hud = owner_mob?.hud_used + if(isnull(get_open_minimap_display(owner_hud))) + return + + var/anchor_z = get_anchor_z_level(new_z_level) + if(is_forbidden_minimap_z(anchor_z)) + remove_huds(owner_hud) + to_chat(owner_mob, span_warning("The minimap closes on this z-level.")) + return + if(isnull(fixed_z_level)) + return + + var/display_z = get_opening_display_z_level(anchor_z, new_z_level) + var/atom/movable/screen/minimap_display/display = get_open_minimap_display(owner_hud) + if(isnull(display) || display.get_viewed_z_level() == display_z) + return + var/datum/minimap/minimap = get_minimap_for_z(display_z) + if(QDELETED(src) || isnull(minimap)) + return + display = get_open_minimap_display(owner_hud) + if(isnull(display) || display.get_viewed_z_level() == display_z) + return + display.change_z_level(display_z, minimap) + +/datum/action/minimap/nuclear + annotation_share_tag = MINIMAP_ANNOTATION_TAG_NUCLEAR + huds = list( + HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap, + HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display/nuclear, + HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator, + HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up, + HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down + ) + +/datum/action/minimap/proc/remove_huds(datum/hud/hud) + for(var/element in huds) + hud.remove_screen_object(element) diff --git a/code/modules/minimap/minimap_table.dm b/code/modules/minimap/minimap_table.dm new file mode 100644 index 000000000000..3af72ea76c47 --- /dev/null +++ b/code/modules/minimap/minimap_table.dm @@ -0,0 +1,225 @@ + +/obj/machinery/minimap_table + name = "reconnaissance platform" + desc = "This holographic projector displays a constant stream of tactical information, enabling cinematic planning and strategizing. \ + Currently, it appears to be monitoring a nearby station and tracking key targets for a clandestine operation." + icon = 'icons/obj/machines/minimap_table.dmi' + icon_state = "off" + processing_flags = START_PROCESSING_MANUALLY + light_range = 4 + light_power = 2 + light_color = LIGHT_COLOR_INTENSE_RED + light_system = OVERLAY_LIGHT + density = TRUE + bound_width = 64 + SET_BASE_PIXEL(0, 2) + /// Hologram icon sheet used for startup/idle/closing projection effects. + var/hologram_icon_file = 'icons/obj/machines/minimap_table_hologram.dmi' + /// Z-trait used to resolve the minimap level this table displays. + var/target_z_trait = ZTRAIT_STATION + /// Cached minimap datum currently displayed by this table. + var/datum/minimap/minimap + /// Users currently viewing the table-projected minimap HUD. + var/list/mob/viewers = list() + /// Whether the table is currently active and projecting. + var/active = FALSE + /// Whether startup animation/state is currently in progress. + var/startup = FALSE + /// Pixel X offset for projected hologram overlays. + var/animation_x = -15 + /// Pixel Y offset for projected hologram overlays. + var/animation_y = 18 + /// Startup/closing animation duration. + var/animation_duration = 5.1 SECONDS + /// Max distance at which users can interact with the table. + var/interactivity_range = 3 + /// Proximity monitor used to close viewers leaving interaction range. + var/datum/proximity_monitor/proximity + /// Light helper used for pulsing hologram illumination. + var/datum/light_middleman/middleman + /// Lowest alpha value used by the pulsing light animation. + var/flicker_min_alpha = 150 + /// HUD elements used by the holotable minimap view. + var/list/table_huds = list( + HUD_TAC_MINIMAP_DIMMER = /atom/movable/screen/fullscreen/dimmer/minimap, + HUD_TAC_MINIMAP = /atom/movable/screen/minimap_display/nuclear, + HUD_TAC_MINIMAP_Z_INDICATOR = /atom/movable/screen/minimap_z_indicator, + HUD_TAC_MINIMAP_Z_INDICATOR_UP = /atom/movable/screen/minimap_z_up, + HUD_TAC_MINIMAP_Z_INDICATOR_DOWN = /atom/movable/screen/minimap_z_down, + ) + +/obj/machinery/minimap_table/Initialize(mapload) + . = ..() + proximity = new(src, interactivity_range) + + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "holotable") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_pulsate)) + middleman.being_overriding_light() + +/obj/machinery/minimap_table/Destroy(force) + for(var/mob/viewer as anything in viewers) + remove_table_huds(viewer.hud_used) + viewers = null + QDEL_NULL(proximity) + QDEL_NULL(middleman) + minimap = null + return ..() + +/obj/machinery/minimap_table/post_machine_initialize() + . = ..() + INVOKE_ASYNC(src, PROC_REF(set_minimap)) + +/obj/machinery/minimap_table/RangedAttackOn(mob/attacker, list/modifiers) + if(get_dist(src, attacker) > interactivity_range) + return + interact(attacker) + +/obj/machinery/minimap_table/interact(mob/user) + . = ..() + if(!is_operational || isnull(minimap) || isnull(user.hud_used)) + return FALSE + if(!isnull(user.hud_used.screen_objects[HUD_TAC_MINIMAP])) + hide_minimap(user) + return TRUE + if(active) + show_minimap(user) + return TRUE + + addtimer(CALLBACK(src, PROC_REF(activate), user), animation_duration, TIMER_UNIQUE | TIMER_CLIENT_TIME) + + startup = TRUE + play_animation("startup") + return TRUE + +/obj/machinery/minimap_table/proc/play_animation(icon_state = "startup", duration = animation_duration) + var/image/img = image(hologram_icon_file, src, icon_state, ABOVE_MOB_LAYER, dir, animation_x, animation_y) + var/image/emissive_img = image(hologram_icon_file, src, icon_state, ABOVE_MOB_LAYER, dir, animation_x, animation_y) + emissive_img.plane = EMISSIVE_PLANE + emissive_img.color = _EMISSIVE_COLOR_NO_BLOOM(1) + + flick_overlay_global(img, GLOB.clients, duration) + flick_overlay_global(emissive_img, GLOB.clients, duration) + +/obj/machinery/minimap_table/proc/activate(mob/activator) + if(active || !is_operational) + return + startup = FALSE + active = TRUE + if(activator && get_dist(src, activator) <= interactivity_range) + show_minimap(activator) + update_appearance(UPDATE_OVERLAYS) + light_pulsate() + +/obj/machinery/minimap_table/proc/deactivate() + if(!active) + return + active = FALSE + update_appearance(UPDATE_OVERLAYS) + var/obj/effect/abstract/main_light = middleman.primary_intercept + animate(main_light, time = 1 SECONDS) + play_animation("closing") + +/obj/machinery/minimap_table/proc/light_pulsate() + SIGNAL_HANDLER + var/obj/effect/abstract/main_light = middleman.primary_intercept + var/matrix/center = matrix() + // center it since we're a 2x2 machine + center.Translate(16, 0) + main_light.transform = center + if(!active) + return + + + var/matrix/bigTransform = matrix() + var/matrix/smallTransform = matrix() + smallTransform.Add(center) + bigTransform.Add(center) + + bigTransform.Scale(1.25) + smallTransform.Scale(0.75) + + animate(main_light, alpha = flicker_min_alpha, time = 2 SECONDS, loop = -1, easing = SINE_EASING) + animate(alpha = 255, time = 2 SECONDS) + animate(transform = bigTransform, time = 3 SECONDS, loop = -1, flags = ANIMATION_PARALLEL, easing = SINE_EASING) + animate(transform = smallTransform, time = 3 SECONDS) + +/obj/machinery/minimap_table/proc/deactive_without_viewers() + if(!length(viewers)) + deactivate() + +/obj/machinery/minimap_table/proc/show_minimap(mob/user) + add_table_huds(user.hud_used) + viewers |= user + +/obj/machinery/minimap_table/proc/hide_minimap(mob/user) + remove_table_huds(user.hud_used) + viewers -= user + if(!length(viewers)) + addtimer(CALLBACK(src, PROC_REF(deactive_without_viewers)), 10 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE) + +/obj/machinery/minimap_table/proc/add_table_huds(datum/hud/hud) + var/target_z = resolve_target_z() + var/allow_draw = can_user_draw(hud?.mymob) + for(var/element in table_huds) + var/hud_element_type = table_huds[element] + var/instanced = new hud_element_type(null, hud, minimap, null, target_z, MINIMAP_ANNOTATION_TAG_NUCLEAR, allow_draw) + hud.add_screen_object(instanced, element, HUD_GROUP_STATIC, update_screen = TRUE) + +/obj/machinery/minimap_table/proc/can_user_draw(mob/user) + return HAS_TRAIT(user, TRAIT_MINIMAP_TABLE_DRAW) + +/obj/machinery/minimap_table/proc/remove_table_huds(datum/hud/hud) + for(var/element in table_huds) + hud.remove_screen_object(element) + +/obj/machinery/minimap_table/proc/resolve_target_z() + if(isnull(target_z_trait)) + return null + var/list/trait_levels = SSmapping.levels_by_trait(target_z_trait) + if(length(trait_levels)) + var/top_level + for(var/level in trait_levels) + if(isnull(top_level) || level > top_level) + top_level = level + return top_level + return null + +/obj/machinery/minimap_table/proc/set_minimap() + var/target_z = resolve_target_z() + minimap = get_minimap_for_z(target_z) + +/obj/machinery/minimap_table/on_set_is_operational() + update_appearance() + set_light_on(is_operational) + + if(!is_operational) + deactivate() + +/obj/machinery/minimap_table/update_overlays() + . = ..() + if(!is_operational) + return + . += mutable_appearance(icon, "idle") + . += emissive_appearance(icon, "idle", src) + if(active) + var/holo_state = "idle" + var/mutable_appearance/idle = mutable_appearance(hologram_icon_file, holo_state, ABOVE_MOB_LAYER) + idle.pixel_x = animation_x + idle.pixel_y = animation_y + . += idle + + var/mutable_appearance/emissive = emissive_appearance(hologram_icon_file, holo_state, src, ABOVE_MOB_LAYER, effect_type = EMISSIVE_NO_BLOOM) + emissive.pixel_x = animation_x + emissive.pixel_y = animation_y + . += emissive + +/obj/machinery/minimap_table/OnProximityExit(atom/movable/gone) + if(!active || !ismob(gone)) + return + var/mob/mob_gone = gone + var/list/adjacent = orange(interactivity_range, src) + if(mob_gone in adjacent) + return + if(mob_gone in viewers) + hide_minimap(gone) diff --git a/code/modules/mob/living/basic/bots/medbot/medbot.dm b/code/modules/mob/living/basic/bots/medbot/medbot.dm index 3e6a22b0c6d8..7e2bd3d85748 100644 --- a/code/modules/mob/living/basic/bots/medbot/medbot.dm +++ b/code/modules/mob/living/basic/bots/medbot/medbot.dm @@ -468,6 +468,9 @@ /mob/living/basic/bot/medbot/nukie/Initialize(mapload, new_skin) . = ..() + var/datum/action/minimap/nuclear/tacmap_action = new + tacmap_action.Grant(src) + add_minimap_blip(src, MINIMAP_NUKEOP_BLIP, "mediborg") RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_DISARMED, PROC_REF(nuke_disarm)) RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_ARMED, PROC_REF(nuke_arm)) RegisterSignal(SSdcs, COMSIG_GLOB_NUKE_DEVICE_DETONATING, PROC_REF(nuke_detonate)) diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm index d114b6d42a1e..f04fa3aea99d 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm @@ -252,6 +252,8 @@ var/datum/callback/got_disk = CALLBACK(src, PROC_REF(got_disk)) var/datum/callback/display_disk = CALLBACK(src, PROC_REF(display_disk)) AddComponent(/datum/component/nuclear_bomb_operator, got_disk, display_disk) + var/obj/item/implant/implanter = SSwardrobe.provide_type(/obj/item/implant/tacmap/nuclear/cayenne, src) + implanter.implant(src, null, TRUE) /mob/living/basic/carp/pet/cayenne/apply_colour() if (prob(RARE_CAYENNE_CHANCE)) diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm index af9b1e6d49fa..1c755a020781 100644 --- a/code/modules/mob/living/silicon/robot/robot_model.dm +++ b/code/modules/mob/living/silicon/robot/robot_model.dm @@ -920,6 +920,9 @@ ..() var/mob/living/silicon/robot/cyborg = loc cyborg.remove_faction(FACTION_SILICON) //ai turrets + add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "combatborg") + var/datum/action/minimap/nuclear/tacmap_action = new + tacmap_action.Grant(cyborg) /obj/item/robot_model/syndicate/remove_module(obj/item/removed_module) ..() @@ -952,6 +955,13 @@ model_traits = list(TRAIT_PUSHIMMUNE) hat_offset = list("north" = list(0, 3), "south" = list(0, 3), "east" = list(-1, 3), "west" = list(1, 3)) +/obj/item/robot_model/syndicate_medical/rebuild_modules() + ..() + var/mob/living/silicon/robot/cyborg = loc + add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "mediborg") + var/datum/action/minimap/nuclear/tacmap_action = new + tacmap_action.Grant(cyborg) + /obj/item/robot_model/saboteur name = "Syndicate Saboteur" basic_modules = list( @@ -982,6 +992,13 @@ canDispose = TRUE var/datum/weakref/thermal_vision_ref +/obj/item/robot_model/saboteur/rebuild_modules() + ..() + var/mob/living/silicon/robot/cyborg = loc + add_minimap_blip(cyborg, MINIMAP_NUKEOP_BORG_BLIP, "engiborg") + var/datum/action/minimap/nuclear/tacmap_action = new + tacmap_action.Grant(cyborg) + /datum/action/cooldown/borg_thermal name = "Toggle Thermal Night Vision" button_icon = 'icons/mob/actions/actions_mecha.dmi' diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 594094deefd6..79a47f4d2440 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -822,6 +822,8 @@ GLOBAL_LIST_INIT(slime_extract_auto_activate_reactions, init_slime_auto_activate var/obj/item/implant/radio/syndicate/imp = new(src) imp.implant(smart_mob, user) smart_mob.AddComponent(/datum/component/simple_access, list(ACCESS_SYNDICATE, ACCESS_MAINT_TUNNELS)) + var/obj/item/implant/implanter = SSwardrobe.provide_type(/obj/item/implant/tacmap/nuclear/cayenne, src) + implanter.implant(src, null, TRUE) /obj/item/slimepotion/sentience/nuclear/dangerous_horse name = "dangerous pony potion" diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index 265c43dce651..e008dbca98d2 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -654,7 +654,7 @@ operative team's body-cams. They can also pilot the shuttle remotely and view the station's camera net. \ If you're a meathead who's just here to kill people and don't care about strategising or intel, you'll still have someone to bear witness to your murder-spree!" item = /obj/item/antag_spawner/nuke_ops/overwatch - cost = 12 + cost = 10 purchasable_from = UPLINK_FIREBASE_OPS // ~~ Disposable Sentry Gun ~~ diff --git a/code/modules/vehicles/mecha/combat/gygax.dm b/code/modules/vehicles/mecha/combat/gygax.dm index 974ec17818a3..951035fdaa91 100644 --- a/code/modules/vehicles/mecha/combat/gygax.dm +++ b/code/modules/vehicles/mecha/combat/gygax.dm @@ -73,6 +73,10 @@ fire = 100 acid = 100 +/obj/vehicle/sealed/mecha/gygax/dark/Initialize(mapload) + . = ..() + add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech") + /obj/vehicle/sealed/mecha/gygax/dark/loaded/Initialize(mapload) . = ..() max_ammo() diff --git a/code/modules/vehicles/mecha/combat/honker.dm b/code/modules/vehicles/mecha/combat/honker.dm index f5ad579cda19..9c63dc718983 100644 --- a/code/modules/vehicles/mecha/combat/honker.dm +++ b/code/modules/vehicles/mecha/combat/honker.dm @@ -60,6 +60,10 @@ MECHA_ARMOR = 2, ) +/obj/vehicle/sealed/mecha/honker/dark/Initialize(mapload, built_manually) + . = ..() + add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech") + /obj/vehicle/sealed/mecha/honker/dark/loaded equip_by_category = list( MECHA_L_ARM = /obj/item/mecha_parts/mecha_equipment/weapon/honker, diff --git a/code/modules/vehicles/mecha/combat/marauder.dm b/code/modules/vehicles/mecha/combat/marauder.dm index e0e214242aa3..e316de70bf7b 100644 --- a/code/modules/vehicles/mecha/combat/marauder.dm +++ b/code/modules/vehicles/mecha/combat/marauder.dm @@ -161,6 +161,10 @@ fire = 100 acid = 100 +/obj/vehicle/sealed/mecha/marauder/mauler/Initialize(mapload) + . = ..() + add_minimap_blip(src, MINIMAP_SYNDICATE_MECH_BLIP, "syndiemech") + /obj/vehicle/sealed/mecha/marauder/mauler/loaded equip_by_category = list( MECHA_L_ARM = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/lmg, diff --git a/config/logging.txt b/config/logging.txt index cd9b38b62039..c116110bd464 100644 --- a/config/logging.txt +++ b/config/logging.txt @@ -50,6 +50,9 @@ LOG_MECHA ## log OOC channel LOG_OOC +## log minimap drawing +LOG_MINIMAP_DRAWING + ## log pda messages LOG_PDA diff --git a/icons/hud/actions.dmi b/icons/hud/actions.dmi index 6d51cc55534a12316c3e5a034267a772707c2de3..e8385331596b16edd2d981b021ef408451dabb71 100644 GIT binary patch literal 9680 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�@Qe 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
ffhl( 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 De1`lY diff --git a/icons/hud/implants.dmi b/icons/hud/implants.dmi index b85ab4573ff2ae02a4b2b1265f03121d59433d42..64ba66dc406829ec5cf618cefeb24a8d2726baa9 100644 GIT binary patch literal 3879 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 literal 8488 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 diff --git a/icons/obj/machines/minimap_table.dmi b/icons/obj/machines/minimap_table.dmi new file mode 100644 index 0000000000000000000000000000000000000000..bb68f2da612c99a757e86f3c1af51b70f4799cda GIT binary patch literal 991 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 Pj`T1C~oNb23EEfhS=Z>BSmam{=jf%+7=yL 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 W)*@!pmqq_TgK(a%elF{r5}E)c2kQ?2 literal 0 HcmV?d00001 diff --git a/icons/ui_icons/minimap/draw_tools.dmi b/icons/ui_icons/minimap/draw_tools.dmi new file mode 100644 index 0000000000000000000000000000000000000000..ba72a74475e458285c5c85a93aa049d7d30a83dd GIT binary patch literal 603 zcmV-h0;K(kP)w|7QQgSvtG`0004WQchCV=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+ z(=$pSoZ^zil2jm5DJiEkRf&r;C9|j)$Tj5ROe;#vO@*-GigFT@QgaBZs!Ywv$uB3U ztSB{wpqhfxqJkX4?n*66EKV)LrA%4D)z1YSC;(OyPr4o%xX}Or0aQsuK~zYI?Ulc3 z!!QuW-Ik;oqoI2d$zBk70iy>90YS$u)!e0M?dZqu=^V`ckt}-(w7E>D@4ow)QzUiW zgE1WmO9B_3oaQ6*RLJq-l72%(Rj(oHdIeF}p~XcRX7vzkxwNKlF7pPh+j&D%zsH}8ZHz2`PKl=a9}#2x zpm|E^iyDsNoB)6!0KgDd9<^Lg5xJfsj`FCr4C*wu4C*vD4HD5bNCZxaw7@B`mIcHo zZ2|S){HryBwE%=@nGrv9K@Fmi56mFHQs?i(O2ja%LqPpH#7-F&Ah=-xLKx)z-+C|f p{oi^o)cxOjFZ5}@zX$(3egVIoq3JS*IzIpa002ovPDHLkV1j6Q39A4A literal 0 HcmV?d00001 diff --git a/icons/ui_icons/minimap/map_blips.dmi b/icons/ui_icons/minimap/map_blips.dmi new file mode 100644 index 0000000000000000000000000000000000000000..ce7c1e579c9ac1045d8967adc31e29b742ea4cf9 GIT binary patch literal 886 zcmV-+1Bv{JP)sMiA*&iPU1_l5C z00RR9EXD=b007+F+)h_p(IX@H#X6Y*0sjCq|C&LWW;6c)0H|;`w9MLwoTpc4etnOb z92^`oGc*7H|Ns9p`PW9kz`!5#X*B=<00DGTPE!Ct=GbNc0097cR9JLGWpiV4X>fFD zZ*Bkpc$}4!!HUBm5Qfk3DWtuI(l%T6w3pISdf2`~7~>=vFdEdf`1UJVcAG;(EF%1l z-~Zz%uKDqNx!(Bc`^xTU9U0BP_{V0;vIj}u#>FSgTBN4+qjnxVMzxYylMo;ju2?3? zx>($?thUhMul`2|{RZq%dmG9goTGa(UXvx=yr%cDiV8xMMP5v-L*J{g=-F=$d7Mw5 zG8K4ZUmJ_3ahJz2v4KFf+>eL2Uk-gLDa}JKvI?ZcnG;VVgO(JVOStwwvQ!8u(G>*L zdO5g5Ny6Bfr<{POn>4U8odBPNj$L7gRXc~S`T2Glf6EW*L+>*2?V8sB00EjwL_t(I zjg68Gdx9Vog-MisuqY-(l9 zlQApc8PG2*Eht`Dg|crfYu7BBm1VivWP%*nZgXX;>aD6Qum*Iw!MGL_N$34Iq$Cd~xn)k2LyCBi zBsrJ5P617wyEk!)d8dhZ>1m1^_(fVv>f;`fZ1U<#;Wvk9rfG^+4!|UOAaRb^uX3cb z%(>c6bX@lHNL_!<_FpaDV=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#atwj&jMU8Z zj1mZ^xFoS8706UdFG@|#i%-cc&W_JdOH<Br?t8oAT0$WK$K~#9! z?Ug@h8gUfIKbNQ=6e)Btw3(%lVhBb8nQjPX2{`mmz))bvV7;>1nQ@amj<@p1^ ze%TT?20#RYlR?gsd6bv}fD%&(P6i>OqdU28=A2`}!K`zJwoz zKNhFuvLJ=P*Lo~)@|r9HN+Hss;*ZK>;`UPxdFhmTX(pU zQ5)M?rv`0jom~R|!&MIKU7S9VF@3O%0=UB93X@T~gIeXKKA`GxE?6+UB wS!MgePDpADEuqm@+-dD)e_1!5cyi zE*d|0q;uX!^CUx|PQ#p@D`&p+2x+X}p|Vj$^h8+Dqeng`b+p)wcz0S{?h+~vR?JCd z4YwEHFq7egwM3=PME7iV=-0C=2*%CQQQysTJ)2H8$EpiJL);A(E~tc0000>P)t-s0001hfPnx1|Ge`1v)TWl z+~%3gW`v!zK%W0*nKK8C$Yy3{5Ov*OUtc{}V+&fVGiCq?KX)@TGanxx1tLE)0A?2# z7Y`2)00027X)ZMY0007XQchCq$gGRCwCGmVs`AAQVO0ZPikp^6;+Z z|G#u_)sU(bD9dh4lcqTb3J)KrlX;m_GRA1Fl~PU9DBUrdUzBn`WEdQ8qS4w5zN4YQ z@ir1?FrGh0LW9wcpQh2Of0f(=c0i;Jluq&vrjpQ9< zOSdr;lH*-~;zD962LrP&<*<8kfkhv9k;2o+U>s+DeIYy+!Sn7#)_?{GgN2{6-$;jf z+=C$OUao-rRB)X4{N(s_9n?3L`qoz8Tx;9=Ukvc`BI&QLqNuJk6i{I3m9e zI&;7aKtY!!i15V%$c8Tz;1d$~LIL?tXIS}C0r|`20-5? XLoKo&Nev)Y00000NkvXXu0mjfK($|w literal 0 HcmV?d00001 diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_blue.dmi new file mode 100644 index 0000000000000000000000000000000000000000..bae80afea3c2292599ab551887ac63ac5d24b91b GIT binary patch literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJl&X-35|`BC)e_1!5cyiE*d|0q;uX!^CUx2Pwx&3;~?Y9#$F0@9!)wj zDMTUIv$NY+b@K{|6DCg{`DlBc@on8ORW#;r%mFnfhFRMsxNBE8Is@$s@pN$vskrs_ zk|Eat10I%xfB)b9vfOv_u_KJ(LT%DJqM|#d)?IV|f32)W!^wYo151zHqE@l9g0DZ$ z)e_1!5cyiE*d|0q;uX!^CUx2Pwx&3;~?Y9#$F0@9!)wj zDMTUIv$NY+b@K{|6DCg{`DlBc@on8ORW#;r%mFnfhFRMsxNBE8Is@$s^mK6yskrs_ z+(O<11|rOX{ zg2M`mO22;zF8KKMiI%sn*10q1gExd4Tr__0Nawtd=1GR4p57f6#zDrHjlC4+JeqW5 zQiwvZXJ@yu>gE*^Crq9^^3nD>o; z1qvVjp8@h2c(3hL0#b}6L4Lsu4$p3+0XdGIE{-7;x89yI)e_1!5cyiE*d|0q;uX!^CUx2Pwx&3;~?Y9#$F0@9!)wj zDMTUIv$NY+b@K{|6DCg{`DlBc@on8ORW#;r%mFnfhFRMsxNBE8Is@$s_jGX#skrra z(nih$4g!a_Tz?PEV!k0W^&x>{YH()zDpfnS{S3Iz<5tt>~5=~ z)8l0wN!r0B6FXc~9_`^Ua69l;Mao_3#dO6Mfx03_F{`~Tx3tAy01boFyt=akR{ E0608mH~;_u literal 0 HcmV?d00001 diff --git a/icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi b/icons/ui_icons/minimap/minimap_mouse/draw_yellow.dmi new file mode 100644 index 0000000000000000000000000000000000000000..5489f87ce4202a8a0570bdbe10568d1b2cbe619d GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJl&X-35|`BC)e_1!5cyiE*d|0q;uX!^CUx2Pwx&3;~?Y9#$F0@9!)wj zDMTUIv$NY+b@K{|6DCg{`DlBc@on8ORW#;r%mFnfhFRMsxNBE8Is@$s_H=O!skrra zk|9?^fXLyh|C_)16$k4UDODj6B`&GO$wiq3C7Jno3=9=> zg2M`mO22;zF8KKMiI%sn*10q1gExd4Tr__0Nawtd=1GR4p57f6#zDrHjlC4+JeqW5 zQiwvZXJ@yu>gE*^Crq9^^3nD>}%WQgJIe z;fKG{5r+Ss3_Exmq?k7{OZ#qNTp^)oClYg@;X=!8Rgq&%XL%HO7BU=CmR+#?NTBkH pbe777B4z=%o~A$pE;dO92IdKia@bN2wE|6L@O1TaS?83{1OVyQR|Eh6 literal 0 HcmV?d00001 diff --git a/tgstation.dme b/tgstation.dme index 66d0a6651369..7dd74e24b78a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -163,6 +163,7 @@ #include "code\__DEFINES\melee.dm" #include "code\__DEFINES\memory_defines.dm" #include "code\__DEFINES\mergers.dm" +#include "code\__DEFINES\minimap.dm" #include "code\__DEFINES\mining.dm" #include "code\__DEFINES\mob_spawn.dm" #include "code\__DEFINES\mobfactions.dm" @@ -2829,6 +2830,7 @@ #include "code\game\objects\items\implants\implant_spell.dm" #include "code\game\objects\items\implants\implant_stealth.dm" #include "code\game\objects\items\implants\implant_storage.dm" +#include "code\game\objects\items\implants\implant_tacmap.dm" #include "code\game\objects\items\implants\implantcase.dm" #include "code\game\objects\items\implants\implantchair.dm" #include "code\game\objects\items\implants\implanter.dm" @@ -5080,6 +5082,18 @@ #include "code\modules\meteors\meteor_spawning.dm" #include "code\modules\meteors\meteor_types.dm" #include "code\modules\meteors\meteor_waves.dm" +#include "code\modules\minimap\_minimap.dm" +#include "code\modules\minimap\minimap.dm" +#include "code\modules\minimap\minimap_action.dm" +#include "code\modules\minimap\minimap_table.dm" +#include "code\modules\minimap\hud\minimal_z_level.dm" +#include "code\modules\minimap\hud\minimap_dimmer.dm" +#include "code\modules\minimap\hud\minimap_display.dm" +#include "code\modules\minimap\hud\minimap_drawing.dm" +#include "code\modules\minimap\hud\minimap_element.dm" +#include "code\modules\minimap\hud\minimap_label.dm" +#include "code\modules\minimap\hud\minimap_toolbar.dm" +#include "code\modules\minimap\hud\minimap_tracking_blip.dm" #include "code\modules\mining\aux_base.dm" #include "code\modules\mining\fulton.dm" #include "code\modules\mining\machine_processing.dm" From f5d6769ace2e126879a7a465fa98b6bc1598abce Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:38:23 +0000 Subject: [PATCH 048/101] Automatic changelog for PR #96068 [ci skip] --- html/changelogs/AutoChangeLog-pr-96068.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-96068.yml diff --git a/html/changelogs/AutoChangeLog-pr-96068.yml b/html/changelogs/AutoChangeLog-pr-96068.yml new file mode 100644 index 000000000000..c42eda89c7d1 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-96068.yml @@ -0,0 +1,9 @@ +author: "EnterTheJake" +delete-after: True +changes: + - rscadd: "Tactical Maps have been added to the game!" + - rscadd: "Nuclear Operatives implants now grant the ability to open a Minimap, this map will display and show the names of various areas of the station, and the positioning of the Nukeops team,borgs,mechs,Cayenne and the nuke disk in real time!" + - rscadd: "Nukie leaders and OW agents can also draw and apply labels to the map." + - rscadd: "the \"Recoinassance Platform\" has been added to the Nukie Base, it allows displaying of the Tactical maps while on the Syndicate Base Z level." + - map: "The briefing room in the NukeOps base has been remapped to better accomodate the new Holographic table." + - map: "Changed the front shutters on the Syndicate Infiltrator to the more fitting Syndicate Shutters." \ No newline at end of file From 78a61f04a84fa194ee611dba5b8ea8f9d9384cdc Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:00:29 +0000 Subject: [PATCH 049/101] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-96068.yml | 9 --------- html/changelogs/archive/2026-06.yml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-96068.yml diff --git a/html/changelogs/AutoChangeLog-pr-96068.yml b/html/changelogs/AutoChangeLog-pr-96068.yml deleted file mode 100644 index c42eda89c7d1..000000000000 --- a/html/changelogs/AutoChangeLog-pr-96068.yml +++ /dev/null @@ -1,9 +0,0 @@ -author: "EnterTheJake" -delete-after: True -changes: - - rscadd: "Tactical Maps have been added to the game!" - - rscadd: "Nuclear Operatives implants now grant the ability to open a Minimap, this map will display and show the names of various areas of the station, and the positioning of the Nukeops team,borgs,mechs,Cayenne and the nuke disk in real time!" - - rscadd: "Nukie leaders and OW agents can also draw and apply labels to the map." - - rscadd: "the \"Recoinassance Platform\" has been added to the Nukie Base, it allows displaying of the Tactical maps while on the Syndicate Base Z level." - - map: "The briefing room in the NukeOps base has been remapped to better accomodate the new Holographic table." - - map: "Changed the front shutters on the Syndicate Infiltrator to the more fitting Syndicate Shutters." \ No newline at end of file diff --git a/html/changelogs/archive/2026-06.yml b/html/changelogs/archive/2026-06.yml index df4091ff3738..49f713342f01 100644 --- a/html/changelogs/archive/2026-06.yml +++ b/html/changelogs/archive/2026-06.yml @@ -220,6 +220,19 @@ medical crutches reduce it by 60%. - refactor: Refactor cane and crutch code logic into a modular walking aid component 2026-06-09: + EnterTheJake: + - rscadd: Tactical Maps have been added to the game! + - rscadd: Nuclear Operatives implants now grant the ability to open a Minimap, this + map will display and show the names of various areas of the station, and the + positioning of the Nukeops team,borgs,mechs,Cayenne and the nuke disk in real + time! + - rscadd: Nukie leaders and OW agents can also draw and apply labels to the map. + - rscadd: the "Recoinassance Platform" has been added to the Nukie Base, it allows + displaying of the Tactical maps while on the Syndicate Base Z level. + - map: The briefing room in the NukeOps base has been remapped to better accomodate + the new Holographic table. + - map: Changed the front shutters on the Syndicate Infiltrator to the more fitting + Syndicate Shutters. mcbalaam: - bugfix: Fixed some of the Big Manipulator behavior such as not working in strict mode, putting items inside itself and incorrectly targeting items due to misconfigured From 456f8ae1cc8afe5e23022fad2a71cf71a206c0a6 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:05:57 +0200 Subject: [PATCH 050/101] Prettifies RND techweb, adds some more info to designs (#96334) ## About The Pull Request Replaces normal ``