From 6ad558f7dcca3197c39a6cfb8ad3a74789dcab9b Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:21:11 -0800 Subject: [PATCH] Arbitrary gun attachment system (#6826) does not currently support projectile modification, that's coming later Includes adding support for attaching maglights to certain guns --- citadel.dme | 8 +- .../signals_atom-context_system.dm | 9 +- .../signals_item/signals_item_inventory.dm | 4 +- .../signals_mob/signals_mob_mobility.dm | 6 +- code/__DEFINES/projectiles/gun_attachment.dm | 52 ++++ .../projectiles/{guns.dm => guns-legacy.dm} | 0 code/__DEFINES/projectiles/projectile.dm | 2 +- code/__DEFINES/projectiles/system.dm | 2 +- code/__HELPERS/type_processing.dm | 7 +- code/game/click/context.dm | 2 +- code/game/click/items-item_attack_chain.dm | 2 +- code/game/objects/items/devices/flashlight.dm | 27 ++ code/game/rendering/legacy/radial.dm | 5 + code/modules/actions/action.dm | 25 +- code/modules/actions/action_button.dm | 2 +- code/modules/actions/action_drawer.dm | 2 +- code/modules/actions/action_drawer_toggle.dm | 2 +- code/modules/actions/action_holder.dm | 2 +- .../actions/types/attachment_action.dm | 25 ++ code/modules/actions/types/item_action.dm | 4 +- code/modules/actions/types/organ_action.dm | 6 +- .../mining/tools/kinetic_accelerator.dm | 10 + code/modules/mob/inventory/inventory.dm | 11 +- code/modules/mob/inventory/items.dm | 117 ++++---- code/modules/mob/mobility.dm | 3 + code/modules/projectiles/gun.dm | 263 ++++++++++++++++- .../projectiles/guns/attachments/bayonet.dm | 16 + .../guns/attachments/flashlight.dm | 94 ++++++ .../projectiles/guns/attachments/harness.dm | 97 ++++++ .../projectiles/guns/gun_attachment.dm | 276 ++++++++++++++++++ .../projectiles/attachments/bayonet.dmi | Bin 0 -> 272 bytes .../projectiles/attachments/flashlight.dmi | Bin 0 -> 1066 bytes .../projectiles/attachments/harness.dmi | Bin 0 -> 545 bytes icons/screen/actions/backgrounds.dmi | Bin 1499 -> 1712 bytes icons/screen/radial/actions.dmi | Bin 0 -> 368 bytes 35 files changed, 982 insertions(+), 99 deletions(-) create mode 100644 code/__DEFINES/projectiles/gun_attachment.dm rename code/__DEFINES/projectiles/{guns.dm => guns-legacy.dm} (100%) create mode 100644 code/modules/actions/types/attachment_action.dm create mode 100644 code/modules/projectiles/guns/attachments/bayonet.dm create mode 100644 code/modules/projectiles/guns/attachments/flashlight.dm create mode 100644 code/modules/projectiles/guns/attachments/harness.dm create mode 100644 code/modules/projectiles/guns/gun_attachment.dm create mode 100644 icons/modules/projectiles/attachments/bayonet.dmi create mode 100644 icons/modules/projectiles/attachments/flashlight.dmi create mode 100644 icons/modules/projectiles/attachments/harness.dmi create mode 100644 icons/screen/radial/actions.dmi diff --git a/citadel.dme b/citadel.dme index b213a43eea20..6bf3846e787a 100644 --- a/citadel.dme +++ b/citadel.dme @@ -307,7 +307,8 @@ #include "code\__DEFINES\projectiles\ammo_casing.dm" #include "code\__DEFINES\projectiles\ammo_magazine.dm" #include "code\__DEFINES\projectiles\gun.dm" -#include "code\__DEFINES\projectiles\guns.dm" +#include "code\__DEFINES\projectiles\gun_attachment.dm" +#include "code\__DEFINES\projectiles\guns-legacy.dm" #include "code\__DEFINES\projectiles\projectile.dm" #include "code\__DEFINES\projectiles\system.dm" #include "code\__DEFINES\radiation\flags.dm" @@ -2108,6 +2109,7 @@ #include "code\modules\actions\action_drawer.dm" #include "code\modules\actions\action_drawer_toggle.dm" #include "code\modules\actions\action_holder.dm" +#include "code\modules\actions\types\attachment_action.dm" #include "code\modules\actions\types\item_action.dm" #include "code\modules\actions\types\organ_action.dm" #include "code\modules\actionspeed\actionspeed_modifier.dm" @@ -4467,9 +4469,13 @@ #include "code\modules\projectiles\ammunition\calibers\special\rocket.dm" #include "code\modules\projectiles\guns\ballistic.dm" #include "code\modules\projectiles\guns\energy.dm" +#include "code\modules\projectiles\guns\gun_attachment.dm" #include "code\modules\projectiles\guns\launcher.dm" #include "code\modules\projectiles\guns\magic.dm" #include "code\modules\projectiles\guns\vox.dm" +#include "code\modules\projectiles\guns\attachments\bayonet.dm" +#include "code\modules\projectiles\guns\attachments\flashlight.dm" +#include "code\modules\projectiles\guns\attachments\harness.dm" #include "code\modules\projectiles\guns\ballistic\microbattery\medigun.dm" #include "code\modules\projectiles\guns\ballistic\microbattery\medigun_cells.dm" #include "code\modules\projectiles\guns\ballistic\microbattery\microbattery-casing.dm" diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom-context_system.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom-context_system.dm index a5fb0b6a8047..d6b1982211a5 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom-context_system.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom-context_system.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /// from base of /atom/proc/context_query: (list/options, datum/event_args/actor/e_args) /// options list is the same format as /atom/proc/context_query, insert directly to it. @@ -8,6 +8,13 @@ #define COMSIG_ATOM_CONTEXT_ACT "atom_context_act" #define RAISE_ATOM_CONTEXT_ACT_HANDLED (1<<0) +/// Creates context key +/// +/// * Used to ensure things like components piggybacking on an atom and +/// hooking the menu with signals don't collide with the atom or other +/// components using the same keys. +#define atom_context_key(atom, key) "[ref(atom)]-[key]" + /// create context /// todo: this is deprecated, i think. /// * name: name diff --git a/code/__DEFINES/dcs/signals/signals_item/signals_item_inventory.dm b/code/__DEFINES/dcs/signals/signals_item/signals_item_inventory.dm index 77773426f55c..0e0495af37e0 100644 --- a/code/__DEFINES/dcs/signals/signals_item/signals_item_inventory.dm +++ b/code/__DEFINES/dcs/signals/signals_item/signals_item_inventory.dm @@ -4,7 +4,7 @@ #define COMPONENT_ITEM_DROPPED_SUPPRESS_SOUND (1<<1) /// From base of obj/item/pickup: (mob/user, inv_op_flags, atom/old_loc) #define COMSIG_ITEM_PICKUP "item_pickup" -/// From base of obj/item/equipped(): (/mob/equipper, slot, inv_op_flags) +/// From base of obj/item/equipped(): (/mob/equipper, slot_id, inv_op_flags) #define COMSIG_ITEM_EQUIPPED "item_equip" -/// From base of obj/item/unequipped(): (/mob/unequipped, slot, inv_op_flags) +/// From base of obj/item/unequipped(): (/mob/unequipped, slot_id, inv_op_flags) #define COMSIG_ITEM_UNEQUIPPED "item_unequip" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_mobility.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_mobility.dm index 717d3c5771f1..c55b3eb8035c 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_mobility.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_mobility.dm @@ -1,4 +1,8 @@ -/// sent at the very end of /mob/update_mobility(): (new_mobility_flags) +/** + * sent at the very end of /mob/update_mobility(): (new_mobility_flags) + * + * * only sent if mobility actually changed + */ #define COMSIG_MOB_ON_UPDATE_MOBILITY "mob_updated_mobility" /// sent at the very end of /mob/set_resitng(): (new_resting) #define COMSIG_MOB_ON_SET_RESTING "mob_set_resting" diff --git a/code/__DEFINES/projectiles/gun_attachment.dm b/code/__DEFINES/projectiles/gun_attachment.dm new file mode 100644 index 000000000000..663b87f380f3 --- /dev/null +++ b/code/__DEFINES/projectiles/gun_attachment.dm @@ -0,0 +1,52 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +//* gun attachment slot *// + +/** + * * align_x is center pixel (or the left pixel of the 2-wide center, if center is 2 wide) + * * align_y is topmost pixel extending out of gun + */ +#define GUN_ATTACHMENT_SLOT_GRIP "grip" +/** + * * align_x is leftmost pixel of the part extending out of gun + * * align_y is center pixel (or the bottom pixel of center, if center is 2 wide) + */ +#define GUN_ATTACHMNET_SLOT_MUZZLE "muzzle" +/** + * * align_x is center pixel (or the left pixel of the 2-wide center, if center is 2 wide) + * * align_y is bottom pixel extending out of gun + */ +#define GUN_ATTACHMENT_SLOT_RAIL "rail" +/** + * * align_x is leftmost pixel of the part extending out of gun + * * align_y is center pixel (or the bottom pixel of center, if center is 2 wide) + * * this means that for many sidebarrel's, the align_x is actually right of the actual attachment because + * it'll be aligned to the pixel right of the muzzle, not to the interior of the gun! + */ +#define GUN_ATTACHMENT_SLOT_SIDEBARREL "sidebarrel" +/** + * * align_x is rightmost pixel extending left from the gun + * * align_y is top pixel of the area that actually attaches to the gun + */ +#define GUN_ATTACHMENT_SLOT_STOCK "stock" +/** + * * align_x is center pixel (or the left pixel of the 2-wide center, if center is 2 wide) + * * align_y is topmost pixel extending out of gun + */ +#define GUN_ATTACHMENT_SLOT_UNDERBARREL "underbarrel" + +// todo: DEFINE_ENUM + +//* gun attachment types *// + +/// flashlight +#define GUN_ATTACHMENT_TYPE_FLASHLIGHT (1<<0) +/// targeting laser +#define GUN_ATTACHMENT_TYPE_AIM_LASER (1<<1) +/// scope +#define GUN_ATTACHMENT_TYPE_SCOPE (1<<2) +/// magharness, lanyard, etc +#define GUN_ATTACHMENT_TYPE_HARNESS (1<<3) + +// todo: DEFINE_BITFIELD diff --git a/code/__DEFINES/projectiles/guns.dm b/code/__DEFINES/projectiles/guns-legacy.dm similarity index 100% rename from code/__DEFINES/projectiles/guns.dm rename to code/__DEFINES/projectiles/guns-legacy.dm diff --git a/code/__DEFINES/projectiles/projectile.dm b/code/__DEFINES/projectiles/projectile.dm index f87cff191198..ac906e1ac987 100644 --- a/code/__DEFINES/projectiles/projectile.dm +++ b/code/__DEFINES/projectiles/projectile.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// //* pre_impact(), impact(), bullet_act(), on_impact() impact_flags *// /// pre_impact, bullet_act, on_impact are called in that order /// diff --git a/code/__DEFINES/projectiles/system.dm b/code/__DEFINES/projectiles/system.dm index fafd60aac2ca..7d177322ac9a 100644 --- a/code/__DEFINES/projectiles/system.dm +++ b/code/__DEFINES/projectiles/system.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// //* rendering system //* this is currently only used on ammo magazines, as guns use composition of datums diff --git a/code/__HELPERS/type_processing.dm b/code/__HELPERS/type_processing.dm index 5ab1778c6eb6..f2496f77efad 100644 --- a/code/__HELPERS/type_processing.dm +++ b/code/__HELPERS/type_processing.dm @@ -19,9 +19,10 @@ /obj/item/mecha_parts/mecha_equipment/weapon = "//mech_weapon", /obj/item/mecha_parts = "//mech_part", /obj/item/organ = "//organ", - /obj/item/gun/ballistic = "//ballistic", - /obj/item/gun/energy = "//energy", - /obj/item/gun/magnetic = "//magnetic", + /obj/item/gun_attachment = "//gun-attachment", + /obj/item/gun/ballistic = "//gun-ballistic", + /obj/item/gun/energy = "//gun-energy", + /obj/item/gun/magnetic = "//gun-magnetic", /obj/item/gun = "//gun", /obj/item/ammo_casing = "//ammo", /obj/item/ammo_magazine = "//magazine", diff --git a/code/game/click/context.dm b/code/game/click/context.dm index 1a0686db1d69..e9ab863450c5 100644 --- a/code/game/click/context.dm +++ b/code/game/click/context.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2023 Citadel Station developers. *// +//* Copyright (c) 2024 Citadel Station Developers *// /** * get context options diff --git a/code/game/click/items-item_attack_chain.dm b/code/game/click/items-item_attack_chain.dm index 8812171abc2d..7dea582cfc3a 100644 --- a/code/game/click/items-item_attack_chain.dm +++ b/code/game/click/items-item_attack_chain.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 Citadel Station developers. *// +//* Copyright (c) 2024 Citadel Station Developers *// //* Code for interacting as an item. *// diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index 5ab5145b43cd..d5d7056ccf20 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -270,6 +270,33 @@ light_color = LIGHT_COLOR_FLUORESCENT_FLASHLIGHT light_wedge = LIGHT_NARROW + /// the gun attachment used for testing if we can attach + var/static/obj/item/gun_attachment/flashlight/maglight/test_attachment + +/obj/item/flashlight/maglight/proc/get_test_attachment() as /obj/item/gun_attachment/flashlight/maglight + if(!test_attachment) + test_attachment = new + return test_attachment + +/obj/item/flashlight/maglight/using_as_item(atom/target, datum/event_args/actor/clickchain/e_args, clickchain_flags, datum/callback/reachability_check) + . = ..() + if(. & CLICKCHAIN_DO_NOT_PROPAGATE) + return + if(istype(target, /obj/item/gun)) + var/obj/item/gun/gun_target = target + var/obj/item/gun_attachment/flashlight/maglight/test_attach = get_test_attachment() + if(gun_target.can_install_attachment(test_attach, e_args)) + if(!e_args.performer.temporarily_remove_from_inventory(src)) + e_args.chat_feedback(SPAN_WARNING("[src] is stuck to your hands!"), src) + return CLICKCHAIN_DO_NOT_PROPAGATE + var/obj/item/gun_attachment/flashlight/maglight/attaching = new + if(!gun_target.install_attachment(attaching, e_args)) + CRASH("install failed after check") + else + attaching.our_maglight = src + forceMove(attaching) + return CLICKCHAIN_DO_NOT_PROPAGATE + /obj/item/flashlight/drone name = "low-power flashlight" desc = "A miniature lamp, that might be used by small robots." diff --git a/code/game/rendering/legacy/radial.dm b/code/game/rendering/legacy/radial.dm index 39da472582e4..0cb587ad8654 100644 --- a/code/game/rendering/legacy/radial.dm +++ b/code/game/rendering/legacy/radial.dm @@ -254,12 +254,17 @@ GLOBAL_LIST_EMPTY(radial_menus) I.layer = FLOAT_LAYER //! end choices_icons[id] = I + else + choices_icons[id] = extract_image(E) setup_menu(use_tooltips) /datum/radial_menu/proc/extract_image(E) + if(!isimage(E) && !isatom(E) && !ismutableappearance(E) && !IS_APPEARANCE(E)) + return null var/mutable_appearance/MA = new /mutable_appearance(E) if(MA) MA.layer = HUD_LAYER_ABOVE + MA.plane = ABOVE_HUD_PLANE MA.appearance_flags |= RESET_TRANSFORM return MA diff --git a/code/modules/actions/action.dm b/code/modules/actions/action.dm index d1b07a88cb61..bd7117c5f973 100644 --- a/code/modules/actions/action.dm +++ b/code/modules/actions/action.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /** * action datums @@ -32,7 +32,7 @@ var/datum/callback/check_callback //* Target / Delegate *// - /// callback to invoke with (actor) on trigger at base of /invoke(). + /// callback to invoke with (datum/action/action, datum/event_args/actor/actor) on trigger at base of /invoke(). /// /// * return a truthy value from the callback to halt propagation var/datum/callback/invoke_callback @@ -51,14 +51,17 @@ //* Button(s) *// /// all buttons that are on us right now var/list/atom/movable/screen/movable/action_button/buttons + /// do not update buttons; something else manages them var/rendering_externally_managed = FALSE + /// where the button's background icon is from var/background_icon = 'icons/screen/actions/backgrounds.dmi' /// what the action's background state should be var/background_icon_state = "default" /// custom background overlay to add; this goes below button sprite / overlays! var/background_additional_overlay + /// the icon of the button's actual internal sprite, overlaid on the background var/button_icon = 'icons/screen/actions/actions.dmi' /// the icon_state of the button's actual internal sprite, overlaid on the background @@ -67,11 +70,17 @@ var/button_additional_only = FALSE /// custom overlay to add to all buttons; this is arbitrary, and can be a reference to an atom var/button_additional_overlay + /// set availability to; it must be 0 to 1, inclusive. var/button_availability = 1 /// default handling for availability should be invoked var/button_availability_automatic = TRUE + /// are we active? + var/button_active = FALSE + /// overlay to add to background if active + var/button_active_overlay = "active-1" + /datum/action/New(datum/target) if(!target_compatible(target)) qdel(src) @@ -108,6 +117,14 @@ if(update) update_buttons(TRUE) +/** + * set button active-ness + */ +/datum/action/proc/set_button_active(active, defer_update) + button_active = active + if(!defer_update) + update_buttons(TRUE) + /** * updates if availability changed */ @@ -168,6 +185,8 @@ generating.plane = HUD_PLANE generating.layer = HUD_LAYER_BASE + if(button_active && button_active_overlay) + generating.overlays += button_active_overlay if(background_additional_overlay) generating.overlays += background_additional_overlay @@ -257,7 +276,7 @@ /datum/action/proc/invoke(datum/event_args/actor/actor) PROTECTED_PROC(TRUE) // you thought i was joking??? do not directly call this goddamn proc. SHOULD_NOT_OVERRIDE(TRUE) - if(invoke_callback?.Invoke()) + if(invoke_callback?.Invoke(src, actor)) return TRUE if(invoke_target(target, actor)) return TRUE diff --git a/code/modules/actions/action_button.dm b/code/modules/actions/action_button.dm index 2a27852c2268..eb4179b046fa 100644 --- a/code/modules/actions/action_button.dm +++ b/code/modules/actions/action_button.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// INITIALIZE_IMMEDIATE(/atom/movable/screen/movable/action_button) /atom/movable/screen/movable/action_button diff --git a/code/modules/actions/action_drawer.dm b/code/modules/actions/action_drawer.dm index 9c4a9518426c..5df2ccd359d4 100644 --- a/code/modules/actions/action_drawer.dm +++ b/code/modules/actions/action_drawer.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /** * the actual action handler at /client, diff --git a/code/modules/actions/action_drawer_toggle.dm b/code/modules/actions/action_drawer_toggle.dm index 9243071306dd..80947075eb92 100644 --- a/code/modules/actions/action_drawer_toggle.dm +++ b/code/modules/actions/action_drawer_toggle.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// INITIALIZE_IMMEDIATE(/atom/movable/screen/movable/action_drawer_toggle) /atom/movable/screen/movable/action_drawer_toggle diff --git a/code/modules/actions/action_holder.dm b/code/modules/actions/action_holder.dm index 398299694893..2c3e1b110d1c 100644 --- a/code/modules/actions/action_holder.dm +++ b/code/modules/actions/action_holder.dm @@ -1,5 +1,5 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /** * holds a set of actions diff --git a/code/modules/actions/types/attachment_action.dm b/code/modules/actions/types/attachment_action.dm new file mode 100644 index 000000000000..7fe7dd62b864 --- /dev/null +++ b/code/modules/actions/types/attachment_action.dm @@ -0,0 +1,25 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/datum/action/attachment_action + target_type = /obj/item/gun_attachment + button_icon_state = null + + /// automatically set button_additional_overlay to the item, if button_icon_state is null + var/render_attachment_as_button = TRUE + +/datum/action/attachment_action/pre_render_hook() + if(render_attachment_as_button && isnull(button_icon_state)) + button_additional_only = TRUE + var/image/generated = new + generated.appearance = target + // i hope you are not doing custom layers and planes for icons, right gamers?? + generated.layer = FLOAT_LAYER + generated.plane = FLOAT_PLANE + button_additional_overlay = generated + return ..() + +/datum/action/attachment_action/calculate_availability() + var/obj/item/item = target + var/mob/worn = item.worn_mob() + return worn? (worn.mobility_flags & check_mobility_flags? 1 : 0) : 1 diff --git a/code/modules/actions/types/item_action.dm b/code/modules/actions/types/item_action.dm index c14605a84324..d600aa9d4219 100644 --- a/code/modules/actions/types/item_action.dm +++ b/code/modules/actions/types/item_action.dm @@ -1,10 +1,10 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /datum/action/item_action target_type = /obj/item - button_icon_state = null + /// automatically set button_additional_overlay to the item, if button_icon_state is null var/render_item_as_button = TRUE diff --git a/code/modules/actions/types/organ_action.dm b/code/modules/actions/types/organ_action.dm index b7228dc8c4c6..dfd5ca3ce492 100644 --- a/code/modules/actions/types/organ_action.dm +++ b/code/modules/actions/types/organ_action.dm @@ -1,10 +1,10 @@ //* This file is explicitly licensed under the MIT license. *// -//* Copyright (c) 2024 silicons *// +//* Copyright (c) 2024 Citadel Station Developers *// /datum/action/organ_action - target_type = /obj/item - + target_type = /obj/item/organ button_icon_state = null + /// automatically set button_additional_overlay to the organ var/render_organ_as_button = TRUE diff --git a/code/modules/mining/tools/kinetic_accelerator.dm b/code/modules/mining/tools/kinetic_accelerator.dm index cfcb7ae64758..cf0f6cf184f9 100644 --- a/code/modules/mining/tools/kinetic_accelerator.dm +++ b/code/modules/mining/tools/kinetic_accelerator.dm @@ -29,6 +29,16 @@ battery_lock = TRUE fire_sound = 'sound/weapons/kenetic_accel.ogg' render_use_legacy_by_default = FALSE + attachment_alignment = list( + GUN_ATTACHMENT_SLOT_RAIL = list( + 17, + 23, + ), + GUN_ATTACHMENT_SLOT_SIDEBARREL = list( + 30, + 17, + ), + ) var/overheat_time = 16 var/holds_charge = FALSE var/unique_frequency = FALSE // modified by KA modkits diff --git a/code/modules/mob/inventory/inventory.dm b/code/modules/mob/inventory/inventory.dm index bb1365b836bb..8d9c39a05ae1 100644 --- a/code/modules/mob/inventory/inventory.dm +++ b/code/modules/mob/inventory/inventory.dm @@ -134,6 +134,9 @@ //* Update Hooks *// +/** + * Only called if mobility changed. + */ /datum/inventory/proc/on_mobility_update() for(var/datum/action/action in actions.actions) action.update_button_availability() @@ -242,6 +245,8 @@ // this qdeleted catches unequipped() deleting the item. . = QDELETED(I)? FALSE : TRUE + log_inventory("[key_name(src)] unequipped [I] from [old].") + if(I) // todo: better rendering that takes observers into account if(client) @@ -262,8 +267,6 @@ else if(newloc != FALSE) I.forceMove(newloc) - log_inventory("[key_name(src)] unequipped [I] from [old].") - /mob/proc/handle_item_denesting(obj/item/I, old_slot, flags, mob/user) // if the item was inside something, if(I.worn_inside) @@ -605,12 +608,12 @@ _equip_slot(I, slot, flags) + log_inventory("[key_name(src)] equipped [I] to [slot].") + // TODO: HANDLE DELETIONS IN PICKUP AND EQUIPPED PROPERLY I.pickup(src, flags, oldLoc) I.equipped(src, slot, flags) - log_inventory("[key_name(src)] equipped [I] to [slot].") - if(I.zoom) I.zoom() diff --git a/code/modules/mob/inventory/items.dm b/code/modules/mob/inventory/items.dm index e0c62b3f61b9..fd57ebf81385 100644 --- a/code/modules/mob/inventory/items.dm +++ b/code/modules/mob/inventory/items.dm @@ -33,9 +33,22 @@ */ /obj/item/proc/equipped(mob/user, slot, flags) SHOULD_CALL_PARENT(TRUE) + + // set slot + worn_slot = slot + // register carry + if(isliving(user)) + var/mob/living/L = user + if((slot == SLOT_ID_HANDS)? (item_flags & ITEM_ENCUMBERS_WHILE_HELD) : !(item_flags & ITEM_ENCUMBERS_ONLY_HELD)) + if(flat_encumbrance) + L.recalculate_carry() + else + encumbrance_registered = get_encumbrance() + L.adjust_current_carry_encumbrance(encumbrance_registered) + // fire signals SEND_SIGNAL(src, COMSIG_ITEM_EQUIPPED, user, slot, flags) SEND_SIGNAL(user, COMSIG_MOB_ITEM_EQUIPPED, src, slot, flags) - worn_slot = slot + if(!(flags & INV_OP_IS_ACCESSORY)) // todo: shouldn't be in here hud_layerise() @@ -47,15 +60,6 @@ playsound(src, equip_sound, 30, ignore_walls = FALSE) user.update_inv_hands() - // register carry - if(isliving(user)) - var/mob/living/L = user - if((slot == SLOT_ID_HANDS)? (item_flags & ITEM_ENCUMBERS_WHILE_HELD) : !(item_flags & ITEM_ENCUMBERS_ONLY_HELD)) - if(flat_encumbrance) - L.recalculate_carry() - else - encumbrance_registered = get_encumbrance() - L.adjust_current_carry_encumbrance(encumbrance_registered) /** * called when an item is unequipped from inventory or moved around in inventory @@ -67,9 +71,20 @@ */ /obj/item/proc/unequipped(mob/user, slot, flags) SHOULD_CALL_PARENT(TRUE) + // clear slot + worn_slot = null + // clear carry + if(isliving(user)) + var/mob/living/L = user + if(flat_encumbrance) + L.recalculate_carry() + else if(!isnull(encumbrance_registered)) + L.adjust_current_carry_encumbrance(-encumbrance_registered) + encumbrance_registered = null + // fire signals SEND_SIGNAL(src, COMSIG_ITEM_UNEQUIPPED, user, slot, flags) SEND_SIGNAL(user, COMSIG_MOB_ITEM_UNEQUIPPED, src, slot, flags) - worn_slot = null + if(!(flags & INV_OP_IS_ACCESSORY)) // todo: shouldn't be in here hud_unlayerise() @@ -79,15 +94,6 @@ if(!(flags & INV_OP_DIRECTLY_DROPPING) && (slot != SLOT_ID_HANDS) && unequip_sound) playsound(src, unequip_sound, 30, ignore_walls = FALSE) - // clear carry - if(isliving(user)) - var/mob/living/L = user - if(flat_encumbrance) - L.recalculate_carry() - else if(!isnull(encumbrance_registered)) - L.adjust_current_carry_encumbrance(-encumbrance_registered) - encumbrance_registered = null - /** * called when a mob drops an item * @@ -99,39 +105,38 @@ /obj/item/proc/dropped(mob/user, flags, atom/newLoc) SHOULD_CALL_PARENT(TRUE) - hud_unlayerise() + // unset things item_flags &= ~ITEM_IN_INVENTORY - - . = SEND_SIGNAL(src, COMSIG_ITEM_DROPPED, user, flags, newLoc) - SEND_SIGNAL(user, COMSIG_MOB_ITEM_DROPPED, src, flags, newLoc) - - if(!(flags & INV_OP_SUPPRESS_SOUND) && isturf(newLoc) && !(. & COMPONENT_ITEM_DROPPED_SUPPRESS_SOUND)) - playsound(src, drop_sound, 30, ignore_walls = FALSE) - // user?.update_equipment_speed_mods() - if(zoom) - zoom() //binoculars, scope, etc - - // unload actions - unregister_item_actions(user) - // clear carry if(isliving(user)) var/mob/living/L = user L.adjust_current_carry_weight(-weight_registered) weight_registered = null - + // unload actions + unregister_item_actions(user) // close context menus context_close() - // storage stuff obj_storage?.on_dropped(user) - // get rid of shieldcalls for(var/datum/shieldcall/shieldcall as anything in shieldcalls) if(!shieldcall.shields_in_inventory) continue user.unregister_shieldcall(shieldcall) + //! LEGACY + hud_unlayerise() + if(!(flags & INV_OP_SUPPRESS_SOUND) && isturf(newLoc) && !(. & COMPONENT_ITEM_DROPPED_SUPPRESS_SOUND)) + playsound(src, drop_sound, 30, ignore_walls = FALSE) + // user?.update_equipment_speed_mods() + if(zoom) + zoom() //binoculars, scope, etc + //! END + + // fire signals + . = SEND_SIGNAL(src, COMSIG_ITEM_DROPPED, user, flags, newLoc) + SEND_SIGNAL(user, COMSIG_MOB_ITEM_DROPPED, src, flags, newLoc) + if((item_flags & ITEM_DROPDEL) && !(flags & INV_OP_DELETING)) qdel(src) . |= ITEM_RELOCATED_BY_DROPPED @@ -146,41 +151,39 @@ /obj/item/proc/pickup(mob/user, flags, atom/oldLoc) SHOULD_CALL_PARENT(TRUE) + // set things + item_flags |= ITEM_IN_INVENTORY // we load the component here as it hooks equipped, // so loading it here means it can still handle the equipped signal. if(passive_parry) LoadComponent(/datum/component/passive_parry, passive_parry) - - SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user, flags, oldLoc) - SEND_SIGNAL(user, COMSIG_MOB_ITEM_PICKUP, src, flags, oldLoc) - - reset_pixel_offsets() - hud_layerise() - - item_flags |= ITEM_IN_INVENTORY - - // todo: should this be here - transform = null - if(isturf(oldLoc) && !(flags & (INV_OP_SILENT | INV_OP_DIRECTLY_EQUIPPING))) - playsound(src, pickup_sound, 20, ignore_walls = FALSE) - + // load action buttons + register_item_actions(user) // register carry weight_registered = get_weight() if(isliving(user)) var/mob/living/L = user L.adjust_current_carry_weight(weight_registered) - - // storage stuff - obj_storage?.on_pickup(user) - // register shieldcalls for(var/datum/shieldcall/shieldcall as anything in shieldcalls) if(!shieldcall.shields_in_inventory) continue user.register_shieldcall(shieldcall) + // storage stuff + obj_storage?.on_pickup(user) - // load action buttons - register_item_actions(user) + //! LEGACY + reset_pixel_offsets() + hud_layerise() + // todo: should this be here + transform = null + if(isturf(oldLoc) && !(flags & (INV_OP_SILENT | INV_OP_DIRECTLY_EQUIPPING))) + playsound(src, pickup_sound, 20, ignore_walls = FALSE) + //! END + + // fire signals + SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user, flags, oldLoc) + SEND_SIGNAL(user, COMSIG_MOB_ITEM_PICKUP, src, flags, oldLoc) /** * update our worn icon if we can diff --git a/code/modules/mob/mobility.dm b/code/modules/mob/mobility.dm index 38d9b60d9de9..c26f243edf93 100644 --- a/code/modules/mob/mobility.dm +++ b/code/modules/mob/mobility.dm @@ -42,8 +42,11 @@ . &= ~MOBILITY_CAN_STAND // set, return, and signal. + var/old = mobility_flags mobility_flags = (. & ~(blocked | mobility_flags_blocked)) | (forced | mobility_flags_forced) . = mobility_flags + if(mobility_flags == old) + return SEND_SIGNAL(src, COMSIG_MOB_ON_UPDATE_MOBILITY, .) inventory?.on_mobility_update() diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 20d133ea8abd..14412db63746 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -30,7 +30,14 @@ for(var/propname in settings) gun.vars[propname] = settings[propname] -//Parent gun type. Guns are weapons that can be aimed at mobs and act over a distance +/** + * Weapons that can be aimed at an angle or a mob or whatever. + * + * Current caveats: + * + * * Flashlight attachments directly edit the light variable of the gun. This means that they'll trample the gun's + * inherent light if there is one. + */ /obj/item/gun name = "gun" desc = "Its a gun. It's pretty terrible, though." @@ -62,6 +69,23 @@ /// * this is a default value; set to null by default to have the projectile's say. var/accuracy_disabled = null + //* Attachments *// + + /// Installed attachments + var/list/obj/item/gun_attachment/attachments + /// Attachment alignments. + /// + /// * Format: "attachment slot" = list(x, y) + /// * Typelisted. If you varedit this, be aware of that. + /// * If an attachment slot isn't here, it's not allowed on the gun. + /// * See `code/__DEFINES/projectiles/gun_attachment.dm` for what this is doing to the attachments. + /// We basically match the specified align_x/y pixel on the attachment to the x/y on the gun's sprite + /// specified here. + /// * This is pixel coordinates on the gun's real icon. Out of bounds is allowed as attachments are just overlays. + var/list/attachment_alignment + /// Blacklisted attachment types. + var/attachment_type_blacklist = NONE + // legacy below // var/burst = 1 @@ -180,7 +204,7 @@ /obj/item/gun/Initialize(mapload) . = ..() - // instantiate & dedupe renderers + //* instantiate & dedupe renderers *// var/requires_icon_update if(item_renderer) if(ispath(item_renderer) || IS_ANONYMOUS_TYPEPATH(item_renderer)) @@ -204,6 +228,11 @@ SLOT_ID_RIGHT_HAND = 'icons/mob/items/righthand_guns.dmi', ) + //* handle attachment typelists *// + if(attachment_alignment) + attachment_alignment = typelist(NAMEOF(src, attachment_alignment), attachment_alignment) + + //! LEGACY: firemodes for(var/i in 1 to firemodes.len) var/key = firemodes[i] if(islist(key)) @@ -216,12 +245,19 @@ sel_mode = 0 switch_firemodes() + //! LEGACY: accuracy if(isnull(scoped_accuracy)) scoped_accuracy = accuracy + //! LEGACY: pin if(pin) pin = new pin(src) +/obj/item/gun/Destroy() + QDEL_NULL(pin) + QDEL_LIST(attachments) + return ..() + /obj/item/gun/CtrlClick(mob/user) if(can_flashlight && ishuman(user) && src.loc == usr && !user.incapacitated(INCAPACITATION_ALL)) toggle_flashlight() @@ -349,6 +385,14 @@ return return ..() //Pistolwhippin' +/obj/item/gun/using_item_on(obj/item/using, datum/event_args/actor/clickchain/e_args, clickchain_flags, datum/callback/reachability_check) + . = ..() + if(. & CLICKCHAIN_DO_NOT_PROPAGATE) + return + if(istype(using, /obj/item/gun_attachment)) + user_install_attachment(using, e_args) + return CLICKCHAIN_DO_NOT_PROPAGATE + /obj/item/gun/attackby(obj/item/A, mob/user) if(A.is_multitool()) if(!scrambled) @@ -487,11 +531,12 @@ accuracy = initial(accuracy) //Reset the gun's accuracyw - if(muzzle_flash) - if(gun_light) - set_light(light_brightness) - else - set_light(0) + // todo: better muzzle flash + // if(muzzle_flash) + // if(gun_light) + // set_light(light_brightness) + // else + // set_light(0) // Similar to the above proc, but does not require a user, which is ideal for things like turrets. /obj/item/gun/proc/Fire_userless(atom/target) @@ -797,7 +842,9 @@ var/datum/firemode/current_mode = firemodes[sel_mode] . += "The fire selector is set to [current_mode.name]." if(safety_state != GUN_NO_SAFETY) - to_chat(user, SPAN_NOTICE("The safety is [check_safety() ? "on" : "off"].")) + . += SPAN_NOTICE("The safety is [check_safety() ? "on" : "off"].") + for(var/obj/item/gun_attachment/attachment as anything in attachments) + . += "It has [attachment] installed on its [attachment.attachment_slot].[attachment.can_detach ? "" : " It doesn't look like it can be removed."]" /obj/item/gun/proc/switch_firemodes(mob/user) if(firemodes.len <= 1) @@ -870,12 +917,6 @@ if(usr == loc) toggle_safety(usr) -/obj/item/gun/AltClick(mob/user) - if(loc == user) - toggle_safety(user) - return TRUE - return ..() - /** * returns TRUE/FALSE based on if we have safeties on */ @@ -888,6 +929,16 @@ return return firemodes[sel_mode] +/obj/item/gun/register_item_actions(mob/user) + . = ..() + for(var/obj/item/gun_attachment/attachment as anything in attachments) + attachment.register_attachment_actions(user) + +/obj/item/gun/unregister_item_actions(mob/user) + . = ..() + for(var/obj/item/gun_attachment/attachment as anything in attachments) + attachment.unregister_attachment_actions(user) + //* Ammo *// /** @@ -900,6 +951,190 @@ /obj/item/gun/proc/get_ammo_ratio() return 0 +//* Attachments *// + +/** + * Check if we can attach an attachment + */ +/obj/item/gun/proc/can_install_attachment(obj/item/gun_attachment/attachment, datum/event_args/actor/actor, silent) + if(!attachment.attachment_slot || !attachment_alignment[attachment.attachment_slot]) + if(!silent) + actor?.chat_feedback( + SPAN_WARNING("[attachment] won't fit anywhere on [src]!"), + target = src, + ) + return FALSE + if(attachment.attachment_type & attachment_type_blacklist) + if(!silent) + actor?.chat_feedback( + SPAN_WARNING("[attachment] doesn't work with [src]!"), + target = src, + ) + return FALSE + for(var/obj/item/gun_attachment/existing as anything in attachments) + if(existing.attachment_slot == attachment.attachment_slot) + if(!silent) + actor?.chat_feedback( + SPAN_WARNING("[src] already has [existing] installed on its [existing.attachment_slot]!"), + target = src, + ) + return FALSE + if(existing.attachment_type & attachment.attachment_type) + if(!silent) + actor?.chat_feedback( + SPAN_WARNING("[src]'s [existing] conflicts with [attachment]!"), + target = src, + ) + return FALSE + if(!attachment.fits_on_gun(src, actor, silent)) + return FALSE + return TRUE + +/** + * Called when a mob tries to uninstall an attachment + */ +/obj/item/gun/proc/user_install_attachment(obj/item/gun_attachment/attachment, datum/event_args/actor/actor) + if(actor) + if(actor.performer && actor.performer.is_in_inventory(attachment)) + if(!actor.performer.can_unequip(attachment, attachment.worn_slot)) + actor.chat_feedback( + SPAN_WARNING("[attachment] is stuck to your hand!"), + target = src, + ) + return FALSE + if(!install_attachment(attachment, actor)) + return FALSE + // todo: better sound + playsound(src, 'sound/weapons/empty.ogg', 25, TRUE, -3) + return TRUE + +/** + * Installs an attachment + * + * * This moves the attachment into the gun if it isn't already. + * * This does have default visible feedback for the installation. + * + * @return TRUE / FALSE on success / failure + */ +/obj/item/gun/proc/install_attachment(obj/item/gun_attachment/attachment, datum/event_args/actor/actor, silent) + if(!can_install_attachment(attachment, actor, silent)) + return FALSE + + if(!silent) + actor?.visible_feedback( + target = src, + visible = SPAN_NOTICE("[actor.performer] attaches [attachment] to [src]'s [attachment.attachment_slot]."), + ) + if(attachment.loc != src) + attachment.forceMove(src) + + LAZYADD(attachments, attachment) + attachment.attached = src + attachment.on_attach(src) + attachment.update_gun_overlay() + on_attachment_install(attachment) + var/mob/holding_mob = worn_mob() + if(holding_mob) + attachment.register_attachment_actions(holding_mob) + return TRUE + +/** + * Called when a mob tries to uninstall an attachment + */ +/obj/item/gun/proc/user_uninstall_attachment(obj/item/gun_attachment/attachment, datum/event_args/actor/actor, put_in_hands) + if(!attachment.can_detach) + actor?.chat_feedback( + SPAN_WARNING("[attachment] is not removable."), + target = src, + ) + return FALSE + var/obj/item/uninstalled = uninstall_attachment(attachment, actor) + if(put_in_hands && actor?.performer) + actor.performer.put_in_hands_or_drop(uninstalled) + else + var/atom/where_to_drop = drop_location() + ASSERT(where_to_drop) + uninstalled.forceMove(where_to_drop) + // todo: better sound + playsound(src, 'sound/weapons/empty.ogg', 25, TRUE, -3) + return TRUE + +/** + * Uninstalls an attachment + * + * * This does not move the attachment after uninstall; you have to do that. + * * This does not have default visible feedback for the uninstallation / removal. + * + * @return the /obj/item uninstalled + */ +/obj/item/gun/proc/uninstall_attachment(obj/item/gun_attachment/attachment, datum/event_args/actor/actor, silent, deleting) + ASSERT(attachment.attached == src) + var/mob/holding_mob = worn_mob() + if(holding_mob) + attachment.unregister_attachment_actions(holding_mob) + attachment.on_detach(src) + attachment.remove_gun_overlay() + attachment.attached = null + on_attachment_uninstall(attachment) + LAZYREMOVE(attachments, attachment) + return deleting ? null : attachment.uninstall_product_transform(src) + +/** + * Align an attachment overlay. + * + * @return TRUE / FALSE on success / failure + */ +/obj/item/gun/proc/align_attachment_overlay(obj/item/gun_attachment/attachment, image/appearancelike) + var/list/alignment = attachment_alignment?[attachment.attachment_slot] + if(!alignment) + return FALSE + appearancelike.pixel_x = (alignment[1] - attachment.align_x) + appearancelike.pixel_y = (alignment[2] - attachment.align_y) + return TRUE + +/** + * Called exactly once when an attachment is installed + * + * * Called before the attachment's on_attach() + */ +/obj/item/gun/proc/on_attachment_install(obj/item/gun_attachment/attachment) + PROTECTED_PROC(TRUE) + +/** + * Called exactly once when an attachment is uninstalled + * + * * Called after the attachment's on_detach() + */ +/obj/item/gun/proc/on_attachment_uninstall(obj/item/gun_attachment/attachment) + PROTECTED_PROC(TRUE) + +//* Context *// + +/obj/item/gun/context_query(datum/event_args/actor/e_args) + . = ..() + if(length(attachments)) + .["remove-attachment"] = atom_context_tuple("Remove Attachment", image('icons/screen/radial/actions.dmi', "red-arrow-up"), 0, MOBILITY_CAN_USE) + if(safety_state != GUN_NO_SAFETY) + .["toggle-safety"] = atom_context_tuple("Toggle Safety", image(src), 0, MOBILITY_CAN_USE, TRUE) + +/obj/item/gun/context_act(datum/event_args/actor/e_args, key) + . = ..() + if(.) + return + switch(key) + if("remove-attachment") + // todo: e_args support + var/obj/item/gun_attachment/attachment = show_radial_menu(e_args.initiator, src, attachments) + if(!attachment) + return TRUE + if(!e_args.performer.Reachability(src) || !(e_args.performer.mobility_flags & MOBILITY_CAN_USE)) + return TRUE + user_uninstall_attachment(attachment, e_args, TRUE) + return TRUE + if("toggle-safety") + toggle_safety(e_args.performer) + return TRUE + //* Rendering *// /obj/item/gun/update_icon(updates) diff --git a/code/modules/projectiles/guns/attachments/bayonet.dm b/code/modules/projectiles/guns/attachments/bayonet.dm new file mode 100644 index 000000000000..ce5e409bcf2b --- /dev/null +++ b/code/modules/projectiles/guns/attachments/bayonet.dm @@ -0,0 +1,16 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/obj/item/gun_attachment/bayonet + abstract_type = /obj/item/gun_attachment/bayonet + icon = 'icons/modules/projectiles/attachments/bayonet.dmi' + +/obj/item/gun_attachment/bayonet/combat_knife + name = "combat knife bayonet" + desc = "A bayonet that's just a particularly tactical knife attached to a gun. Does do the job, though." + // todo: prototype id. also, generic bayonet knife mount instead? + icon_state = "combat-knife" + align_x = 1 + align_y = 1 + +// todo: make this actually work; also, this should adapt to certain knives much like the maglight does. diff --git a/code/modules/projectiles/guns/attachments/flashlight.dm b/code/modules/projectiles/guns/attachments/flashlight.dm new file mode 100644 index 000000000000..26de336192fb --- /dev/null +++ b/code/modules/projectiles/guns/attachments/flashlight.dm @@ -0,0 +1,94 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/** + * A gun-attached flashlight + * + * * [light_range] and [light_color] are set directly, as they don't determine + * if a light source is enabled. + * * [light_power] is toggled when on/off. + * * We will set our icon_state directly, not gun_state. Do not use gun_state with flashlights. + */ +/obj/item/gun_attachment/flashlight + abstract_type = /obj/item/gun_attachment/flashlight + icon = 'icons/modules/projectiles/attachments/flashlight.dmi' + light_range = 4.75 + light_color = "#ffffff" + light_power = 0 + + attachment_action_name = "Toggle Light" + attachment_type = GUN_ATTACHMENT_TYPE_FLASHLIGHT + + /// are we on? + var/on = FALSE + /// power when on + var/light_power_on = 0.75 + /// activation sound + // todo: better sound + var/on_sound = 'sound/weapons/empty.ogg' + /// deactivation sound + // todo: better sound + var/off_sound = 'sound/weapons/empty.ogg' + +/obj/item/gun_attachment/flashlight/ui_action_click(datum/action/action, datum/event_args/actor/actor) + set_on(!on) + +/obj/item/gun_attachment/flashlight/on_detach(obj/item/gun/gun) + set_on(FALSE) + ..() + +/obj/item/gun_attachment/flashlight/update_icon_state() + icon_state = "[base_icon_state || initial(icon_state)][on ? "-on" : ""]" + return ..() + +/obj/item/gun_attachment/flashlight/proc/set_on(state, datum/event_args/actor/actor) + if(on == state) + return + on = state + update_icon() + var/datum/action/potential_action = istype(attachment_actions, /datum/action) ? attachment_actions : null + // todo: silent support? + if(on) + playsound(src, on_sound, 15, TRUE, -4) + attached.set_light(light_range, light_power_on, light_color) + potential_action?.set_button_active(TRUE) + else + playsound(src, off_sound, 15, TRUE, -4) + attached.set_light(light_range, 0, light_color) + potential_action?.set_button_active(FALSE) + +// todo: make this directional at some point +/obj/item/gun_attachment/flashlight/rail + name = "rail light" + icon_state = "raillight" + prototype_id = "attachment-rail-light" + align_x = 19 + align_y = 17 + attachment_slot = GUN_ATTACHMENT_SLOT_RAIL + +// todo: make this directional at some point +/** + * Actually a 'virtual' attachment. When uninstalled, will drop a maglight instead of itself. + */ +/obj/item/gun_attachment/flashlight/maglight + name = "maglight" + icon_state = "maglight" + prototype_id = "attachment-mag-light" + align_x = 11 + align_y = 3 + attachment_slot = GUN_ATTACHMENT_SLOT_SIDEBARREL + /// the maglight that made us + var/obj/item/flashlight/maglight/our_maglight + +/obj/item/gun_attachment/flashlight/maglight/Destroy() + QDEL_NULL(our_maglight) + return ..() + +/obj/item/gun_attachment/flashlight/maglight/uninstall_product_transform(atom/move_to_temporarily) + if(our_maglight) + . = our_maglight + our_maglight.forceMove(move_to_temporarily) + our_maglight = null + else + return new /obj/item/flashlight/maglight + qdel(src) diff --git a/code/modules/projectiles/guns/attachments/harness.dm b/code/modules/projectiles/guns/attachments/harness.dm new file mode 100644 index 000000000000..6dc21922bdb9 --- /dev/null +++ b/code/modules/projectiles/guns/attachments/harness.dm @@ -0,0 +1,97 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/obj/item/gun_attachment/harness + abstract_type = /obj/item/gun_attachment/harness + icon = 'icons/modules/projectiles/attachments/harness.dmi' + +/obj/item/gun_attachment/harness/magnetic + name = "magnetic harness" + desc = "A fancy harness that will snap a gun back to an attachment point when it's dropped by its wearer." + prototype_id = "attachment-magnetic-harness" + icon_state = "magnetic" + align_x = 15 + align_y = 16 + attachment_slot = GUN_ATTACHMENT_SLOT_RAIL + + /// active? + var/active = FALSE + /// activation sound + // todo: better sound + var/activate_sound = 'sound/weapons/empty.ogg' + /// deactivation sound + // todo: better sound + var/deactivate_sound = 'sound/weapons/empty.ogg' + + attachment_action_name = "Engage Harness" + +/obj/item/gun_attachment/harness/magnetic/on_attach(obj/item/gun/gun) + ..() + RegisterSignal(gun, COMSIG_ITEM_DROPPED, PROC_REF(on_drop)) + RegisterSignal(gun, COMSIG_ITEM_PICKUP, PROC_REF(on_pickup)) + +/obj/item/gun_attachment/harness/magnetic/on_detach(obj/item/gun/gun) + ..() + UnregisterSignal(gun, list( + COMSIG_ITEM_PICKUP, + COMSIG_ITEM_DROPPED, + )) + +/obj/item/gun_attachment/harness/magnetic/ui_action_click(datum/action/action, datum/event_args/actor/actor) + set_active(!active, actor) + +/obj/item/gun_attachment/harness/magnetic/proc/on_drop(datum/source, mob/user, inv_op_flags, atom/new_loc) + SIGNAL_HANDLER + if(!active) + return NONE + if(inv_op_flags & (INV_OP_DELETING | INV_OP_SHOULD_NOT_INTERCEPT)) + return NONE + // don't react if it was already yanked + if(attached.loc != user) + return NONE + if(!snap_back_to_user(user)) + return NONE + return COMPONENT_ITEM_DROPPED_RELOCATE | COMPONENT_ITEM_DROPPED_SUPPRESS_SOUND + +/obj/item/gun_attachment/harness/magnetic/proc/on_pickup(datum/source, mob/user, inv_op_flags, atom/old_loc) + SIGNAL_HANDLER + if(active) + return + set_active(TRUE, no_message = TRUE) + to_chat(user, SPAN_NOTICE("The [src] engages as you pick up \the [attached].")) + +/obj/item/gun_attachment/harness/magnetic/proc/snap_back_to_user(mob/user) + var/target_slot_phrase + for(var/slot_id in list( + /datum/inventory_slot/inventory/suit_storage, + /datum/inventory_slot/inventory/back, + )) + if(!user.equip_to_slot_if_possible(attached, slot_id, INV_OP_SILENT)) + continue + var/datum/inventory_slot/slot = resolve_inventory_slot(slot_id) + target_slot_phrase = slot.display_name + . = TRUE + if(!.) + return + attached.visible_message( + SPAN_WARNING("[attached] snaps back to [user]'s [target_slot_phrase]!"), + range = MESSAGE_RANGE_COMBAT_SILENCED, + ) + +/obj/item/gun_attachment/harness/magnetic/proc/set_active(state, datum/event_args/actor/actor, no_sound, no_message) + if(state == active) + return + active = state + var/datum/action/potential_action = istype(attachment_actions, /datum/action) ? attachment_actions : null + if(active) + potential_action?.set_button_active(TRUE) + if(!no_sound) + playsound(src, activate_sound, 15, TRUE, -4) + if(!no_message) + actor?.chat_feedback(SPAN_NOTICE("You activate \the [src]."), target = attached) + else + potential_action?.set_button_active(FALSE) + if(!no_sound) + playsound(src, deactivate_sound, 15, TRUE, -4) + if(!no_message) + actor?.chat_feedback(SPAN_NOTICE("You deactivate \the [src]."), target = attached) diff --git a/code/modules/projectiles/guns/gun_attachment.dm b/code/modules/projectiles/guns/gun_attachment.dm new file mode 100644 index 000000000000..f160c2c1ce41 --- /dev/null +++ b/code/modules/projectiles/guns/gun_attachment.dm @@ -0,0 +1,276 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2024 Citadel Station Developers *// + +/** + * Base type of gun attachments + * + * Invariants: + * + * * while attached, our loc is **always** the gun we're attached to + * * on_attach() will only ever be called once during one logical attach/detach cycle + * * on_detach() will only ever be called once during one logical attach/detach cycle + * * on_attach() will be properly called if we are created as part of a gun's init + * * on_detach() will be properly called if we, or the gun, are destroyed + */ +/obj/item/gun_attachment + //* System *// + + /// The gun we're attached to + var/obj/item/gun/attached + /// Can detach + var/can_detach = TRUE + + //* Actions *// + + /// cached action descriptors + /// + /// this can be: + /// * a /datum/action instance + /// * a /datum/action typepath + /// * a list of /datum/action instancse + /// * a list of /datum/action typepaths + /// + /// typepaths get instanced on us entering inventory + var/attachment_actions + /// if [attachment_actions] is not set, and this is, we make a single action rendering ourselves + /// and set its name to this + var/attachment_action_name + /// if [attachment_actions] is not set, and [action_name] is set, this is the mobility flags + /// the action will check for + var/attachment_action_mobility_flags = MOBILITY_CAN_HOLD | MOBILITY_CAN_USE + + /// grant item actions to gun wielder + var/relay_item_actions = TRUE + + //* Alignment *// + + /// alignment x + /// + /// * what this is depends on our [attachment_slot] + var/align_x = 0 + /// alignment y + /// + /// * what this is depends on our [attachment_slot] + var/align_y = 0 + + //* Rendering *// + + /// the icon-state in our icon to use for the gun overlay + /// + /// * defaults to "[icon_state]-gun" otherwise + var/gun_state + /// the current applied overlay + /// + /// * only update_gun_overlay() can modify this, and you shouldn't be using this for anything in a non-read-only + /// context. no, you are not special; there's no exceptions + var/list/appearance/gun_applied_overlay + + //* Slots *// + + /// attachment slot enum + var/attachment_slot + /// attachment type conflict; if set, we cannot be attached if another of this type + /// is attached. + var/attachment_type = NONE + +/obj/item/gun_attachment/Destroy() + if(attached) + attached.uninstall_attachment(src, deleting = TRUE) + return ..() + +/obj/item/gun_attachment/update_icon() + // update_icon_state can change state + var/old_state = icon_state + . = ..() + if(icon_state != old_state) + update_gun_overlay() + +/** + * Checks if we fit on a gun. + * + * * gun already checks for [attachment_slot] + * * gun already checks for [attachment_type] + */ +/obj/item/gun_attachment/proc/fits_on_gun(obj/item/gun/gun, datum/event_args/actor/actor, silent) + return TRUE + +/** + * called on attach (including at init) + * + * * `attached` is set by this point + * * [attachment_actions] is handled gun-side + * * overlay addition is handled on the gun side, but can be handled on our side too + */ +/obj/item/gun_attachment/proc/on_attach(obj/item/gun/gun) + SHOULD_CALL_PARENT(TRUE) + +/** + * called on detach (including during destroy) + * + * * `attached` is not cleared yet, at this point + * * [attachment_actions] is handled gun-side + * * overlay removal is handled on the gun side + */ +/obj/item/gun_attachment/proc/on_detach(obj/item/gun/gun) + SHOULD_CALL_PARENT(TRUE) + +/** + * sets our gun state + * + * * setting it to null is a good way to force an update if our icon state is being used + */ +/obj/item/gun_attachment/proc/set_gun_state(new_state) + gun_state = new_state + update_gun_overlay() + +/** + * get the overlay to apply to our gun + */ +/obj/item/gun_attachment/proc/get_gun_overlay() + if(!attached) + return null + var/mutable_appearance/applying = new /mutable_appearance + applying.icon = icon + applying.icon_state = gun_state || "[icon_state]-gun" + applying.layer = FLOAT_LAYER + applying.plane = FLOAT_PLANE + attached.align_attachment_overlay(src, applying) + return applying + +/** + * reapplies gun overlay + */ +/obj/item/gun_attachment/proc/update_gun_overlay() + if(!attached) + return + remove_gun_overlay() + var/appearance/applying = get_gun_overlay() + if(!applying) + return + // no stuck overlays now + ASSERT(!gun_applied_overlay) + attached.add_overlay(applying, TRUE) + gun_applied_overlay = applying + +/** + * removes gun overlay + */ +/obj/item/gun_attachment/proc/remove_gun_overlay() + if(!attached) + return + if(!gun_applied_overlay) + return + attached.cut_overlay(gun_applied_overlay, TRUE) + gun_applied_overlay = null + +/** + * This is a very special proc. + * + * This proc allows you to 'redirect' the actual item uninstalled to another item, + * useful for 'virtual' attachments made for things like maglights. + * + * The item returned will be what is dropped / put into the user's hands. + * + * If you return something else, you generally want to qdel(src). + * + * * This proc will **not** be called if we're being deleted. + * + * @params + * * move_to_temporarily - move something to here if it's being dropped, or it'll be deleted by root of /movable/Destroy() + * due to being in contents. + */ +/obj/item/gun_attachment/proc/uninstall_product_transform(atom/move_to_temporarily) + return src + +//* Actions *// + +/** + * instructs all our action buttons to re-render + */ +/obj/item/gun_attachment/update_action_buttons() + ..() + if(islist(attachment_actions)) + for(var/datum/action/action in attachment_actions) + action.update_buttons() + else if(istype(attachment_actions, /datum/action)) + var/datum/action/action = attachment_actions + action.update_buttons() + +/** + * ensures our [attachment_actions] variable is set to: + * + * * null + * * a list of actions + * * an action instance + */ +/obj/item/gun_attachment/proc/ensure_attachment_actions_loaded() + if(attachment_actions) + if(islist(attachment_actions)) + var/requires_init = FALSE + for(var/i in 1 to length(attachment_actions)) + if(ispath(attachment_actions[i])) + requires_init = TRUE + break + if(requires_init) + set_attachment_actions_to(attachment_actions) + else if(ispath(attachment_actions)) + set_attachment_actions_to(attachment_actions) + else if(attachment_action_name) + var/datum/action/attachment_action/created = new(src) + created.name = attachment_action_name + created.check_mobility_flags = attachment_action_mobility_flags + set_attachment_actions_to(created) + +/** + * setter for [attachment_actions] + * + * accepts: + * + * * an instance of /datum/action + * * a typepath of /datum/action + * * a list of /datum/action instances and typepaths + * * null + */ +/obj/item/gun_attachment/proc/set_attachment_actions_to(descriptor) + var/mob/worn_mob = attached.worn_mob() + + if(worn_mob) + unregister_attachment_actions(worn_mob) + + if(ispath(descriptor, /datum/action)) + descriptor = new descriptor(src) + else if(islist(descriptor)) + var/list/transforming = descriptor:Copy() + for(var/i in 1 to length(transforming)) + if(ispath(transforming[i], /datum/action)) + var/path = transforming[i] + transforming[i] = new path(src) + descriptor = transforming + else + attachment_actions = descriptor + + if(worn_mob) + register_attachment_actions(worn_mob) + +/** + * handles action granting + */ +/obj/item/gun_attachment/proc/register_attachment_actions(mob/user) + ensure_attachment_actions_loaded() + if(islist(attachment_actions)) + for(var/datum/action/action in attachment_actions) + action.grant(user.inventory.actions) + else if(istype(attachment_actions, /datum/action)) + var/datum/action/action = attachment_actions + action.grant(user.inventory.actions) + +/** + * handles action revoking + */ +/obj/item/gun_attachment/proc/unregister_attachment_actions(mob/user) + if(islist(attachment_actions)) + for(var/datum/action/action in attachment_actions) + action.revoke(user.inventory.actions) + else if(istype(attachment_actions, /datum/action)) + var/datum/action/action = attachment_actions + action.revoke(user.inventory.actions) diff --git a/icons/modules/projectiles/attachments/bayonet.dmi b/icons/modules/projectiles/attachments/bayonet.dmi new file mode 100644 index 0000000000000000000000000000000000000000..6a25513b994b5ec7f39b86f4abe7ddbcb55e6a65 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvp#Yx{*8>L*OrN{f%_Yd#&|XaK~d@VFTn*L zzdq6O*3~+9=6vvmP=kxc4<6~9_t8AbP}I}A!@@Yo__DE=!kkBwj!X(s2-dW;H`mo`{az;^d?OO+OOm3 z;uxYaF*!kk^{^Pnp$3K!hnY7eIi^lwO4Mqa;&6cFTzWtu5b%n&d}dr$&&qqe=%6l8 OAA_f>pUXO@geCw#%}I+ryj1qVgy2T-twisYI7^5x>(KIS-L5D%bF;-R{yG55MFfI%#*mzQ@<$=zL z$|+)#);eb@$tF1jXn6c;o%APE)>(ofhJdn4uh!+4v7@&hWV|OxFIkedO<(J0t#nkvbbJ zEGwC`Jjl^(_R-&Ob49#W_uRb2wI}xl&yAHK3=B7O#2DBd85abuUmT&emeD{-A>+@} z)mnVZKkzsd1Fg52TYPWktA2TLhM4QTdvo6$Z*bfD&-R_!d*S~_g-!VFmGi4Utgp7+ zvCdke=6dPI-MghTelfnA$ha?ee&Xw+K`#q$m?ihuhsy3To6C50@!|u`wbC;UL^n?N z_P%%I%9XkEXWOx{^wvL?FvzuHUL@q@aB0UKeUmhv`K+$0SNAigh0S@i;y<59DVs`T zNV&N@Q%0GJhx!YovpY24}nD12u`96;8UNcP=uef?}{inDq*Dm*8Ynya2QUDT7 zsMa~S6z#OsKlQCsFyZ#L^xI`i=Jrb2&;Puq=3F#$L+j5-euk*EWlOA|x<7iKm2>;s zzdKL+^0r%7?D=_Tla7r1^@~|sH9wxW3azMU;i;=R@!Y27XO&#$gW4C81=r6-Ph(|x zQMOxI*8XE~z}m3WcfRF*FTW$-5!fw%U1Z9P)eHaqiC=v+$@A~;BlfRD7!&s1JjJ88 zMgPqCs=#H7k8l{cE(}<3Y`MSV<37y92J=O-vcjv>99&Cp7E+>6^ZaC-j3CLMj)r>nU+44C;DJYD@<);T3K F0RRL}*CYS{ literal 0 HcmV?d00001 diff --git a/icons/modules/projectiles/attachments/harness.dmi b/icons/modules/projectiles/attachments/harness.dmi new file mode 100644 index 0000000000000000000000000000000000000000..6dc6b896428e089fb9d8efe5b3530792aac1ed0e GIT binary patch literal 545 zcmV++0^a?JP)V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex7wuvIWN;^NFm z%}mcIfpCgT5=&AQY!#Gp6VvljOEQy{xHwZXi;5L&6%4sJ(~1&vQz2{&g}UjbdAJlS zE4cc(D7g4~asdF_Kq#<+;a{19+aNq?9UaPD-gf&&!&vidYiVrg4hkVqbR1kXo3zut?sO1EQLg)2I={oN>#9n%1ORv# zKBms@9qs}EdSU?I_pPN3-#FA8HME27hqZd6hSA+U0N~(gAEzg0+WTe(m~Xs2+qs-3 z$>vs}@T7B{+0WQ< z6JnCgTM5x}7>Z87EM2yf_gR$4e@%7Oi){U&ee0u?5(e0Biv^CIDN2jS0XO jU}FNX1=yGXYymbe1BZG4 literal 0 HcmV?d00001 diff --git a/icons/screen/actions/backgrounds.dmi b/icons/screen/actions/backgrounds.dmi index 29147d498b5a9d1b3c191acc5e415e0c305eec00..fb9ca28a15998bdcfd1201bfc30cda855ec0fa44 100644 GIT binary patch delta 1709 zcmV;e22%Ok3$P847k^*~1^@s6;+S_h0001&dQ@0+L}hbha%pgMX>V=-0C=2@&A|$T zFc1XLbM`A1JzHwqYc548Ip{wGvo;HfO0u=T--EY8NpCaon5psJKg@{TU|^?|d0kNB z?Q{n9W+xssUN4|L@zcvuvREP1SUH;QD@X;(amuK%1vMud>3_)Iu348NjU1_n@ zvuj9Zxrv>Uty`cJ(w41+sW}Wn=e9d^WTzcWT1p`$1(s_P$ihWcp<2!?MbN zs&-~hyevomuivrp=Q?iR{#=TFK0k&Rd~e7qCqxKmGJkz_)_JSECPE&WzE_y%^Z79$ z`d-6b11zhYNG6j=r+b;j@v^hC<2zOluyj-^8wtxQCt6Lg%4>KrnZ%U`7x(>I|AmFo zMOfvvfC>5h7)D1&aR2^Sd%nigr<=ax?(Uv_>tDO}F7o*?*92bzVzFa&+qMG*jtW&( z5sMwG1Aky9ZD?i?)%7ZV-Tj)2{%PeZhGqs)npk9x4-8yDp-{lqmd!;!mC`UUZ~;Kk zJ?Aq((==|ycTiOoo0}GMEO|1C#mtwjZ!0n$;Tv4E-U}Ts6bgaa9W~wEJ(!-Jc8`6Z zl;`YsXt!Kd*Q@AS+vciYRjQ$5JZHZ{Dy8|34}V<(Y8gJW-_@{^p<|Q)N&qE*5lUaEjk90dFAJ?2L$>b{!Ew!vG-I_Y)OP|4?WGo;>*_5!RS>5L4RC@=9eRp(qM- zY=2>N5koVB7+illFm{k$q0`D$=J_hB_~C;`q0dtk1yfT~%&~(cqji>h@fIHde7&Kf zTN5p-3;>-TeG&4FuDI=OSXLPu8-KW6YNA^c1HGYPJ)5Ek>)8~Aj3!-$+SBE>r#BLy z4|mFV0|0R2*3I2`7vD?IcG`1y?6>?I-k zOOGG=PdLSI7R6U5m}vQ9#yQWJ$$HMes-q32A?!P}kOT+&0C+s|X#zkC>#}U)g zq2oQrd%5VgvibK0^So31{{DXV+}73>Y&*JN$EIoemMH%(;d5sKvpZ_kSPYv>26HT| zFuc)q-CcH^;@9hS)a&&~i_g;Z?|%hxoc#`JJRUkGI{^;7i|>UJz(7>-0ZIUX=5o-y za*2A)55v)<1hgP(<~vXVBtr=R|7-ttnv?)I#Spt6e|H%@58QRpO1i-%?Eehs#^#3&h-px4Cnv?*^Pyz&^X@6Q^x0MnA zF`IFkrXiI|xdt#d<0t`q&@>H+1iS5)62KX!`1>+IJ9h>^u%c6G2%J{I zW?@7&=Qi#S_~;WU{qgf3zOUT4byJGA!`$6_%H-svl=m<@JF6&$VaU*S$!4=s^<^XW zmJ}j@?>k_8B`f4sr8}pLE)hCM0oQ=>m26ErFGh5HIa?zW8Zd9pvVWKPh>0@zC=oEg zDSp&5bbKXSvzN()R+wMHJeCJqa@f#SkYvaJ$&dk(Ap;~s21teskPI0h88SdJWPoJI z0LhR6k|6^mLk38O43G>NAQ>`1GGu^c$N1yP~I;Tmw~lqS(NzD%u&qVXkw3L_r63X;q}n63VS7CuNJ00000NkvXXu0mjf Dz5FQG delta 1494 zcmV;{1u6Ql4ciNl7k@wq1^@s6V=-0C=2@&Akf3 zFcih{Irk|J?X38?Vhcl~aXi^)C=DqmDMRB(YDzZpwtv6fZkekzO5`faD~C{3 zHVFT5$aimlsTTd%p&JZ+0L=eU#Hdjl+yDRt21!IgRCt{2n?YzCRUF2@H*vQMi96e5 zCnZ`9NFi-e2u^blLQPu_m3Z>7q@@(nLk`}S2p+RJ3JE=_;GseZw&dW+1rKJshJb<$ z^kfyS5EEV9*?-jRlEc!5#KZ0zXLjCf5}Eg2!2fsJc`v))?*IROv$HSXuyVPKT<-b~ zG-!GG^F9E`<*x4o01QK||NSKf0231v`$~=5DwoTQ4GzArV_8Ew{NcJ4z%Y8PQ!ncA z|Lb>D|J=mw+n;Ii&*!J{T;L74m6Qr&%NlBm#@)(>3V(Z8LoW%(^Z98d{{Du04RGB` zDxFRvn;jGu$FIG;y}+??gi}YYR!zBXCDqvkx3Yof(`j6}fAP?-^_YYuxe%?C{d{UXKpR3#H zs{xPEzklc9rX%Nw03v`0AOeU0B0#2y03v`0 zAOa#p1P}p401-e05CJkp1P}p4fL}y_PecF_Km-s0L;w*WQ$)aVCjbcNe&P}@em4SM zN3NZWh+o|j5kRW}Ksfgkmw0|Cl7PpLe@;aOvyRe~ZmYajtEL!Z!nu{HRg5i+Vs!JZ z(0|-fvO;IrRpEF84SfIp!^r0`#xOTGC!9M98J(-#FMsg?Adn4J9ZhuI3IO!_{8iYu zQgOSxaNP>3)jzzHn(Am`C>t7;*;J*d%%&>o2CJPcYjMoB!Bu2o4@|1#sBmxFXPlnt94*I-nhRZ z{>P5vz;PVdm=V67gHK`GcIY{x$8cJI6TCm5RD=peHGKTsB_;lAj~)aEc=6js3DgM= zI{sKN=9_aw&-r)W^Rpemv3>Ukl!{2d_A0V(j34*`_MSZp92<$mfzRQpeY*y?RDTRL zK~?d&SEvbI@uP|tk8Im+d9|A!yg$IqqW;5=*n9d^in)O7ZQ~*56g( zI4}P2@US#cib`(32d#Eg>zAbkuBS{z0(daezVy` zv)PQb_#)5XUXaAq&t)W&k#o8U;D3@`e7}eQfm9VAAOZliPY3NQm$=XQQR1FNK!;SV zd@dqDvxor5zxMCuNd$lwL+ySL_ap)U*qG_EeIVD92+%AdK(mMd%_0IciwMvxA^@%b zWGTN4ZF>>{2yREKl5jiv|4M*=GfuT95ujN_fI^w38QN_n0-$Cy&NNMAGJhGb0m98V zB0!Ez(?lvIj@=RglEsUEs0Ntl&jJYNe&Q0(4;?oGWNk2~H&y#`p9C*hWgk*hN1#D8P09pAMoMFTJrJB wAH2tI+`6g7n``OLT{b&AtK~f`E-tcv0RDQ?(Dt}r<^TWy07*qoM6N<$f~VKZr2qf` diff --git a/icons/screen/radial/actions.dmi b/icons/screen/radial/actions.dmi new file mode 100644 index 0000000000000000000000000000000000000000..4b3d86a03ae6d8ca0da5f164a1165517a2595765 GIT binary patch literal 368 zcmV-$0gwKPP)YDR4**08B~Dv=;}rk^0F_BZ zK~yM_g_6MyfFKM-s|gqHF3~9*!Gi-i3h`t*r9#_Zgo7N^ytI!Gm3FEtk*`BvE5Q2U zrFJXNsdUbSPsPg_#kG-hNGg>Z5><#mq6!g6R3QS1D)@4VDsIj$QAOs=Cb9UI