diff --git a/_maps/RandomZLevels/museum.dmm b/_maps/RandomZLevels/museum.dmm
index f19ee06ddb56c..7429c0b45382e 100644
--- a/_maps/RandomZLevels/museum.dmm
+++ b/_maps/RandomZLevels/museum.dmm
@@ -3147,7 +3147,6 @@
/obj/structure/transport/linear/tram/slow,
/obj/structure/thermoplastic,
/obj/effect/spawner/random/structure/closet_empty/crate/with_loot,
-/obj/effect/spawner/random/maintenance/five,
/turf/open/chasm/true/no_smooth,
/area/awaymission/museum)
"Cc" = (
diff --git a/_maps/map_files/Birdshot/birdshot.dmm b/_maps/map_files/Birdshot/birdshot.dmm
index 09cd9472764a6..192a6571d8a55 100644
--- a/_maps/map_files/Birdshot/birdshot.dmm
+++ b/_maps/map_files/Birdshot/birdshot.dmm
@@ -19620,13 +19620,9 @@
/obj/effect/turf_decal/siding/wood{
dir = 9
},
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/storage/fancy/candle_box,
/obj/machinery/light_switch/directional/west,
+/obj/structure/rack/skeletal,
/turf/open/floor/iron/grimy,
/area/station/service/library)
"hei" = (
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index 03dfa234b14be..6b8edaaba1092 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -24819,13 +24819,9 @@
/turf/open/floor/iron/white,
/area/station/science/ordnance/storage)
"iWB" = (
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/storage/fancy/candle_box,
/obj/machinery/light/small/directional/east,
+/obj/structure/rack/skeletal,
/turf/open/floor/engine/cult,
/area/station/service/library)
"iWD" = (
@@ -56637,10 +56633,6 @@
"uhI" = (
/obj/structure/cable,
/obj/effect/landmark/event_spawn,
-/obj/machinery/requests_console/directional/north{
- department = "Security";
- name = "Security Requests Console"
- },
/turf/open/floor/iron,
/area/station/security/checkpoint/customs)
"uhP" = (
diff --git a/_maps/shuttles/emergency_monastery.dmm b/_maps/shuttles/emergency_monastery.dmm
index 13236bfabbbab..080f2f2d4e18a 100644
--- a/_maps/shuttles/emergency_monastery.dmm
+++ b/_maps/shuttles/emergency_monastery.dmm
@@ -2433,16 +2433,12 @@
/turf/open/floor/carpet,
/area/shuttle/escape)
"Ju" = (
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/book/codex_gigas,
/obj/machinery/camera/directional/south{
c_tag = "Monastery Archives Aft";
network = list("ss13","monastery")
},
+/obj/structure/rack/skeletal,
/turf/open/floor/iron/dark,
/area/shuttle/escape)
"Jv" = (
diff --git a/_maps/shuttles/pirate_dutchman.dmm b/_maps/shuttles/pirate_dutchman.dmm
index 86b85f7b70edc..fb36638173ed1 100644
--- a/_maps/shuttles/pirate_dutchman.dmm
+++ b/_maps/shuttles/pirate_dutchman.dmm
@@ -464,15 +464,11 @@
/obj/effect/turf_decal/siding/wood{
dir = 8
},
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/food/grown/sugarcane,
/obj/item/food/grown/sugarcane,
/obj/item/food/grown/sugarcane,
/obj/item/reagent_containers/cup/bucket/wooden,
+/obj/structure/rack/skeletal,
/turf/open/floor/wood/airless,
/area/shuttle/pirate/flying_dutchman)
"vT" = (
@@ -550,17 +546,13 @@
dir = 10
},
/obj/machinery/light/floor,
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/reagent_containers/condiment/milk{
pixel_x = -5
},
/obj/item/reagent_containers/condiment/milk{
pixel_x = 5
},
+/obj/structure/rack/skeletal,
/turf/open/floor/wood/airless,
/area/shuttle/pirate/flying_dutchman)
"zE" = (
diff --git a/_maps/virtual_domains/pirates.dmm b/_maps/virtual_domains/pirates.dmm
index f48c9ff7eaea1..1d330adcc4ddf 100644
--- a/_maps/virtual_domains/pirates.dmm
+++ b/_maps/virtual_domains/pirates.dmm
@@ -474,14 +474,10 @@
/area/virtual_domain/fullbright)
"AF" = (
/obj/effect/mapping_helpers/burnt_floor,
-/obj/structure/rack{
- icon = 'icons/obj/fluff/general.dmi';
- icon_state = "minibar";
- name = "skeletal minibar"
- },
/obj/item/storage/bag/money/dutchmen{
pixel_y = 13
},
+/obj/structure/rack/skeletal,
/turf/open/floor/wood/parquet,
/area/virtual_domain)
"AP" = (
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index 1813198b77415..cd1c636c283ca 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -243,3 +243,6 @@
/// from /mob/proc/slip(): (knockdown_amonut, obj/slipped_on, lube_flags [mobs.dm], paralyze, force_drop)
#define COMSIG_MOB_SLIPPED "mob_slipped"
+
+/// from /mob/proc/key_down(): (key, client/client, full_key)
+#define COMSIG_MOB_KEYDOWN "mob_key_down"
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 5946f76b7a109..ee78e8fcb9fc3 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -341,6 +341,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_DEL_ON_SPACE_DUMP "del_on_hyperspace_leave"
/// We can walk up or around cliffs, or at least we don't fall off of it
#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"
/// Gets double arcade prizes
#define TRAIT_GAMERGOD "gamer-god"
#define TRAIT_GIANT "giant"
@@ -357,6 +359,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_NO_EXTINGUISH "no_extinguish"
/// Indicates if the mob is currently speaking with sign language
#define TRAIT_SIGN_LANG "sign_language"
+/// Trait given to mobs to indicate that they can catch papers thrown at them midair without trying,
+/// and make syndicate airplanes when folding paper up.
+#define TRAIT_PAPER_MASTER "paper_master"
/// This mob is able to use sign language over the radio.
#define TRAIT_CAN_SIGN_ON_COMMS "can_sign_on_comms"
/// nobody can use martial arts on this mob
@@ -634,6 +639,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Used by the honkspam element to avoid spamming the sound. Amusing considering its name.
#define TRAIT_HONKSPAMMING "trait_honkspamming"
+/// Required by the waddling element since there are multiple sources of it.
+#define TRAIT_WADDLING "trait_waddling"
///Used for managing KEEP_TOGETHER in [/atom/var/appearance_flags]
#define TRAIT_KEEP_TOGETHER "keep-together"
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index f009b6ccf0f6c..ae3d7ae24be24 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -41,6 +41,9 @@
/// Trait from light debugging
#define LIGHT_DEBUG_TRAIT "light-debug"
+/// Trait given by an Action datum
+#define ACTION_TRAIT "action"
+
#define CLOTHING_TRAIT "clothing"
#define HELMET_TRAIT "helmet"
/// inherited from the mask
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 4bc72354a8625..64b4b7202c1b6 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -64,6 +64,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_UNIQUE_IMMERSE" = TRAIT_UNIQUE_IMMERSE,
"TRAIT_VOIDSTORM_IMMUNE" = TRAIT_VOIDSTORM_IMMUNE,
"TRAIT_WAS_RENAMED" = TRAIT_WAS_RENAMED,
+ "TRAIT_WADDLING" = TRAIT_WADDLING,
"TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE,
"TRAIT_CHASM_STOPPER" = TRAIT_CHASM_STOPPER,
),
@@ -399,6 +400,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SHOCKIMMUNE" = TRAIT_SHOCKIMMUNE,
"TRAIT_SHOVE_KNOCKDOWN_BLOCKED" = TRAIT_SHOVE_KNOCKDOWN_BLOCKED,
"TRAIT_SIGN_LANG" = TRAIT_SIGN_LANG,
+ "TRAIT_PAPER_MASTER" = TRAIT_PAPER_MASTER,
"TRAIT_SILENT_FOOTSTEPS" = TRAIT_SILENT_FOOTSTEPS,
"TRAIT_SIXTHSENSE" = TRAIT_SIXTHSENSE,
"TRAIT_SKITTISH" = TRAIT_SKITTISH,
@@ -428,6 +430,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_TACKLING_FRAIL_ATTACKER" = TRAIT_TACKLING_FRAIL_ATTACKER,
"TRAIT_TACKLING_TAILED_DEFENDER" = TRAIT_TACKLING_TAILED_DEFENDER,
"TRAIT_TACKLING_WINGED_ATTACKER" = TRAIT_TACKLING_WINGED_ATTACKER,
+ "TRAIT_TACTICALLY_CAMOUFLAGED" = TRAIT_TACTICALLY_CAMOUFLAGED,
"TRAIT_TAGGER" = TRAIT_TAGGER,
"TRAIT_TEMPORARY_BODY" = TRAIT_TEMPORARY_BODY,
"TRAIT_TENACIOUS" = TRAIT_TENACIOUS,
diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm
index b81719bc808c3..84b5bd6d7d7c2 100644
--- a/code/_globalvars/traits/admin_tooling.dm
+++ b/code/_globalvars/traits/admin_tooling.dm
@@ -188,6 +188,7 @@ GLOBAL_LIST_INIT(admin_visible_traits, list(
"TRAIT_SHIFTY_EYES" = TRAIT_SHIFTY_EYES,
"TRAIT_SHOCKIMMUNE" = TRAIT_SHOCKIMMUNE,
"TRAIT_SIGN_LANG" = TRAIT_SIGN_LANG,
+ "TRAIT_PAPER_MASTER" = TRAIT_PAPER_MASTER,
"TRAIT_SILENT_FOOTSTEPS" = TRAIT_SILENT_FOOTSTEPS,
"TRAIT_SIXTHSENSE" = TRAIT_SIXTHSENSE,
"TRAIT_SKITTISH" = TRAIT_SKITTISH,
diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm
index 533e8f1e8dd48..92defc661016c 100644
--- a/code/_onclick/hud/action_button.dm
+++ b/code/_onclick/hud/action_button.dm
@@ -18,6 +18,8 @@
/// A weakref of the last thing we hovered over
/// God I hate how dragging works
var/datum/weakref/last_hovored_ref
+ /// overlay for keybind maptext
+ var/mutable_appearance/keybind_maptext
/atom/movable/screen/movable/action_button/Destroy()
if(our_hud)
@@ -48,6 +50,9 @@
return FALSE
var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, ALT_CLICK))
+ begin_creating_bind(usr)
+ return TRUE
if(LAZYACCESS(modifiers, SHIFT_CLICK))
var/datum/hud/our_hud = usr.hud_used
our_hud.position_action(src, SCRN_OBJ_DEFAULT)
@@ -61,6 +66,14 @@
linked_action.Trigger(trigger_flags = trigger_flags)
return TRUE
+/atom/movable/screen/movable/action_button/proc/begin_creating_bind(mob/user)
+ if(!isnull(linked_action.full_key))
+ linked_action.full_key = null
+ linked_action.update_button_status(src)
+ return
+ linked_action.full_key = tgui_input_keycombo(user, "Please bind a key for this action.")
+ linked_action.update_button_status(src)
+
// Entered and Exited won't fire while you're dragging something, because you're still "holding" it
// Very much byond logic, but I want nice behavior, so we fake it with drag
/atom/movable/screen/movable/action_button/MouseDrag(atom/over_object, src_location, over_location, src_control, over_control, params)
@@ -149,6 +162,15 @@
return
user.client.prefs.action_buttons_screen_locs -= "[name]_[id]"
+/atom/movable/screen/movable/action_button/proc/update_keybind_maptext(key)
+ cut_overlay(keybind_maptext)
+ if(!key)
+ return
+ keybind_maptext = new
+ keybind_maptext.maptext = MAPTEXT("[key]")
+ keybind_maptext.transform = keybind_maptext.transform.Translate(-4, length(key) > 1 ? -6 : 2) //with modifiers, its placed lower so cooldown is visible
+ add_overlay(keybind_maptext)
+
/**
* This is a silly proc used in hud code code to determine what icon and icon state we should be using
* for hud elements (such as action buttons) that don't have their own icon and icon state set.
@@ -241,7 +263,7 @@
action.HideFrom(src)
/atom/movable/screen/button_palette
- desc = "Drag buttons to move them
Shift-click any button to reset it
Alt-click this to reset all buttons"
+ desc = "Drag buttons to move them
Shift-click any button to reset it
Alt-click any button to begin binding it to a key
Alt-click this to reset all buttons"
icon = 'icons/hud/64x16_actions.dmi'
icon_state = "screen_gen_palette"
screen_loc = ui_action_palette
diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm
index 916bbed6e4e1d..71eaed14ea23f 100644
--- a/code/controllers/subsystem/atoms.dm
+++ b/code/controllers/subsystem/atoms.dm
@@ -1,4 +1,3 @@
-#define SUBSYSTEM_INIT_SOURCE "subsystem init"
SUBSYSTEM_DEF(atoms)
name = "Atoms"
init_order = INIT_ORDER_ATOMS
@@ -41,11 +40,16 @@ SUBSYSTEM_DEF(atoms)
if(initialized == INITIALIZATION_INSSATOMS)
return
- set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, SUBSYSTEM_INIT_SOURCE)
+ // Generate a unique mapload source for this run of InitializeAtoms
+ var/static/uid = 0
+ uid = (uid + 1) % (SHORT_REAL_LIMIT - 1)
+ var/source = "subsystem init [uid]"
+ set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, source)
// This may look a bit odd, but if the actual atom creation runtimes for some reason, we absolutely need to set initialized BACK
- CreateAtoms(atoms, atoms_to_return)
- clear_tracked_initalize(SUBSYSTEM_INIT_SOURCE)
+ CreateAtoms(atoms, atoms_to_return, source)
+ clear_tracked_initalize(source)
+ SSicon_smooth.free_deferred(source)
if(late_loaders.len)
for(var/I in 1 to late_loaders.len)
@@ -72,7 +76,7 @@ SUBSYSTEM_DEF(atoms)
#endif
/// Actually creates the list of atoms. Exists soley so a runtime in the creation logic doesn't cause initalized to totally break
-/datum/controller/subsystem/atoms/proc/CreateAtoms(list/atoms, list/atoms_to_return = null)
+/datum/controller/subsystem/atoms/proc/CreateAtoms(list/atoms, list/atoms_to_return = null, mapload_source = null)
if (atoms_to_return)
LAZYINITLIST(created_atoms)
@@ -90,7 +94,12 @@ SUBSYSTEM_DEF(atoms)
for(var/I in 1 to atoms.len)
var/atom/A = atoms[I]
if(!(A.flags_1 & INITIALIZED_1))
- CHECK_TICK
+ // Unrolled CHECK_TICK setup to let us enable/disable mapload based off source
+ if(TICK_CHECK)
+ clear_tracked_initalize(mapload_source)
+ stoplag()
+ if(mapload_source)
+ set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, mapload_source)
PROFILE_INIT_ATOM_BEGIN()
InitAtom(A, TRUE, mapload_arg)
PROFILE_INIT_ATOM_END(A)
@@ -107,7 +116,11 @@ SUBSYSTEM_DEF(atoms)
#ifdef TESTING
++count
#endif
- CHECK_TICK
+ if(TICK_CHECK)
+ clear_tracked_initalize(mapload_source)
+ stoplag()
+ if(mapload_source)
+ set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, mapload_source)
testing("Initialized [count] atoms")
@@ -117,6 +130,13 @@ SUBSYSTEM_DEF(atoms)
/datum/controller/subsystem/atoms/proc/map_loader_stop(source)
clear_tracked_initalize(source)
+/// Returns the source currently modifying SSatom's init behavior
+/datum/controller/subsystem/atoms/proc/get_initialized_source()
+ var/state_length = length(initialized_state)
+ if(!state_length)
+ return null
+ return initialized_state[state_length][1]
+
/// Use this to set initialized to prevent error states where the old initialized is overriden, and we end up losing all context
/// Accepts a state and a source, the most recent state is used, sources exist to prevent overriding old values accidentially
/datum/controller/subsystem/atoms/proc/set_tracked_initalized(state, source)
@@ -199,5 +219,3 @@ SUBSYSTEM_DEF(atoms)
var/initlog = InitLog()
if(initlog)
text2file(initlog, "[GLOB.log_directory]/initialize.log")
-
-#undef SUBSYSTEM_INIT_SOURCE
diff --git a/code/controllers/subsystem/icon_smooth.dm b/code/controllers/subsystem/icon_smooth.dm
index 4d5f3069eca6f..9e5e9ffa99de6 100644
--- a/code/controllers/subsystem/icon_smooth.dm
+++ b/code/controllers/subsystem/icon_smooth.dm
@@ -9,6 +9,7 @@ SUBSYSTEM_DEF(icon_smooth)
var/list/blueprint_queue = list()
var/list/smooth_queue = list()
var/list/deferred = list()
+ var/list/deferred_by_source = list()
/datum/controller/subsystem/icon_smooth/fire()
// We do not want to smooth icons of atoms whose neighbors are not initialized yet,
@@ -61,16 +62,30 @@ SUBSYSTEM_DEF(icon_smooth)
return SS_INIT_SUCCESS
+/// Releases a pool of delayed smooth attempts from a particular source
+/datum/controller/subsystem/icon_smooth/proc/free_deferred(source_to_free)
+ smooth_queue += deferred_by_source[source_to_free]
+ deferred_by_source -= source_to_free
+ if(!can_fire)
+ can_fire = TRUE
/datum/controller/subsystem/icon_smooth/proc/add_to_queue(atom/thing)
if(thing.smoothing_flags & SMOOTH_QUEUED)
return
thing.smoothing_flags |= SMOOTH_QUEUED
+ // If we're currently locked into mapload BY something
+ // Then put us in a deferred list that we release when this mapload run is finished
+ if(initialized && length(SSatoms.initialized_state) && SSatoms.initialized == INITIALIZATION_INNEW_MAPLOAD)
+ var/source = SSatoms.get_initialized_source()
+ LAZYADD(deferred_by_source[source], thing)
+ return
smooth_queue += thing
if(!can_fire)
can_fire = TRUE
/datum/controller/subsystem/icon_smooth/proc/remove_from_queues(atom/thing)
+ // Lack of removal from deferred_by_source is safe because the lack of SMOOTH_QUEUED will just free it anyway
+ // Hopefully this'll never cause a harddel (dies)
thing.smoothing_flags &= ~SMOOTH_QUEUED
smooth_queue -= thing
if(blueprint_queue)
diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm
index 80ee843bf8b94..bc894de9beee8 100644
--- a/code/controllers/subsystem/timer.dm
+++ b/code/controllers/subsystem/timer.dm
@@ -4,8 +4,6 @@
#define BUCKET_POS(timer) (((ROUND_UP((timer.timeToRun - timer.timer_subsystem.head_offset) / world.tick_lag)+1) % BUCKET_LEN) || BUCKET_LEN)
/// Gets the maximum time at which timers will be invoked from buckets, used for deferring to secondary queue
#define TIMER_MAX(timer_ss) (timer_ss.head_offset + TICKS2DS(BUCKET_LEN + timer_ss.practical_offset - 1))
-/// Max float with integer precision
-#define TIMER_ID_MAX (2**24)
/**
* # Timer Subsystem
@@ -731,4 +729,3 @@ SUBSYSTEM_DEF(timer)
#undef BUCKET_LEN
#undef BUCKET_POS
#undef TIMER_MAX
-#undef TIMER_ID_MAX
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index 75c9cf59aebf2..39e69ba9fa8fd 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -46,6 +46,10 @@
var/overlay_icon = 'icons/mob/actions/backgrounds.dmi'
/// This is the icon state for any FOREGROUND overlay icons on the button (such as borders)
var/overlay_icon_state
+
+ /// full key we are bound to
+ var/full_key
+
/// Toggles whether this action is usable or not
var/action_disabled = FALSE
@@ -110,6 +114,7 @@
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED)), PROC_REF(update_status_on_signal))
if(owner_has_control)
+ RegisterSignal(grant_to, COMSIG_MOB_KEYDOWN, PROC_REF(keydown), override = TRUE)
GiveAction(grant_to)
/// Remove the passed mob from being owner of our action
@@ -122,6 +127,7 @@
HideFrom(hud.mymob)
LAZYREMOVE(remove_from?.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
viewers = list()
+ UnregisterSignal(remove_from, COMSIG_MOB_KEYDOWN)
if(isnull(owner))
return
@@ -312,6 +318,7 @@
* force - whether an update is forced regardless of existing status
*/
/datum/action/proc/update_button_status(atom/movable/screen/movable/action_button/current_button, force = FALSE)
+ current_button.update_keybind_maptext(full_key)
if(IsAvailable())
current_button.color = rgb(255,255,255,255)
else
@@ -411,3 +418,14 @@
/// Checks if our action is actively selected. Used for selecting icons primarily.
/datum/action/proc/is_action_active(atom/movable/screen/movable/action_button/current_button)
return FALSE
+
+/datum/action/proc/keydown(mob/source, key, client/client, full_key)
+ SIGNAL_HANDLER
+ if(isnull(full_key) || full_key != src.full_key)
+ return
+ if(istype(source))
+ if(source.next_click > world.time)
+ return
+ else
+ source.next_click = world.time + CLICK_CD_RANGE
+ INVOKE_ASYNC(src, PROC_REF(Trigger))
diff --git a/code/datums/actions/mobs/assume_form.dm b/code/datums/actions/mobs/assume_form.dm
index a335d0e745dc5..03a1a38d3c84a 100644
--- a/code/datums/actions/mobs/assume_form.dm
+++ b/code/datums/actions/mobs/assume_form.dm
@@ -64,7 +64,7 @@
// important: do this at the very end because we might have SIGNAL_ADDTRAIT for this on the mob that's dependent on the above logic
SEND_SIGNAL(owner, COMSIG_ACTION_DISGUISED_APPEARANCE, target_atom)
- ADD_TRAIT(owner, TRAIT_DISGUISED, REF(src))
+ ADD_TRAIT(owner, TRAIT_DISGUISED, ACTION_TRAIT)
/// Resets the appearances of the mob to the default.
/datum/action/cooldown/mob_cooldown/assume_form/proc/reset_appearances()
@@ -85,4 +85,4 @@
owner.cut_overlays()
// important: do this very end because we might have SIGNAL_REMOVETRAIT for this on the mob that's dependent on the above logic
- REMOVE_TRAIT(owner, TRAIT_DISGUISED, REF(src))
+ REMOVE_TRAIT(owner, TRAIT_DISGUISED, ACTION_TRAIT)
diff --git a/code/datums/actions/mobs/lava_swoop.dm b/code/datums/actions/mobs/lava_swoop.dm
index b86a710fbf227..a6c8282fd10c0 100644
--- a/code/datums/actions/mobs/lava_swoop.dm
+++ b/code/datums/actions/mobs/lava_swoop.dm
@@ -14,11 +14,11 @@
/datum/action/cooldown/mob_cooldown/lava_swoop/Grant(mob/M)
. = ..()
- M.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE), REF(src))
+ M.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE), ACTION_TRAIT)
/datum/action/cooldown/mob_cooldown/lava_swoop/Remove(mob/M)
. = ..()
- M.remove_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE), REF(src))
+ M.remove_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE), ACTION_TRAIT)
/datum/action/cooldown/mob_cooldown/lava_swoop/Activate(atom/target_atom)
disable_cooldown_actions()
diff --git a/code/datums/actions/mobs/sign_language.dm b/code/datums/actions/mobs/sign_language.dm
index da375fd1f8577..20c1157f32652 100644
--- a/code/datums/actions/mobs/sign_language.dm
+++ b/code/datums/actions/mobs/sign_language.dm
@@ -42,17 +42,17 @@
SIGNAL_ADDTRAIT(TRAIT_MUTE),
SIGNAL_REMOVETRAIT(TRAIT_MUTE)
))
- REMOVE_TRAIT(grant_to, TRAIT_SIGN_LANG, TRAIT_GENERIC)
+ REMOVE_TRAIT(grant_to, TRAIT_SIGN_LANG, ACTION_TRAIT)
/datum/action/innate/sign_language/Activate()
active = TRUE
- ADD_TRAIT(owner, TRAIT_SIGN_LANG, TRAIT_GENERIC)
+ ADD_TRAIT(owner, TRAIT_SIGN_LANG, ACTION_TRAIT)
to_chat(owner, span_green("You are now communicating with sign language."))
build_all_button_icons(UPDATE_BUTTON_BACKGROUND)
/datum/action/innate/sign_language/Deactivate()
active = FALSE
- REMOVE_TRAIT(owner, TRAIT_SIGN_LANG, TRAIT_GENERIC)
+ REMOVE_TRAIT(owner, TRAIT_SIGN_LANG, ACTION_TRAIT)
to_chat(owner, span_green("You have stopped using sign language."))
build_all_button_icons(UPDATE_BUTTON_BACKGROUND)
diff --git a/code/datums/actions/mobs/sneak.dm b/code/datums/actions/mobs/sneak.dm
index 738bb7b70cf5d..521181fa19b15 100644
--- a/code/datums/actions/mobs/sneak.dm
+++ b/code/datums/actions/mobs/sneak.dm
@@ -16,7 +16,7 @@
/datum/action/cooldown/mob_cooldown/sneak/Remove(mob/living/remove_from)
if(HAS_TRAIT(remove_from, TRAIT_SNEAK))
remove_from.alpha = initial(remove_from.alpha)
- REMOVE_TRAIT(remove_from, TRAIT_SNEAK, name)
+ REMOVE_TRAIT(remove_from, TRAIT_SNEAK, ACTION_TRAIT)
return ..()
@@ -26,11 +26,11 @@
// Otherwise we get permanent invisbility exploits.
animate(owner, alpha = initial(owner.alpha), time = animation_time)
owner.balloon_alert(owner, "you reveal yourself")
- REMOVE_TRAIT(owner, TRAIT_SNEAK, name)
+ REMOVE_TRAIT(owner, TRAIT_SNEAK, ACTION_TRAIT)
else
animate(owner, alpha = sneak_alpha, time = animation_time)
owner.balloon_alert(owner, "you blend into the environment")
- ADD_TRAIT(owner, TRAIT_SNEAK, name)
+ ADD_TRAIT(owner, TRAIT_SNEAK, ACTION_TRAIT)
return TRUE
diff --git a/code/datums/ai/objects/vending_machines/vending_machine_controller.dm b/code/datums/ai/objects/vending_machines/vending_machine_controller.dm
index cd779b7d691f0..50523db946ca6 100644
--- a/code/datums/ai/objects/vending_machines/vending_machine_controller.dm
+++ b/code/datums/ai/objects/vending_machines/vending_machine_controller.dm
@@ -14,7 +14,7 @@
return AI_CONTROLLER_INCOMPATIBLE
var/obj/machinery/vending/vendor_pawn = new_pawn
vendor_pawn.tiltable = FALSE //Not manually tiltable by hitting it anymore. We are now aggressively doing it ourselves.
- vendor_pawn.AddElement(/datum/element/waddling)
+ vendor_pawn.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
vendor_pawn.AddElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE)
vendor_pawn.squish_damage = 15
return ..() //Run parent at end
@@ -22,7 +22,7 @@
/datum/ai_controller/vending_machine/UnpossessPawn(destroy)
var/obj/machinery/vending/vendor_pawn = pawn
vendor_pawn.tiltable = TRUE
- vendor_pawn.RemoveElement(/datum/element/waddling)
+ REMOVE_TRAIT(vendor_pawn, TRAIT_WADDLING, REF(src))
vendor_pawn.squish_damage = initial(vendor_pawn.squish_damage)
RemoveElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE)
return ..() //Run parent at end
diff --git a/code/datums/components/tactical.dm b/code/datums/components/tactical.dm
index e8e5492694928..e0f131258e732 100644
--- a/code/datums/components/tactical.dm
+++ b/code/datums/components/tactical.dm
@@ -1,5 +1,8 @@
+///A simple component that replacess the user's appearance with that of the parent item when equipped.
/datum/component/tactical
+ ///The allowed slot(s) for the effect.
var/allowed_slot
+ ///A cached of where the item is currently equipped.
var/current_slot
/datum/component/tactical/Initialize(allowed_slot)
@@ -11,50 +14,63 @@
/datum/component/tactical/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(modify))
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(unmodify))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATED_ICON, PROC_REF(tactical_update))
+ var/obj/item/item = parent
+ if(ismob(item.loc))
+ var/mob/holder = item.loc
+ modify(item, holder, holder.get_slot_by_item(item))
/datum/component/tactical/UnregisterFromParent()
- UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED))
+ UnregisterSignal(parent, list(
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_ITEM_DROPPED,
+ COMSIG_ATOM_UPDATED_ICON,
+ ))
unmodify()
/datum/component/tactical/Destroy()
unmodify()
return ..()
-/datum/component/tactical/proc/on_z_move(datum/source)
- SIGNAL_HANDLER
- var/obj/item/master = parent
- if(!ismob(master.loc))
- return
- var/old_slot = current_slot
- unmodify(master, master.loc)
- modify(master, master.loc, old_slot)
-
/datum/component/tactical/proc/modify(obj/item/source, mob/user, slot)
SIGNAL_HANDLER
if(allowed_slot && !(slot & allowed_slot))
- unmodify()
+ if(current_slot)
+ unmodify(source, user)
return
+ if(current_slot) //If the current slot is set, this means the icon was updated or the item changed z-levels.
+ user.remove_alt_appearance("sneaking_mission[REF(src)]")
+ else
+ RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(tactical_update))
+
current_slot = slot
var/obj/item/master = parent
- var/image/I = image(icon = master.icon, icon_state = master.icon_state, loc = user)
- I.copy_overlays(master)
- I.override = TRUE
- source.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/everyone, "sneaking_mission", I)
- I.layer = ABOVE_MOB_LAYER
- RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_move))
+ var/image/image = image(master, loc = user)
+ image.copy_overlays(master)
+ image.override = TRUE
+ image.layer = ABOVE_MOB_LAYER
+ image.plane = FLOAT_PLANE
+ source.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/everyone, "sneaking_mission[REF(src)]", image)
/datum/component/tactical/proc/unmodify(obj/item/source, mob/user)
SIGNAL_HANDLER
- var/obj/item/master = source || parent
+ var/obj/item/master = parent
if(!user)
if(!ismob(master.loc))
return
user = master.loc
- user.remove_alt_appearance("sneaking_mission")
+ user.remove_alt_appearance("sneaking_mission[REF(src)]")
current_slot = null
UnregisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED)
+
+/datum/component/tactical/proc/tactical_update(datum/source)
+ SIGNAL_HANDLER
+ var/obj/item/master = parent
+ if(!ismob(master.loc))
+ return
+ modify(master, master.loc, current_slot)
diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm
index bcafc83497cfc..b2e021ad2be17 100644
--- a/code/datums/elements/_element.dm
+++ b/code/datums/elements/_element.dm
@@ -28,7 +28,7 @@
if(element_flags & ELEMENT_DETACH_ON_HOST_DESTROY)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(OnTargetDelete), override = TRUE)
-/datum/element/proc/OnTargetDelete(datum/source, force)
+/datum/element/proc/OnTargetDelete(datum/source)
SIGNAL_HANDLER
Detach(source)
@@ -75,3 +75,29 @@
ele.Detach(arglist(arguments))
else
ele.Detach(src)
+
+/**
+ * Used to manage (typically non_bespoke) elements with multiple sources through traits
+ * so we don't have to make them a components again.
+ * The element will be later removed once all trait sources are gone, there's no need of a
+ * "RemoveElementTrait" counterpart.
+ */
+/datum/proc/AddElementTrait(trait, source, datum/element/eletype, ...)
+ if(!ispath(eletype, /datum/element))
+ CRASH("AddElementTrait called, but [eletype] is not of a /datum/element path")
+ ADD_TRAIT(src, trait, source)
+ if(HAS_TRAIT_NOT_FROM(src, trait, source))
+ return
+ var/list/arguments = list(eletype)
+ /// 3 is the length of fixed args of this proc, any further one is passed down to AddElement.
+ if(length(args) > 3)
+ arguments += args.Copy(4)
+ /// We actually pass down a copy of the arguments since it's manipulated by the end of the proc.
+ _AddElement(arguments.Copy())
+ var/datum/ele = SSdcs.GetElement(arguments)
+ ele.RegisterSignal(src, SIGNAL_REMOVETRAIT(trait), TYPE_PROC_REF(/datum/element, _detach_on_trait_removed))
+
+/datum/element/proc/_detach_on_trait_removed(datum/source, trait)
+ SIGNAL_HANDLER
+ Detach(source)
+ UnregisterSignal(source, SIGNAL_REMOVETRAIT(trait))
diff --git a/code/datums/elements/waddling.dm b/code/datums/elements/waddling.dm
index c51a1759768b5..e63d0329bb630 100644
--- a/code/datums/elements/waddling.dm
+++ b/code/datums/elements/waddling.dm
@@ -4,6 +4,8 @@
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
+ if(!HAS_TRAIT(target, TRAIT_WADDLING))
+ stack_trace("[type] added to [target] without adding TRAIT_WADDLING first. Please use AddElementTrait instead.")
RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Waddle))
/datum/element/waddling/Detach(datum/source)
diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm
index 06b6dbc76a857..c6b64cc3a7ad3 100644
--- a/code/datums/spawners_menu.dm
+++ b/code/datums/spawners_menu.dm
@@ -30,9 +30,9 @@
this["important_warning"] = ""
this["amount_left"] = 0
for(var/spawner_obj in GLOB.mob_spawners[spawner])
+ var/obj/effect/mob_spawn/ghost_role/mob_spawner = spawner_obj
if(!this["desc"])
if(istype(spawner_obj, /obj/effect/mob_spawn))
- var/obj/effect/mob_spawn/ghost_role/mob_spawner = spawner_obj
if(!mob_spawner.allow_spawn(user, silent = TRUE))
continue
this["you_are_text"] = mob_spawner.you_are_text
@@ -41,8 +41,9 @@
else
var/obj/object = spawner_obj
this["desc"] = object.desc
- this["amount_left"] += 1
- if(this["amount_left"] > 0)
+ this["amount_left"] += mob_spawner.uses
+ this["infinite"] += mob_spawner.infinite_use
+ if(this["amount_left"] > 0 || this["infinite"])
data["spawners"] += list(this)
for(var/mob_type in GLOB.joinable_mobs)
var/list/this = list()
diff --git a/code/game/objects/items/cardboard_cutouts.dm b/code/game/objects/items/cardboard_cutouts.dm
index f58ea90211d1e..97f9c29491eeb 100644
--- a/code/game/objects/items/cardboard_cutouts.dm
+++ b/code/game/objects/items/cardboard_cutouts.dm
@@ -14,11 +14,19 @@
var/deceptive = FALSE
/// What cutout datum we spawn at the start? Uses the name, not the path.
var/starting_cutout
+ /// Reference to the tactical component that should be deleted when the cutout is toppled.
+ var/datum/component/tactical/tacticool
/obj/item/cardboard_cutout/Initialize(mapload)
. = ..()
if(starting_cutout)
return INITIALIZE_HINT_LATELOAD
+ if(!pushed_over)
+ AddComponent(/datum/component/tactical)
+
+/obj/item/cardboard_cutout/Destroy()
+ tacticool = null
+ return ..()
/obj/item/cardboard_cutout/LateInitialize()
ASSERT(!isnull(starting_cutout))
@@ -33,6 +41,8 @@
ASSERT(!isnull(cutout), "No cutout found with name [starting_cutout]")
cutout.apply(src)
+ if(!pushed_over)
+ tacticool = AddComponent(/datum/component/tactical)
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/cardboard_cutout/attack_hand(mob/living/user, list/modifiers)
@@ -42,12 +52,22 @@
playsound(src, 'sound/weapons/genhit.ogg', 50, TRUE)
push_over()
+/obj/item/cardboard_cutout/equipped(mob/living/user, slot)
+ . = ..()
+ //Because of the tactical element, the user won't tilt left and right, but it'll still hop.
+ user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
+
+/obj/item/cardboard_cutout/dropped(mob/living/user)
+ . = ..()
+ REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src))
+
/obj/item/cardboard_cutout/proc/push_over()
appearance = initial(appearance)
desc = "[initial(desc)] It's been pushed over."
icon_state = "cutout_pushed_over"
remove_atom_colour(FIXED_COLOUR_PRIORITY)
pushed_over = TRUE
+ QDEL_NULL(tacticool)
/obj/item/cardboard_cutout/attack_self(mob/living/user)
if(!pushed_over)
@@ -57,6 +77,7 @@
icon = initial(icon)
icon_state = initial(icon_state) //This resets a cutout to its blank state - this is intentional to allow for resetting
pushed_over = FALSE
+ tacticool = AddComponent(/datum/component/tactical)
/obj/item/cardboard_cutout/attackby(obj/item/I, mob/living/user, params)
if(istype(I, /obj/item/toy/crayon))
@@ -100,7 +121,7 @@
for (var/datum/cardboard_cutout/cutout_subtype as anything in subtypesof(/datum/cardboard_cutout))
var/datum/cardboard_cutout/cutout = get_cardboard_cutout_instance(cutout_subtype)
appearances_by_name[cutout.name] = cutout
- possible_appearances[cutout.name] = image(icon = cutout.applied_appearance)
+ possible_appearances[cutout.name] = image(icon = cutout.preview_appearance)
var/new_appearance = show_radial_menu(user, src, possible_appearances, custom_check = CALLBACK(src, PROC_REF(check_menu), user, crayon), radius = 36, require_near = TRUE)
if(!new_appearance)
@@ -144,19 +165,16 @@
return FALSE
return TRUE
-// Cutouts always face forward
-/obj/item/cardboard_cutout/setDir(newdir)
- SHOULD_CALL_PARENT(FALSE)
- return
-
/obj/item/cardboard_cutout/adaptive //Purchased by Syndicate agents, these cutouts are indistinguishable from normal cutouts but aren't discolored when their appearance is changed
deceptive = TRUE
/datum/cardboard_cutout
/// Name of the cutout, used for radial selection and the global list.
var/name = "Boardjak"
- /// The appearance we apply to the cardboard cutout.
- var/mutable_appearance/applied_appearance = null
+ /// The appearance of the cardboard cutout that we show in the radial menu.
+ var/mutable_appearance/preview_appearance
+ /// A flat appearance, with only one direction, that we apply to the cardboard cutout.
+ var/image/applied_appearance
/// The base name we actually give to to the cardboard cutout. Can be overridden in get_name().
var/applied_name = "boardjak"
/// The desc we give to the cardboard cutout.
@@ -179,9 +197,9 @@
/datum/cardboard_cutout/New()
. = ..()
if(direct_icon)
- applied_appearance = mutable_appearance(direct_icon, direct_icon_state)
+ preview_appearance = mutable_appearance(direct_icon, direct_icon_state)
else
- applied_appearance = get_dynamic_human_appearance(outfit, species, mob_spawner, l_hand, r_hand, animated = FALSE)
+ preview_appearance = get_dynamic_human_appearance(outfit, species, mob_spawner, l_hand, r_hand, animated = FALSE)
/// This proc returns the name that the cardboard cutout item will use.
/datum/cardboard_cutout/proc/get_name()
@@ -189,9 +207,14 @@
/// This proc sets the cardboard cutout item's vars.
/datum/cardboard_cutout/proc/apply(obj/item/cardboard_cutout/cutouts)
+ if(isnull(applied_appearance))
+ applied_appearance = image(fcopy_rsc(getFlatIcon(preview_appearance, no_anim = TRUE)))
+ applied_appearance.plane = cutouts.plane
+ applied_appearance.layer = cutouts.layer
cutouts.appearance = applied_appearance
cutouts.name = get_name()
cutouts.desc = applied_desc
+ cutouts.update_appearance() //forces an update on the tactical comp's appearance.
/datum/cardboard_cutout/assistant
name = "Assistant"
diff --git a/code/game/objects/items/granters/oragami.dm b/code/game/objects/items/granters/oragami.dm
index 0b7d6d9261570..0691349756cf9 100644
--- a/code/game/objects/items/granters/oragami.dm
+++ b/code/game/objects/items/granters/oragami.dm
@@ -21,11 +21,13 @@
check_flags = NONE
/datum/action/innate/origami/Activate()
+ ADD_TRAIT(owner, TRAIT_PAPER_MASTER, ACTION_TRAIT)
to_chat(owner, span_notice("You will now fold origami planes."))
active = TRUE
build_all_button_icons(UPDATE_BUTTON_ICON)
/datum/action/innate/origami/Deactivate()
+ REMOVE_TRAIT(owner, TRAIT_PAPER_MASTER, ACTION_TRAIT)
to_chat(owner, span_notice("You will no longer fold origami planes."))
active = FALSE
build_all_button_icons(UPDATE_BUTTON_ICON)
diff --git a/code/game/objects/items/knives.dm b/code/game/objects/items/knives.dm
index 315862d2d79ff..8b4808848c245 100644
--- a/code/game/objects/items/knives.dm
+++ b/code/game/objects/items/knives.dm
@@ -144,7 +144,6 @@
user.apply_damage(5, BRUTE, BODY_ZONE_HEAD)
playsound(user, 'sound/weapons/slice.ogg', 50, TRUE)
user.visible_message(span_danger("[user] accidentally cuts [user.p_them()]self while pulling [src] out of [user.p_them()] teeth! What a doofus!"), span_userdanger("You accidentally cut your mouth with [src]!"))
- . = ..()
/obj/item/knife/combat/equipped(mob/living/user, slot, initial = FALSE)
. = ..()
diff --git a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
index 31600b03354c7..514ab36ed66d3 100644
--- a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
+++ b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm
@@ -404,7 +404,7 @@
. = ..()
if (!.)
return
- owner.AddElement(/datum/element/waddling)
+ owner.AddElementTrait(TRAIT_WADDLING, TRAIT_STATUS_EFFECT(id), /datum/element/waddling)
ADD_TRAIT(owner, TRAIT_NO_SLIP_WATER, TRAIT_STATUS_EFFECT(id))
slipperiness = owner.AddComponent(\
/datum/component/slippery,\
@@ -418,8 +418,7 @@
return owner.body_position == LYING_DOWN
/datum/status_effect/golem/bananium/on_remove()
- REMOVE_TRAIT(owner, TRAIT_NO_SLIP_WATER, TRAIT_STATUS_EFFECT(id))
- owner.RemoveElement(/datum/element/waddling)
+ owner.remove_traits(owner, list(TRAIT_WADDLING, TRAIT_NO_SLIP_WATER), TRAIT_STATUS_EFFECT(id))
QDEL_NULL(slipperiness)
return ..()
diff --git a/code/game/objects/items/tanks/tanks.dm b/code/game/objects/items/tanks/tanks.dm
index 6c11c43550718..a670a966805e7 100644
--- a/code/game/objects/items/tanks/tanks.dm
+++ b/code/game/objects/items/tanks/tanks.dm
@@ -440,6 +440,10 @@
if(LAZYLEN(assembly.assemblies) == igniter_count)
return
+
+ if(isitem(loc)) // we are in a storage item
+ balloon_alert(user, "can't reach!")
+ return
if((src in user.get_equipped_items(include_pockets = TRUE, include_accessories = TRUE)) && !user.canUnEquip(src))
balloon_alert(user, "it's stuck!")
@@ -456,6 +460,7 @@
tank_assembly = assembly //Tell the tank about its assembly part
assembly.master = src //Tell the assembly about its new owner
assembly.on_attach()
+ w_class = WEIGHT_CLASS_BULKY
balloon_alert(user, "bomb assembled")
update_appearance(UPDATE_OVERLAYS)
@@ -469,6 +474,7 @@
user.put_in_hands(tank_assembly)
tank_assembly.master = null
tank_assembly = null
+ w_class = initial(w_class)
update_appearance(UPDATE_OVERLAYS)
/// Ignites the contents of the tank. Called when receiving a signal if the tank is welded and has an igniter attached.
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index a0b8f9d79dcef..debd4d81bc367 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -825,6 +825,12 @@
pass_flags_self = LETPASSTHROW //You can throw objects over this, despite it's density.
max_integrity = 20
+/obj/structure/rack/skeletal
+ name = "skeletal minibar"
+ desc = "Rattle me boozes!"
+ icon = 'icons/obj/fluff/general.dmi'
+ icon_state = "minibar"
+
/obj/structure/rack/Initialize(mapload)
. = ..()
AddElement(/datum/element/climbable)
diff --git a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
index 44695b6104730..989156867491f 100644
--- a/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
+++ b/code/modules/atmospherics/machinery/components/gas_recipe_machines/crystallizer.dm
@@ -55,7 +55,7 @@
if(TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
if(TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "Rotate"
+ context[SCREENTIP_CONTEXT_RMB] = "Rotate"
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/atmospherics/components/binary/crystallizer/attackby(obj/item/I, mob/user, params)
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 96ad34b315868..3d930ce898904 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -213,7 +213,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
//Sanitize
lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog))
default_slot = sanitize_integer(default_slot, 1, max_save_slots, initial(default_slot))
- toggles = sanitize_integer(toggles, 0, (2**24)-1, initial(toggles))
+ toggles = sanitize_integer(toggles, 0, SHORT_REAL_LIMIT-1, initial(toggles))
be_special = sanitize_be_special(SANITIZE_LIST(be_special))
key_bindings = sanitize_keybindings(key_bindings)
favorite_outfits = SANITIZE_LIST(favorite_outfits)
diff --git a/code/modules/clothing/shoes/clown.dm b/code/modules/clothing/shoes/clown.dm
index dea346b8613b2..aff47fde7fabe 100644
--- a/code/modules/clothing/shoes/clown.dm
+++ b/code/modules/clothing/shoes/clown.dm
@@ -20,13 +20,13 @@
. = ..()
if(slot & ITEM_SLOT_FEET)
if(enabled_waddle)
- user.AddElement(/datum/element/waddling)
+ user.AddElementTrait(TRAIT_WADDLING, SHOES_TRAIT, /datum/element/waddling)
if(is_clown_job(user.mind?.assigned_role))
user.add_mood_event("clownshoes", /datum/mood_event/clownshoes)
/obj/item/clothing/shoes/clown_shoes/dropped(mob/living/user)
. = ..()
- user.RemoveElement(/datum/element/waddling)
+ REMOVE_TRAIT(user, TRAIT_WADDLING, SHOES_TRAIT)
if(is_clown_job(user.mind?.assigned_role))
user.clear_mood_event("clownshoes")
diff --git a/code/modules/clothing/shoes/costume.dm b/code/modules/clothing/shoes/costume.dm
index 1a3e9b0b2beb7..1e15f25e1f630 100644
--- a/code/modules/clothing/shoes/costume.dm
+++ b/code/modules/clothing/shoes/costume.dm
@@ -128,8 +128,8 @@
/obj/item/clothing/shoes/ducky_shoes/equipped(mob/living/user, slot)
. = ..()
if(slot & ITEM_SLOT_FEET)
- user.AddElement(/datum/element/waddling)
+ user.AddElementTrait(TRAIT_WADDLING, SHOES_TRAIT, /datum/element/waddling)
/obj/item/clothing/shoes/ducky_shoes/dropped(mob/living/user)
. = ..()
- user.RemoveElement(/datum/element/waddling)
+ REMOVE_TRAIT(user, TRAIT_WADDLING, SHOES_TRAIT)
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_martian.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_martian.dm
index fb4f29284b87b..02bb9ae7bf82c 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_martian.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_martian.dm
@@ -168,7 +168,7 @@
/obj/item/food/onion_slice = 1,
/datum/reagent/consumable/nutriment/soup/teriyaki = 4,
)
- result = /obj/item/food/salad/bibimbap
+ result = /obj/item/food/salad/bulgogi_noodles
category = CAT_MARTIAN
/datum/crafting_recipe/food/yakisoba_katsu
diff --git a/code/modules/keybindings/bindings_atom.dm b/code/modules/keybindings/bindings_atom.dm
index e9e38489a4df3..e99e3714c6b60 100644
--- a/code/modules/keybindings/bindings_atom.dm
+++ b/code/modules/keybindings/bindings_atom.dm
@@ -11,6 +11,9 @@
// If we're not movin anywhere, we aren't movin anywhere
// Safe because nothing adds to movement_dir after this moment
if(!movement_dir)
+ // No input == our removal would have done nothing
+ // So we can safely forget about it
+ user.next_move_dir_sub = NONE
return FALSE
if(user.next_move_dir_sub)
diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm
index 0a8cc20b38fe5..4a72fb9123da8 100644
--- a/code/modules/keybindings/bindings_client.dm
+++ b/code/modules/keybindings/bindings_client.dm
@@ -75,11 +75,10 @@
if(kb.can_use(src) && kb.down(src) && keycount >= MAX_COMMANDS_PER_KEY)
break
- holder?.key_down(_key, src)
- mob.focus?.key_down(_key, src)
+ holder?.key_down(_key, src, full_key)
+ mob.focus?.key_down(_key, src, full_key)
mob.update_mouse_pointer()
-
/client/verb/keyUp(_key as text)
set instant = TRUE
set hidden = TRUE
diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm
index ef87e12d90103..d239c48d9ceee 100644
--- a/code/modules/keybindings/setup.dm
+++ b/code/modules/keybindings/setup.dm
@@ -1,6 +1,7 @@
// Set a client's focus to an object and override these procs on that object to let it handle keypresses
-/datum/proc/key_down(key, client/user) // Called when a key is pressed down initially
+/datum/proc/key_down(key, client/user, full_key) // Called when a key is pressed down initially
+ SHOULD_CALL_PARENT(TRUE)
return
/datum/proc/key_up(key, client/user) // Called when a key is released
return
diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 1a713ec04f884..88c2b9496a929 100644
--- a/code/modules/mob/living/basic/clown/clown.dm
+++ b/code/modules/mob/living/basic/clown/clown.dm
@@ -49,7 +49,7 @@
ai_controller.set_blackboard_key(BB_BASIC_MOB_SPEAK_LINES, emotes)
//im not putting dynamic humans or whatever its called here because this is the base path of nonhuman clownstrosities
if(waddles)
- AddElement(/datum/element/waddling)
+ AddElementTrait(TRAIT_WADDLING, INNATE_TRAIT, /datum/element/waddling)
if(length(loot))
loot = string_list(loot)
AddElement(/datum/element/death_drops, loot)
diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm
index df8f3a1fd4eec..7795ec630e6d5 100644
--- a/code/modules/mob/living/basic/farm_animals/pony.dm
+++ b/code/modules/mob/living/basic/farm_animals/pony.dm
@@ -40,7 +40,7 @@
AddElement(/datum/element/pet_bonus, "whickers.")
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/ai_flee_while_injured)
- AddElement(/datum/element/waddling)
+ AddElementTrait(TRAIT_WADDLING, INNATE_TRAIT, /datum/element/waddling)
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/apple), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, PROC_REF(tamed)), unique = unique_tamer)
/mob/living/basic/pony/proc/tamed(mob/living/tamer)
diff --git a/code/modules/mob/living/basic/pets/penguin.dm b/code/modules/mob/living/basic/pets/penguin.dm
index 671c2cf30c130..e8e2a038c0ea9 100644
--- a/code/modules/mob/living/basic/pets/penguin.dm
+++ b/code/modules/mob/living/basic/pets/penguin.dm
@@ -23,7 +23,7 @@
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/ai_flee_while_injured)
AddElement(/datum/element/pet_bonus, "honks happily!")
- AddElement(/datum/element/waddling)
+ AddElementTrait(TRAIT_WADDLING, INNATE_TRAIT, /datum/element/waddling)
if(!can_lay_eggs)
return
AddComponent(\
diff --git a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
index 3602d5e8a991c..be83d3e058f91 100644
--- a/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
+++ b/code/modules/mob/living/basic/space_fauna/regal_rat/regal_rat.dm
@@ -52,7 +52,7 @@
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
RegisterSignal(src, COMSIG_MOB_LOGIN, PROC_REF(on_login))
- AddElement(/datum/element/waddling)
+ AddElementTrait(TRAIT_WADDLING, INNATE_TRAIT, /datum/element/waddling)
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/door_pryer, pry_time = 5 SECONDS, interaction_key = REGALRAT_INTERACTION)
AddComponent(\
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 0301b59e8a0a7..0085d0b2fb693 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1647,3 +1647,7 @@
set name = "View Skills"
mind?.print_levels(src)
+
+/mob/key_down(key, client/client, full_key)
+ ..()
+ SEND_SIGNAL(src, COMSIG_MOB_KEYDOWN, key, client, full_key)
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 245afb869323f..aaf654e6dba2b 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -200,4 +200,3 @@
var/active_typing_indicator
///the icon currently used for the thinking indicator's bubble
var/active_thinking_indicator
-
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index c54d0fd7c33d0..2403ca2da53c3 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -47,8 +47,8 @@
/client/Move(new_loc, direct)
if(world.time < move_delay) //do not move anything ahead of this check please
return FALSE
- next_move_dir_add = 0
- next_move_dir_sub = 0
+ next_move_dir_add = NONE
+ next_move_dir_sub = NONE
var/old_move_delay = move_delay
move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick
if(!direct || !new_loc)
diff --git a/code/modules/mod/modules/modules_service.dm b/code/modules/mod/modules/modules_service.dm
index 5381a26e88bd4..e6e4a01c66475 100644
--- a/code/modules/mod/modules/modules_service.dm
+++ b/code/modules/mod/modules/modules_service.dm
@@ -70,13 +70,13 @@
/obj/item/mod/module/waddle/on_suit_activation()
mod.boots.AddComponent(/datum/component/squeak, list('sound/effects/footstep/clownstep1.ogg'=1,'sound/effects/footstep/clownstep2.ogg'=1), 50, falloff_exponent = 20) //die off quick please
- mod.wearer.AddElement(/datum/element/waddling)
+ mod.wearer.AddElementTrait(TRAIT_WADDLING, MOD_TRAIT, /datum/element/waddling)
if(is_clown_job(mod.wearer.mind?.assigned_role))
mod.wearer.add_mood_event("clownshoes", /datum/mood_event/clownshoes)
/obj/item/mod/module/waddle/on_suit_deactivation(deleting = FALSE)
if(!deleting)
qdel(mod.boots.GetComponent(/datum/component/squeak))
- mod.wearer.RemoveElement(/datum/element/waddling)
+ REMOVE_TRAIT(mod.wearer, TRAIT_WADDLING, MOD_TRAIT)
if(is_clown_job(mod.wearer.mind?.assigned_role))
mod.wearer.clear_mood_event("clownshoes")
diff --git a/code/modules/modular_computers/file_system/programs/radar.dm b/code/modules/modular_computers/file_system/programs/radar.dm
index b506777f3de7a..34771cde63b20 100644
--- a/code/modules/modular_computers/file_system/programs/radar.dm
+++ b/code/modules/modular_computers/file_system/programs/radar.dm
@@ -149,16 +149,13 @@
return RADAR_NOT_TRACKABLE
var/turf/here = get_turf(computer)
var/turf/there = get_turf(signal)
- if(!here || !there)
- return RADAR_NOT_TRACKABLE //I was still getting a runtime even after the above check while scanning, so fuck it
- if(there.z != here.z && (!is_station_level(here.z) || !is_station_level(there.z)))
+ if(isnull(here) || isnull(there) || !is_valid_z_level(here, there))
return RADAR_NOT_TRACKABLE
var/trackable_signal = SEND_SIGNAL(computer, COMSIG_MODULAR_COMPUTER_RADAR_TRACKABLE, signal, here, there)
- switch(trackable_signal)
- if(COMPONENT_RADAR_TRACK_ANYWAY)
- return RADAR_TRACKABLE_ANYWAY
- if(COMPONENT_RADAR_DONT_TRACK)
- return RADAR_NOT_TRACKABLE
+ if(trackable_signal & COMPONENT_RADAR_TRACK_ANYWAY)
+ return RADAR_TRACKABLE_ANYWAY
+ if(trackable_signal & COMPONENT_RADAR_DONT_TRACK)
+ return RADAR_NOT_TRACKABLE
return RADAR_TRACKABLE
/**
@@ -268,15 +265,16 @@
/datum/computer_file/program/radar/lifeline/trackable(mob/living/carbon/human/humanoid)
. = ..()
- if(. == RADAR_TRACKABLE_ANYWAY)
- return RADAR_TRACKABLE_ANYWAY
- if(!humanoid || !istype(humanoid))
+ if(. != RADAR_TRACKABLE)
+ return .
+ if(!istype(humanoid))
return RADAR_NOT_TRACKABLE
if(!istype(humanoid.w_uniform, /obj/item/clothing/under))
return RADAR_NOT_TRACKABLE
var/obj/item/clothing/under/uniform = humanoid.w_uniform
- if(uniform.has_sensor && uniform.sensor_mode >= SENSOR_COORDS) // Suit sensors must be on maximum
- return RADAR_TRACKABLE
+ if(!uniform.has_sensor || uniform.sensor_mode < SENSOR_COORDS) // Suit sensors must be on maximum
+ return RADAR_NOT_TRACKABLE
+ return .
///Tracks all janitor equipment
/datum/computer_file/program/radar/custodial_locator
diff --git a/code/modules/paperwork/carbonpaper.dm b/code/modules/paperwork/carbonpaper.dm
index b2f985dc357fd..1dfe4ea782160 100644
--- a/code/modules/paperwork/carbonpaper.dm
+++ b/code/modules/paperwork/carbonpaper.dm
@@ -20,6 +20,12 @@
return
. += span_notice("Right-click to tear off the carbon-copy (you must use both hands).")
+/obj/item/paper/carbon/AltClick(mob/living/user)
+ if(!copied)
+ to_chat(user, span_notice("Take off the carbon copy first."))
+ return
+ return ..()
+
/obj/item/paper/carbon/proc/removecopy(mob/living/user)
if(copied)
to_chat(user, span_notice("There are no more carbon copies attached to this paper!"))
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index 82517f72494ca..1c1daaad51140 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -319,6 +319,7 @@
/obj/item/paper/examine(mob/user)
. = ..()
+ . += span_notice("Alt-click [src] to fold it into a paper plane.")
if(!in_range(user, src) && !isobserver(user))
. += span_warning("You're too far away to read it!")
return
@@ -358,6 +359,31 @@
return TRUE
return ..()
+/obj/item/paper/AltClick(mob/living/user)
+ . = ..()
+ if(!user.can_perform_action(src, NEED_DEXTERITY|NEED_HANDS))
+ return
+ if(HAS_TRAIT(user, TRAIT_PAPER_MASTER))
+ return make_plane(user, /obj/item/paperplane/syndicate)
+ return make_plane(user, /obj/item/paperplane)
+
+
+/**
+ * Paper plane folding
+ * Makes a paperplane depending on args and returns it.
+ *
+ * Arguments:
+ * * mob/living/user - who's folding
+ * * obj/item/paperplane/plane_type - what it will be folded into (path)
+ */
+/obj/item/paper/proc/make_plane(mob/living/user, obj/item/paperplane/plane_type = /obj/item/paperplane)
+ balloon_alert(user, "folded into a plane")
+ user.temporarilyRemoveItemFromInventory(src)
+ var/obj/item/paperplane/new_plane = new plane_type(loc, src)
+ if(user.Adjacent(new_plane))
+ user.put_in_hands(new_plane)
+ return new_plane
+
/obj/item/proc/burn_paper_product_attackby_check(obj/item/attacking_item, mob/living/user, bypass_clumsy = FALSE)
//can't be put on fire!
if((resistance_flags & FIRE_PROOF) || !(resistance_flags & FLAMMABLE))
diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm
index d3688ff2156ac..0b9c4feccb18c 100644
--- a/code/modules/paperwork/paperplane.dm
+++ b/code/modules/paperwork/paperplane.dm
@@ -3,6 +3,7 @@
desc = "Paper, folded in the shape of a plane."
icon = 'icons/obj/service/bureaucracy.dmi'
icon_state = "paperplane"
+ base_icon_state = "paperplane"
custom_fire_overlay = "paperplane_onfire"
throw_range = 7
throw_speed = 1
@@ -11,43 +12,45 @@
resistance_flags = FLAMMABLE
max_integrity = 50
- var/hit_probability = 2 //%
- var/obj/item/paper/internalPaper
+ ///The chance of hitting a mob in the eye when thrown, in percentage.
+ var/hit_probability = 2
+ ///Reference to the paper that's folded up in this paperplane, which we return when unfolded.
+ var/obj/item/paper/internal_paper
/obj/item/paperplane/syndicate
desc = "Paper, masterfully folded in the shape of a plane."
- throwforce = 20 //same as throwing stars, but no chance of embedding.
- hit_probability = 100 //guaranteed to cause eye damage when it hits a mob.
+ throwforce = 20
+ hit_probability = 100
-/obj/item/paperplane/Initialize(mapload, obj/item/paper/newPaper)
+/obj/item/paperplane/Initialize(mapload, obj/item/paper/paper_made_of)
. = ..()
pixel_x = base_pixel_x + rand(-9, 9)
pixel_y = base_pixel_y + rand(-8, 8)
- if(newPaper)
- internalPaper = newPaper
- flags_1 = newPaper.flags_1
- color = newPaper.color
- newPaper.forceMove(src)
+ if(paper_made_of)
+ internal_paper = paper_made_of
+ flags_1 = paper_made_of.flags_1
+ color = paper_made_of.color
+ paper_made_of.forceMove(src)
else
- internalPaper = new(src)
- if(internalPaper.icon_state == "cpaper" || internalPaper.icon_state == "cpaper_words")
- icon_state = "paperplane_carbon" // It's the purple carbon copy. Use the purple paper plane
- update_appearance()
+ internal_paper = new(src)
+ if(istype(internal_paper, /obj/item/paper/carbon_copy))
+ icon_state = "[base_icon_state]_carbon"
+ update_appearance(UPDATE_ICON)
/obj/item/paperplane/Exited(atom/movable/gone, direction)
. = ..()
- if (internalPaper == gone)
- internalPaper = null
+ if (internal_paper == gone)
+ internal_paper = null
if(!QDELETED(src))
qdel(src)
/obj/item/paperplane/Destroy()
- internalPaper = null
+ internal_paper = null
return ..()
/obj/item/paperplane/suicide_act(mob/living/user)
var/obj/item/organ/internal/eyes/eyes = user.get_organ_slot(ORGAN_SLOT_EYES)
- user.Stun(200)
+ user.Stun(20 SECONDS)
user.visible_message(span_suicide("[user] jams [src] in [user.p_their()] nose. It looks like [user.p_theyre()] trying to commit suicide!"))
user.adjust_eye_blur(12 SECONDS)
if(eyes)
@@ -57,8 +60,8 @@
/obj/item/paperplane/update_overlays()
. = ..()
- for(var/stamp in internalPaper.stamp_cache)
- . += "paperplane_[stamp]"
+ for(var/stamp in internal_paper.stamp_cache)
+ . += "[base_icon_state]_[stamp]"
/obj/item/paperplane/attack_self(mob/user)
balloon_alert(user, "unfolded")
@@ -66,82 +69,45 @@
var/atom/location = drop_location()
// Need to keep a reference to the internal paper
// when we move it out of the plane, our ref gets set to null
- var/obj/item/paper/internal_paper = internalPaper
- internal_paper.forceMove(location)
+ var/obj/item/paper/released_paper = internal_paper
+ released_paper.forceMove(location)
// This will as a side effect, qdel the paper plane, making the user's hands empty
- user.put_in_hands(internal_paper)
+ user.put_in_hands(released_paper)
-/obj/item/paperplane/attackby(obj/item/P, mob/living/carbon/human/user, params)
- if(burn_paper_product_attackby_check(P, user))
+/obj/item/paperplane/attackby(obj/item/attacking_item, mob/user, params)
+ if(burn_paper_product_attackby_check(attacking_item, user))
return
- if(istype(P, /obj/item/pen) || istype(P, /obj/item/toy/crayon))
+ if(istype(attacking_item, /obj/item/pen) || istype(attacking_item, /obj/item/toy/crayon))
to_chat(user, span_warning("You should unfold [src] before changing it!"))
return
-
- else if(istype(P, /obj/item/stamp)) //we don't randomize stamps on a paperplane
- internalPaper.attackby(P, user) //spoofed attack to update internal paper.
+ else if(istype(attacking_item, /obj/item/stamp)) //we don't randomize stamps on a paperplane
+ internal_paper.attackby(attacking_item, user) //spoofed attack to update internal paper.
update_appearance()
add_fingerprint(user)
return
-
return ..()
-
-/obj/item/paperplane/throw_at(atom/target, range, speed, mob/thrower, spin=FALSE, diagonals_first = FALSE, datum/callback/callback, gentle, quickstart = TRUE)
- . = ..(target, range, speed, thrower, FALSE, diagonals_first, callback, quickstart = quickstart)
-
/obj/item/paperplane/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
- if(iscarbon(hit_atom))
- var/mob/living/carbon/C = hit_atom
- if(C.can_catch_item(TRUE))
- var/datum/action/innate/origami/origami_action = locate() in C.actions
- if(origami_action?.active) //if they're a master of origami and have the ability turned on, force throwmode on so they'll automatically catch the plane.
- C.throw_mode_on(THROW_MODE_TOGGLE)
-
- if(..() || !ishuman(hit_atom))//if the plane is caught or it hits a nonhuman
- return
- var/mob/living/carbon/human/H = hit_atom
- var/obj/item/organ/internal/eyes/eyes = H.get_organ_slot(ORGAN_SLOT_EYES)
- if(prob(hit_probability))
- if(H.is_eyes_covered())
- return
- visible_message(span_danger("\The [src] hits [H] in the eye[eyes ? "" : " socket"]!"))
- H.adjust_eye_blur(12 SECONDS)
- eyes?.apply_organ_damage(rand(6,8))
- H.Paralyze(40)
- H.emote("scream")
+ if(iscarbon(hit_atom) && HAS_TRAIT(hit_atom, TRAIT_PAPER_MASTER))
+ var/mob/living/carbon/hit_carbon = hit_atom
+ if(hit_carbon.can_catch_item(TRUE))
+ hit_carbon.throw_mode_on(THROW_MODE_TOGGLE)
-/obj/item/paper/examine(mob/user)
. = ..()
- . += span_notice("Alt-click [src] to fold it into a paper plane.")
-
-/obj/item/paper/AltClick(mob/living/user, obj/item/I)
- if(!user.can_perform_action(src, NEED_DEXTERITY|NEED_HANDS))
+ if(. || !ishuman(hit_atom)) //if the plane is caught or it hits a nonhuman
return
- if(istype(src, /obj/item/paper/carbon))
- var/obj/item/paper/carbon/Carbon = src
- if(!Carbon.copied)
- to_chat(user, span_notice("Take off the carbon copy first."))
- return
- //Origami Master
- var/datum/action/innate/origami/origami_action = locate() in user.actions
- if(origami_action?.active)
- make_plane(user, I, /obj/item/paperplane/syndicate)
- else
- make_plane(user, I, /obj/item/paperplane)
+ var/mob/living/carbon/human/hit_human = hit_atom
+ var/obj/item/organ/internal/eyes/eyes = hit_human.get_organ_slot(ORGAN_SLOT_EYES)
+ if(!prob(hit_probability))
+ return
+ if(hit_human.is_eyes_covered())
+ return
+ visible_message(span_danger("\The [src] hits [hit_human] in the eye[eyes ? "" : " socket"]!"))
+ hit_human.adjust_eye_blur(12 SECONDS)
+ eyes?.apply_organ_damage(rand(6, 8))
+ hit_human.Paralyze(4 SECONDS)
+ hit_human.emote("scream")
-/**
- * Paper plane folding
- *
- * Arguments:
- * * mob/living/user - who's folding
- * * obj/item/I - what's being folded
- * * obj/item/paperplane/plane_type - what it will be folded into (path)
- */
-/obj/item/paper/proc/make_plane(mob/living/user, obj/item/I, obj/item/paperplane/plane_type = /obj/item/paperplane)
- balloon_alert(user, "folded into a plane")
- user.temporarilyRemoveItemFromInventory(src)
- I = new plane_type(loc, src)
- if(user.Adjacent(I))
- user.put_in_hands(I)
+/obj/item/paperplane/throw_at(atom/target, range, speed, mob/thrower, spin=FALSE, diagonals_first = FALSE, datum/callback/callback, gentle, quickstart = TRUE)
+ return ..(target, range, speed, thrower, FALSE, diagonals_first, callback, quickstart = quickstart)
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index aface2dce34b5..d20f2b786277d 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -273,9 +273,9 @@
. = ..()
var/need_mob_update
if(affected_mob.getFireLoss() > 25)
- need_mob_update = affected_mob.adjustFireLoss(-4 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype) //Twice as effective as AIURI for severe burns
+ need_mob_update = affected_mob.adjustFireLoss(-4 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype) //Twice as effective as AIURI for severe burns
else
- need_mob_update = affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype) //But only a quarter as effective for more minor ones
+ need_mob_update = affected_mob.adjustFireLoss(-0.5 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype) //But only a quarter as effective for more minor ones
if(need_mob_update)
return UPDATE_MOB_HEALTH
@@ -535,9 +535,9 @@
. = ..()
var/need_mob_update
if(affected_mob.getBruteLoss() > 25)
- need_mob_update = affected_mob.adjustBruteLoss(-4 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update = affected_mob.adjustBruteLoss(-4 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype)
else
- need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick, updating_health = FALSE, required_bodytype = affected_bodytype)
+ need_mob_update = affected_mob.adjustBruteLoss(-0.5 * REM * seconds_per_tick * normalise_creation_purity(), updating_health = FALSE, required_bodytype = affected_bodytype)
if(need_mob_update)
return UPDATE_MOB_HEALTH
@@ -1235,7 +1235,7 @@
need_mob_update = affected_mob.adjustBruteLoss(-5 * REM * seconds_per_tick, updating_health = FALSE) //A ton of healing - this is a 50 telecrystal investment.
need_mob_update += affected_mob.adjustFireLoss(-5 * REM * seconds_per_tick, updating_health = FALSE)
need_mob_update += affected_mob.adjustOxyLoss(-15 * REM * seconds_per_tick, updating_health = FALSE)
- need_mob_update += affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += affected_mob.adjustToxLoss(-5 * REM * seconds_per_tick, updating_health = FALSE, forced = TRUE, required_biotype = affected_biotype)
need_mob_update += affected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, -15 * REM * seconds_per_tick)
if(need_mob_update)
return UPDATE_MOB_HEALTH
diff --git a/code/modules/surgery/organs/internal/ears/_ears.dm b/code/modules/surgery/organs/internal/ears/_ears.dm
index ad48645e89366..f538f8789b600 100644
--- a/code/modules/surgery/organs/internal/ears/_ears.dm
+++ b/code/modules/surgery/organs/internal/ears/_ears.dm
@@ -89,15 +89,13 @@
/obj/item/organ/internal/ears/penguin/on_mob_insert(mob/living/carbon/human/ear_owner)
. = ..()
- if(istype(ear_owner))
- to_chat(ear_owner, span_notice("You suddenly feel like you've lost your balance."))
- ear_owner.AddElement(/datum/element/waddling)
+ to_chat(ear_owner, span_notice("You suddenly feel like you've lost your balance."))
+ ear_owner.AddElementTrait(TRAIT_WADDLING, ORGAN_TRAIT, /datum/element/waddling)
/obj/item/organ/internal/ears/penguin/on_mob_remove(mob/living/carbon/human/ear_owner)
. = ..()
- if(istype(ear_owner))
- to_chat(ear_owner, span_notice("Your sense of balance comes back to you."))
- ear_owner.RemoveElement(/datum/element/waddling)
+ to_chat(ear_owner, span_notice("Your sense of balance comes back to you."))
+ REMOVE_TRAIT(ear_owner, TRAIT_WADDLING, ORGAN_TRAIT)
/obj/item/organ/internal/ears/cybernetic
name = "basic cybernetic ears"
diff --git a/code/modules/tgui_input/keycombo.dm b/code/modules/tgui_input/keycombo.dm
new file mode 100644
index 0000000000000..948dbaea234a8
--- /dev/null
+++ b/code/modules/tgui_input/keycombo.dm
@@ -0,0 +1,126 @@
+/**
+ * Creates a TGUI window with a key input. Returns the user's response as a full key with modifiers, eg ShiftK.
+ *
+ * This proc should be used to create windows for key entry that the caller will wait for a response from.
+ * If tgui fancy chat is turned off: Will return a normal input.
+ *
+ * Arguments:
+ * * user - The user to show the number input to.
+ * * message - The content of the number input, shown in the body of the TGUI window.
+ * * title - The title of the number input modal, shown on the top of the TGUI window.
+ * * default - The default (or current) key, shown as a placeholder.
+ */
+/proc/tgui_input_keycombo(mob/user = usr, message, title = "Key Input", default = 0, timeout = 0, ui_state = GLOB.always_state)
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return null
+
+ if (isnull(user.client))
+ return null
+
+ // Client does NOT have tgui_input on: Returns regular input
+ if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ var/input_key = input(user, message, title + "(Modifiers are TGUI only, sorry!)", default) as null|text
+ return input_key[1]
+ var/datum/tgui_input_keycombo/key_input = new(user, message, title, default, timeout, ui_state)
+ key_input.ui_interact(user)
+ key_input.wait()
+ if (key_input)
+ . = key_input.entry
+ qdel(key_input)
+
+/**
+ * # tgui_input_keycombo
+ *
+ * Datum used for instantiating and using a TGUI-controlled key input that prompts the user with
+ * a message and listens for key presses.
+ */
+/datum/tgui_input_keycombo
+ /// Boolean field describing if the tgui_input_number was closed by the user.
+ var/closed
+ /// The default (or current) value, shown as a default. Users can press reset with this.
+ var/default
+ /// The entry that the user has return_typed in.
+ var/entry
+ /// The prompt's body, if any, of the TGUI window.
+ var/message
+ /// The time at which the number input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the number input, after which the window will close and delete itself.
+ var/timeout
+ /// The title of the TGUI window
+ var/title
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
+
+/datum/tgui_input_keycombo/New(mob/user, message, title, default, timeout, ui_state)
+ src.default = default
+ src.message = message
+ src.title = title
+ src.state = ui_state
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_input_keycombo/Destroy(force)
+ SStgui.close_uis(src)
+ state = null
+ return ..()
+
+/**
+ * Waits for a user's response to the tgui_input_keycombo's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_input_keycombo/proc/wait()
+ while (!entry && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_input_keycombo/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "KeyComboModal")
+ ui.open()
+
+/datum/tgui_input_keycombo/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_input_keycombo/ui_state(mob/user)
+ return state
+
+/datum/tgui_input_keycombo/ui_static_data(mob/user)
+ var/list/data = list()
+ data["init_value"] = default // Default is a reserved keyword
+ data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ data["message"] = message
+ data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ data["title"] = title
+ return data
+
+/datum/tgui_input_keycombo/ui_data(mob/user)
+ var/list/data = list()
+ if(timeout)
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
+
+/datum/tgui_input_keycombo/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("submit")
+ set_entry(params["entry"])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_input_keycombo/proc/set_entry(entry)
+ src.entry = entry
diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm
index 032a054a77c9a..37f6eb7efa501 100644
--- a/code/modules/vehicles/cars/clowncar.dm
+++ b/code/modules/vehicles/cars/clowncar.dm
@@ -179,7 +179,7 @@
to_chat(user, span_danger("You scramble [src]'s child safety lock, and a panel with six colorful buttons appears!"))
initialize_controller_action_type(/datum/action/vehicle/sealed/roll_the_dice, VEHICLE_CONTROL_DRIVE)
initialize_controller_action_type(/datum/action/vehicle/sealed/cannon, VEHICLE_CONTROL_DRIVE)
- AddElement(/datum/element/waddling)
+ AddElementTrait(TRAIT_WADDLING, INNATE_TRAIT, /datum/element/waddling)
return TRUE
/obj/vehicle/sealed/car/clowncar/atom_destruction(damage_flag)
diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm
index 47bb966555686..3c544552d512a 100644
--- a/code/modules/vending/_vending.dm
+++ b/code/modules/vending/_vending.dm
@@ -236,6 +236,11 @@
voice = vendor_voice_by_type[type]
if(build_inv) //non-constructable vending machine
+ ///Non-constructible vending machines do not have a refill canister to populate its products list from,
+ ///Which apparently is still needed in the case we use product categories instead.
+ if(product_categories)
+ for(var/list/category as anything in product_categories)
+ products |= category["products"]
build_inventories()
slogan_list = splittext(product_slogans, ";")
diff --git a/html/changelogs/AutoChangeLog-pr-81409.yml b/html/changelogs/AutoChangeLog-pr-81409.yml
deleted file mode 100644
index f3f1949b1ad00..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81409.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Thunder12345"
-delete-after: True
-changes:
- - qol: "The bitrunning quantum console UI now lists domains in tabs by difficulty."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81413.yml b/html/changelogs/AutoChangeLog-pr-81413.yml
deleted file mode 100644
index b64eee4e917c7..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81413.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Melbert"
-delete-after: True
-changes:
- - rscadd: "Wizards have a new mobility option available, the Telegram Scepter. The ability to travel anywhere you can see at the point of a wand... but at a price?"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81435.yml b/html/changelogs/AutoChangeLog-pr-81435.yml
deleted file mode 100644
index 0ef4705974e09..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81435.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Ghommie"
-delete-after: True
-changes:
- - rscadd: "Added three new 'special' bedsheets. One of them is quite rare and made from gondola hide."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81452.yml b/html/changelogs/AutoChangeLog-pr-81452.yml
deleted file mode 100644
index d501307c8ba40..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81452.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "mogeoko"
-delete-after: True
-changes:
- - bugfix: "Thermomachines now reconnect to pipes on multitool's act."
- - bugfix: "Multi-deck connectors won't connect pipes not located in front/top/bottom of it."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81459.yml b/html/changelogs/AutoChangeLog-pr-81459.yml
deleted file mode 100644
index 39addd9cbe676..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81459.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-author: "SyncIt21"
-delete-after: True
-changes:
- - qol: "adds examines & screentips for ore box"
- - code_imp: "cleans up some procs for ore box"
- - spellcheck: "corrected description & ui notice of ore box to specify it can hold boulders too"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81472.yml b/html/changelogs/AutoChangeLog-pr-81472.yml
deleted file mode 100644
index 0ca1b4ccb0f35..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81472.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "kawaiinick"
-delete-after: True
-changes:
- - rscadd: "Your mouth now fits combat or survival knives(it's totally safe)"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81481.yml b/html/changelogs/AutoChangeLog-pr-81481.yml
deleted file mode 100644
index d076280f163ec..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81481.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Sylphet"
-delete-after: True
-changes:
- - bugfix: "cargo lockboxes update iconstates correctly now"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81482.yml b/html/changelogs/AutoChangeLog-pr-81482.yml
deleted file mode 100644
index 7306b7c28b4de..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81482.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "00-Steven"
-delete-after: True
-changes:
- - bugfix: "Space cats CAN into space. (They're back to surviving being in unsuitable atmos.)"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81483.yml b/html/changelogs/AutoChangeLog-pr-81483.yml
deleted file mode 100644
index 8c82b8396d588..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81483.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "IndieanaJones"
-delete-after: True
-changes:
- - bugfix: "Tank spider corpses should no longer be conditionally invisible"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81485.yml b/html/changelogs/AutoChangeLog-pr-81485.yml
deleted file mode 100644
index bac62bcf734ae..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81485.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "IndieanaJones"
-delete-after: True
-changes:
- - bugfix: "Blob Zombies now render their blob heads correctly again."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81487.yml b/html/changelogs/AutoChangeLog-pr-81487.yml
deleted file mode 100644
index db13b1e0f97da..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81487.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SyncIt21"
-delete-after: True
-changes:
- - bugfix: "Indestructible items like the pai card don't teleport to the ore silo when you insert them into silo linked machine & also displays a message saying it was rejected."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81492.yml b/html/changelogs/AutoChangeLog-pr-81492.yml
deleted file mode 100644
index d2aeff28762c2..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81492.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "13spacemen"
-delete-after: True
-changes:
- - code_imp: "Removed unused global lists and sprite accessories related to tail spines"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81493.yml b/html/changelogs/AutoChangeLog-pr-81493.yml
deleted file mode 100644
index 4f7d4b4b89c23..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-81493.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "13spacemen"
-delete-after: True
-changes:
- - code_imp: "removed redundant check for plasmamen in survival box code"
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81523.yml b/html/changelogs/AutoChangeLog-pr-81523.yml
new file mode 100644
index 0000000000000..b0279df0d02b2
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-81523.yml
@@ -0,0 +1,4 @@
+author: "Rhials"
+delete-after: True
+changes:
+ - bugfix: "Removes the double-newscaster from the arrivals sec post."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81526.yml b/html/changelogs/AutoChangeLog-pr-81526.yml
new file mode 100644
index 0000000000000..7a601ac9ac82f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-81526.yml
@@ -0,0 +1,4 @@
+author: "LemonInTheDark"
+delete-after: True
+changes:
+ - bugfix: "Maps loaded in after roundstart will no longer have broken smoothing"
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-02.yml b/html/changelogs/archive/2024-02.yml
index 6afd8b08c3696..28735b510d5b6 100644
--- a/html/changelogs/archive/2024-02.yml
+++ b/html/changelogs/archive/2024-02.yml
@@ -446,3 +446,61 @@
- rscadd: In an attempt to stop the greytide, NanoTrasen has increased security's
baton energy output. This has, through testing, done nothing but make the device
spark more than it used to.
+2024-02-17:
+ 00-Steven:
+ - bugfix: Space cats CAN into space. (They're back to surviving being in unsuitable
+ atmos.)
+ 13spacemen:
+ - code_imp: Removed unused global lists and sprite accessories related to tail spines
+ - code_imp: removed redundant check for plasmamen in survival box code
+ Ghommie:
+ - rscadd: Added three new 'special' bedsheets. One of them is quite rare and made
+ from gondola hide.
+ IndieanaJones:
+ - bugfix: Tank spider corpses should no longer be conditionally invisible
+ - bugfix: Blob Zombies now render their blob heads correctly again.
+ MLGTASTICa:
+ - balance: Oxandrolone now scales with purity. Its default purity is 100%
+ - balance: Salicylic acid now scales with purity. Its default purity is 100%
+ Melbert:
+ - rscadd: Wizards have a new mobility option available, the Telegram Scepter. The
+ ability to travel anywhere you can see at the point of a wand... but at a price?
+ Sylphet:
+ - bugfix: cargo lockboxes update iconstates correctly now
+ SyncIt21:
+ - bugfix: Indestructible items like the pai card don't teleport to the ore silo
+ when you insert them into silo linked machine & also displays a message saying
+ it was rejected.
+ - qol: adds examines & screentips for ore box
+ - code_imp: cleans up some procs for ore box
+ - spellcheck: corrected description & ui notice of ore box to specify it can hold
+ boulders too
+ Thunder12345:
+ - qol: The bitrunning quantum console UI now lists domains in tabs by difficulty.
+ destrucktoid:
+ - bugfix: Restorative Nanites now heal Slimepeople just as well as they do other
+ people.
+ kawaiinick:
+ - rscadd: Your mouth now fits combat or survival knives(it's totally safe)
+ mc-oofert:
+ - balance: assemblybombs are bulky
+ - qol: you may altclick action buttons to bind them to a key
+ mogeoko:
+ - bugfix: Thermomachines now reconnect to pipes on multitool's act.
+ - bugfix: Multi-deck connectors won't connect pipes not located in front/top/bottom
+ of it.
+2024-02-18:
+ 1393F:
+ - bugfix: The crystallizer screentip for rotating it has been updated with the correct
+ button
+ Ghommie:
+ - rscadd: Players holding cardboard cutouts will now assume their appearance, just
+ like for potted plants.
+ GoldenAlpharex:
+ - bugfix: The Spawners menu now accurately displays the amount of uses left in each
+ spawner option, taking into account individual spawners that either allow more
+ than one use, or an infinite amount of uses.
+ Melbert:
+ - bugfix: Lifeline can no longer track mobs with suit sensors off
+ Roastglue:
+ - bugfix: Crafting Bulgogi no longer makes Bibimbap. It instead makes Bulgogi.
diff --git a/tgstation.dme b/tgstation.dme
index 0a32dc4496121..45c11123587cd 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -5799,6 +5799,7 @@
#include "code\modules\tgui\states\zlevel.dm"
#include "code\modules\tgui_input\alert.dm"
#include "code\modules\tgui_input\checkboxes.dm"
+#include "code\modules\tgui_input\keycombo.dm"
#include "code\modules\tgui_input\list.dm"
#include "code\modules\tgui_input\number.dm"
#include "code\modules\tgui_input\text.dm"
diff --git a/tgui/packages/tgui/interfaces/KeyComboModal.tsx b/tgui/packages/tgui/interfaces/KeyComboModal.tsx
new file mode 100644
index 0000000000000..e0b598764f156
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/KeyComboModal.tsx
@@ -0,0 +1,147 @@
+import { KEY } from 'common/keys';
+import { useState } from 'react';
+
+import { useBackend, useLocalState } from '../backend';
+import { Autofocus, Box, Button, Section, Stack } from '../components';
+import { Window } from '../layouts';
+import { InputButtons } from './common/InputButtons';
+import { Loader } from './common/Loader';
+
+type KeyInputData = {
+ init_value: string;
+ large_buttons: boolean;
+ message: string;
+ timeout: number;
+ title: string;
+};
+
+const isStandardKey = (event: React.KeyboardEvent): boolean => {
+ return (
+ event.key !== KEY.Alt &&
+ event.key !== KEY.Control &&
+ event.key !== KEY.Shift &&
+ event.key !== KEY.Escape
+ );
+};
+
+const KEY_CODE_TO_BYOND: Record = {
+ DEL: 'Delete',
+ DOWN: 'South',
+ END: 'Southwest',
+ HOME: 'Northwest',
+ INSERT: 'Insert',
+ LEFT: 'West',
+ PAGEDOWN: 'Southeast',
+ PAGEUP: 'Northeast',
+ RIGHT: 'East',
+ SPACEBAR: 'Space',
+ UP: 'North',
+};
+
+const DOM_KEY_LOCATION_NUMPAD = 3;
+
+const formatKeyboardEvent = (
+ event: React.KeyboardEvent,
+): string => {
+ let text = '';
+
+ if (event.altKey) {
+ text += 'Alt';
+ }
+
+ if (event.ctrlKey) {
+ text += 'Ctrl';
+ }
+
+ if (event.shiftKey) {
+ text += 'Shift';
+ }
+
+ if (event.location === DOM_KEY_LOCATION_NUMPAD) {
+ text += 'Numpad';
+ }
+
+ if (isStandardKey(event)) {
+ const key = event.key.toUpperCase();
+ text += KEY_CODE_TO_BYOND[key] || key;
+ }
+
+ return text;
+};
+
+export const KeyComboModal = (props) => {
+ const { act, data } = useBackend();
+ const { init_value, large_buttons, message = '', title, timeout } = data;
+ const [input, setInput] = useState(init_value);
+ const [binding, setBinding] = useLocalState('binding', true);
+
+ const setValue = (value: string) => {
+ if (value === input) {
+ return;
+ }
+ setInput(value);
+ };
+
+ // Dynamically changes the window height based on the message.
+ const windowHeight =
+ 130 +
+ (message.length > 30 ? Math.ceil(message.length / 3) : 0) +
+ (message.length && large_buttons ? 5 : 0);
+
+ return (
+
+ {timeout && }
+ {
+ if (!binding) {
+ if (event.key === KEY.Enter) {
+ act('submit', { entry: input });
+ }
+ if (event.key === KEY.Escape) {
+ act('cancel');
+ }
+ return;
+ }
+
+ event.preventDefault();
+
+ if (isStandardKey(event)) {
+ setValue(formatKeyboardEvent(event));
+ setBinding(false);
+ return;
+ } else if (event.key === KEY.Escape) {
+ setValue(init_value);
+ setBinding(false);
+ return;
+ }
+ }}
+ >
+
+
+
+
+ {message}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/SpawnersMenu.tsx b/tgui/packages/tgui/interfaces/SpawnersMenu.tsx
index 50366a22b2ff9..12f06cfdb62b4 100644
--- a/tgui/packages/tgui/interfaces/SpawnersMenu.tsx
+++ b/tgui/packages/tgui/interfaces/SpawnersMenu.tsx
@@ -10,6 +10,7 @@ type SpawnersMenuContext = {
type spawner = {
name: string;
amount_left: number;
+ infinite: boolean;
desc?: string;
you_are_text?: string;
flavor_text?: string;
@@ -31,9 +32,15 @@ export const SpawnersMenu = (props) => {
title={capitalizeAll(spawner.name)}
buttons={
-
- {spawner.amount_left} left
-
+ {spawner.infinite ? (
+
+ Infinite
+
+ ) : (
+
+ {spawner.amount_left} left
+
+ )}