From 6ebfbccebbe174f23dc669b4381e6e210d831f02 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:31:29 -0600 Subject: [PATCH] Refactors `unique_reskin`, deletes retool kit (#93775) ## About The Pull Request Closes #93635 `unique_reskin` is no longer a list on `/item`, now `/datum/atom_skin` The actual reskinning behavior has been moved out to `/datum/component/reskinable_item` PKC reskinning is now handled via alt-click reskin, rather than via the retooling kit. The retooling kit has been removed. There's no limit on how many times you can reskin your PKC (though perhaps we limit it to one reskin and keep the retooling kit as a way to allow a miner to reskin it a second time?) The Ashen Skull unique reskin is still a trophy, and instead unlocks its unique reskin option in the alt-click radial. ## Why It's Good For The Game I'm unsure why the retooling kit exists on its own, when it's relatively cheap and just performs the behavior of alt-click reskinning. So to keep it consistent with all other forms of reskinning I've just made it baseline. To accomplish that I refactored reskinning. The new form of reskinning allows for greater potential in adding reskins, allowing far more than just an icon state change. Also we can put it on turfs and mobs and structures now which is cool I guess There's also the added benefit of being able to see an item's reskins without needing to instantiate it, which the loadout menu uses to great effect. ## Changelog :cl: Melbert refactor: Refactored item reskinning (the alt-click way), report any oddities with that del: Deleted the crusher retool kit, now you can just reskin your crusher with alt-click. The Skull skin is still locked behind having the Ashen Skull trophy applied. fix: Stunswords no longer have an incorrect lore blurb fix: Fixed loadout item reskinning's UI /:cl: --- code/__DEFINES/obj_flags.dm | 8 +- code/__DEFINES/preferences.dm | 2 - code/__DEFINES/reskin.dm | 18 ++ code/__HELPERS/global_lists.dm | 18 +- code/_globalvars/bitfields.dm | 1 - code/datums/components/dart_insert.dm | 4 +- code/datums/components/reskinnable_atom.dm | 202 +++++++++++++ .../orders/order_items/mining/order_mining.dm | 4 - code/game/machinery/dna_scanner.dm | 47 ++- code/game/objects/items.dm | 27 -- code/game/objects/items/melee/baton.dm | 26 +- code/game/objects/items_reskin.dm | 95 ------ code/modules/clothing/masks/_masks.dm | 1 - code/modules/clothing/masks/costume.dm | 38 ++- code/modules/clothing/under/_under.dm | 7 - .../under/accessories/_accessories.dm | 7 - .../clothing/under/accessories/badges.dm | 57 ++-- code/modules/clothing/under/costume.dm | 30 +- .../modules/loadout/categories/accessories.dm | 2 +- code/modules/loadout/loadout_items.dm | 45 ++- .../kinetic_crusher/kinetic_crusher.dm | 28 +- .../kinetic_crusher/trophies_misc.dm | 276 ++++++++---------- code/modules/paperwork/clipboard.dm | 25 +- code/modules/paperwork/pen.dm | 70 +++-- code/modules/projectiles/guns/ballistic.dm | 1 - .../projectiles/guns/ballistic/revolver.dm | 59 +++- .../projectiles/guns/ballistic/shotgun.dm | 40 ++- .../reagents/reagent_containers/medigel.dm | 44 ++- .../reagents/reagent_containers/spray.dm | 33 ++- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/reskin_validation.dm | 15 + tgstation.dme | 3 +- .../loadout/ModifyPanel.tsx | 9 +- .../CharacterPreferences/loadout/base.ts | 3 +- 34 files changed, 757 insertions(+), 489 deletions(-) create mode 100644 code/__DEFINES/reskin.dm create mode 100644 code/datums/components/reskinnable_atom.dm delete mode 100644 code/game/objects/items_reskin.dm create mode 100644 code/modules/unit_tests/reskin_validation.dm diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm index 7679d482bff..b01406bf631 100644 --- a/code/__DEFINES/obj_flags.dm +++ b/code/__DEFINES/obj_flags.dm @@ -24,14 +24,12 @@ #define BLOCKS_CONSTRUCTION_DIR (1<<10) /// Can we ignore density when building on this object (for example, directional windows and grilles) #define IGNORE_DENSITY (1<<11) -/// We can reskin this item infinitely -#define INFINITE_RESKIN (1<<12) /// Can this object conduct electricity -#define CONDUCTS_ELECTRICITY (1<<13) +#define CONDUCTS_ELECTRICITY (1<<12) /// Atoms don't spawn anything when deconstructed (they just vanish) -#define NO_DEBRIS_AFTER_DECONSTRUCTION (1<<14) +#define NO_DEBRIS_AFTER_DECONSTRUCTION (1<<13) /// Flag which tells an object to hang onto an support atom on late initialize. Usefull only during mapload and supported by some atoms only -#define MOUNT_ON_LATE_INITIALIZE (1<<15) +#define MOUNT_ON_LATE_INITIALIZE (1<<14) // If you add new ones, be sure to add them to /obj/Initialize as well for complete mapping support diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 42e4d3869d2..1ae8403c644 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -164,8 +164,6 @@ #define LOADOUT_FLAG_GREYSCALING_ALLOWED (1<<2) /// Allows the item to be renamed by the player. #define LOADOUT_FLAG_ALLOW_NAMING (1<<3) -/// Allows the item to be reskinned by the player. Only applicable to items with unique_reskin defined. -#define LOADOUT_FLAG_ALLOW_RESKIN (1<<4) // Loadout item info keys // Changing these will break existing loadouts diff --git a/code/__DEFINES/reskin.dm b/code/__DEFINES/reskin.dm new file mode 100644 index 00000000000..250b3959fe2 --- /dev/null +++ b/code/__DEFINES/reskin.dm @@ -0,0 +1,18 @@ +/// Global list of available atom skins +GLOBAL_LIST_INIT_TYPED(atom_skins, /datum/atom_skin, init_subtypes_w_path_keys(/datum/atom_skin)) + +/// Sets the atom's varname to newvalue if newvalue is not null, otherwise resets it to its initial value if resetcondition is true +#define APPLY_VAR_OR_RESET_INITIAL(atom, varname, newvalue, resetcondition) \ + if(newvalue) {atom.##varname = (##newvalue) } else if(resetcondition) { atom.##varname = initial(atom.##varname) } + +/// Sets the atom's varname to newvalue if newvalue is not null, otherwise sets it to resetvalue if resetcondition is true +#define APPLY_VAR_OR_RESET_TO(atom, varname, newvalue, resetcondition, resetvalue) \ + if(newvalue) {atom.##varname = (##newvalue) } else if(resetcondition) { atom.##varname = (resetvalue) } + +/// Resets the atom's varname to its initial value if oldvalue is not null +#define RESET_INITIAL_IF_SET(atom, varname, oldvalue) \ + if(oldvalue) { atom.##varname = initial(atom.##varname) } + +/// Sets the atom's varname to resetvalue if oldvalue is not null +#define RESET_TO_IF_SET(atom, varname, oldvalue, resetvalue) \ + if(oldvalue) { atom.##varname = (resetvalue) } diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index 4f1f2ca0d3d..d5638e0312b 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -137,29 +137,21 @@ for(var/atom/req_atom as anything in recipe.structures) atom_list |= req_atom -//creates every subtype of prototype (excluding prototype) and adds it to list L. -//if no list/L is provided, one is created. +/// Creates every subtype of prototype (excluding prototype and abstract types) and adds it to list L. +/// If no list/L is provided, one is created. /proc/init_subtypes(prototype, list/L) if(!istype(L)) L = list() - for(var/path in subtypesof(prototype)) + for(var/path in valid_subtypesof(prototype)) L += new path() return L -//returns a list of paths to every subtype of prototype (excluding prototype) -//if no list/L is provided, one is created. -/proc/init_paths(prototype, list/L) - if(!istype(L)) - L = list() - for(var/path in subtypesof(prototype)) - L+= path - return L - /// Functions like init_subtypes, but uses the subtype's path as a key for easy access +/// If no list/L is provided, one is created. /proc/init_subtypes_w_path_keys(prototype, list/L) if(!istype(L)) L = list() - for(var/path in subtypesof(prototype)) + for(var/path in valid_subtypesof(prototype)) L[path] = new path() return L diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index 400b0609122..6be87df0909 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -361,7 +361,6 @@ DEFINE_BITFIELD(obj_flags, list( "BLOCKS_CONSTRUCTION" = BLOCKS_CONSTRUCTION, "BLOCKS_CONSTRUCTION_DIR" = BLOCKS_CONSTRUCTION_DIR, "IGNORE_DENSITY" = IGNORE_DENSITY, - "INFINITE_RESKIN" = INFINITE_RESKIN, "CONDUCTS_ELECTRICITY" = CONDUCTS_ELECTRICITY, "NO_DEBRIS_AFTER_DECONSTRUCTION" = NO_DEBRIS_AFTER_DECONSTRUCTION, )) diff --git a/code/datums/components/dart_insert.dm b/code/datums/components/dart_insert.dm index 9770de669b5..f2816949c9d 100644 --- a/code/datums/components/dart_insert.dm +++ b/code/datums/components/dart_insert.dm @@ -60,9 +60,9 @@ add_to_dart(dart, user) return COMPONENT_CANCEL_ATTACK_CHAIN -/datum/component/dart_insert/proc/on_reskin(datum/source, mob/user, skin) +/datum/component/dart_insert/proc/on_reskin(datum/source, skin) SIGNAL_HANDLER - SEND_SIGNAL(parent, COMSIG_DART_INSERT_PARENT_RESKINNED) + SEND_SIGNAL(parent, COMSIG_DART_INSERT_PARENT_RESKINNED, skin) /datum/component/dart_insert/proc/add_to_dart(obj/item/ammo_casing/dart, mob/user) var/obj/projectile/dart_projectile = dart.loaded_projectile diff --git a/code/datums/components/reskinnable_atom.dm b/code/datums/components/reskinnable_atom.dm new file mode 100644 index 00000000000..a14563bd630 --- /dev/null +++ b/code/datums/components/reskinnable_atom.dm @@ -0,0 +1,202 @@ +/** + * ### Atom skin singleton datum + * + * Simple datum which holds information about a skin that can be applied to an atom. + */ +/datum/atom_skin + abstract_type = /datum/atom_skin + + /// Required, name shown in the radial menu + var/preview_name + + /// If true, changing the reskin also changes the base_icon_state of the atom + var/change_base_icon_state = FALSE + /// If true, changing the reskin also changes the inhand_icon_state of the atom + var/change_inhand_icon_state = FALSE + /// If true, unset vars are reset to their original values when applying this skin + var/reset_missing = TRUE + + /// Optional, name to change the atom to when applied + var/new_name + /// Optional, description to change the atom to when applied + var/new_desc + /// Optional, icon to change the atom to when applied + var/new_icon + /// Optional, icon_state to change the atom to when applied + var/new_icon_state + +/** + * Applies all relevant skin changes to the given atom + * Can be overridden to add additional behavior, such as registering signals or altering other vars. + * + * * apply_to: The atom to apply the skin to + */ +/datum/atom_skin/proc/apply(atom/apply_to) + SHOULD_CALL_PARENT(TRUE) + APPLY_VAR_OR_RESET_INITIAL(apply_to, name, new_name, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, desc, new_desc, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, icon, new_icon, reset_missing) + APPLY_VAR_OR_RESET_TO(apply_to, icon_state, new_icon_state, reset_missing, initial(apply_to.post_init_icon_state) || initial(apply_to.icon_state)) + if(change_base_icon_state) + APPLY_VAR_OR_RESET_INITIAL(apply_to, base_icon_state, new_icon_state, reset_missing) + if(change_inhand_icon_state && isitem(apply_to)) + var/obj/item/item_apply_to = apply_to + APPLY_VAR_OR_RESET_INITIAL(item_apply_to, inhand_icon_state, new_icon_state, reset_missing) + +/** + * Resets all changes this skin would have made to the given atom + * Does not verify that the skin was actually applied to the atom beforehand. + * Can be overridden to add additional behavior, such as unregistering signals or altering other vars. + * + * * clear_from: The atom to clear the skin from + */ +/datum/atom_skin/proc/clear_skin(atom/clear_from) + SHOULD_CALL_PARENT(TRUE) + RESET_INITIAL_IF_SET(clear_from, name, new_name) + RESET_INITIAL_IF_SET(clear_from, desc, new_desc) + RESET_INITIAL_IF_SET(clear_from, icon, new_icon) + RESET_TO_IF_SET(clear_from, icon_state, new_icon_state, initial(clear_from.post_init_icon_state) || initial(clear_from.icon_state)) + if(change_base_icon_state) + RESET_INITIAL_IF_SET(clear_from, base_icon_state, new_icon_state) + if(change_inhand_icon_state && isitem(clear_from)) + var/obj/item/item_clear_from = clear_from + RESET_INITIAL_IF_SET(item_clear_from, inhand_icon_state, new_icon_state) + +/** + * ### Reskinnable atoms + * + * Simple component which lets an atom be alt-clicked to open a radial menu to choose a new skin to apply. + */ +/datum/component/reskinable_item + dupe_mode = COMPONENT_DUPE_SELECTIVE + /// Base reskin type to pull options from - all subtypes except those blacklisted are valid options + VAR_PRIVATE/base_reskin_type + /// If TRUE, the reskin option is infinite-use. If FALSE, the component is deleted on use (so you're stuck with that skin). + VAR_PRIVATE/infinite_reskin = FALSE + /// List of subtypes of /datum/atom_skin that are not allowed to be used for this item + VAR_PRIVATE/list/blacklisted_subtypes + + /// Currently applied skin preview_name + VAR_PRIVATE/current_skin + +/datum/component/reskinable_item/Initialize(base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes = list()) + if(!isatom(parent) || isarea(parent)) + return COMPONENT_INCOMPATIBLE + + src.base_reskin_type = base_reskin_type + src.infinite_reskin = infinite + src.blacklisted_subtypes = blacklisted_subtypes + + if(initial_skin) + set_skin_by_name(initial_skin) + + var/atom/atom_parent = parent + atom_parent.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1 + +/datum/component/reskinable_item/RegisterWithParent() + RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin)) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(add_tags)) + RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(add_context)) + +/datum/component/reskinable_item/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_CLICK_ALT) + UnregisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS) + UnregisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM) + +/datum/component/reskinable_item/CheckDupeComponent(datum/component/comp, base_reskin_type, infinite = FALSE, initial_skin, list/blacklisted_subtypes = list()) + if(src.base_reskin_type != base_reskin_type) + return FALSE // new comp - though the alt-click behavior will collide + + src.infinite_reskin = infinite + src.blacklisted_subtypes = blacklisted_subtypes + + set_skin_by_name(initial_skin) + return TRUE // same comp + +/datum/component/reskinable_item/proc/get_skins_by_name() + var/list/reskin_options = list() + for(var/datum/atom_skin/reskin_option as anything in valid_subtypesof(base_reskin_type) - blacklisted_subtypes) + reskin_options[reskin_option::preview_name] = reskin_option + + return reskin_options + +/datum/component/reskinable_item/proc/set_skin_by_name(input_name) + var/list/reskin_options = get_skins_by_name() + if(current_skin) + var/datum/atom_skin/previous_skin = GLOB.atom_skins[reskin_options[current_skin]] + previous_skin.clear_skin(parent) + + if(input_name) + var/datum/atom_skin/reskin_to_apply = GLOB.atom_skins[reskin_options[input_name]] + reskin_to_apply.apply(parent) + + current_skin = input_name + + var/atom/atom_parent = parent + atom_parent.update_appearance() + + if(isitem(parent)) + var/obj/item/item_parent = parent + item_parent.update_slot_icon() + + SEND_SIGNAL(parent, COMSIG_OBJ_RESKIN, input_name) + +/datum/component/reskinable_item/proc/add_context(atom/source, list/context, obj/item/held_item, mob/user) + SIGNAL_HANDLER + + context[SCREENTIP_CONTEXT_ALT_LMB] = "Reskin" + return CONTEXTUAL_SCREENTIP_SET + +/datum/component/reskinable_item/proc/add_tags(atom/source, mob/user, list/tags) + SIGNAL_HANDLER + + tags["reskinnable"] = "This item is able to be reskinned! Alt-Click to do so!" + +/// Called when alt clicked and the item has unique reskin options +/datum/component/reskinable_item/proc/on_click_alt_reskin(datum/source, mob/user) + SIGNAL_HANDLER + + if(!user.can_perform_action(parent, NEED_DEXTERITY)) + return NONE + + INVOKE_ASYNC(src, PROC_REF(reskin_obj), user) + return CLICK_ACTION_SUCCESS + +/** + * Reskins object based on a user's choice + * + * Arguments: + * * user The mob choosing a reskin option + */ +/datum/component/reskinable_item/proc/reskin_obj(mob/user) + var/atom/atom_parent = parent + + var/list/items = list() + for(var/reskin_name, reskin_typepath in get_skins_by_name()) + var/datum/atom_skin/reskin = GLOB.atom_skins[reskin_typepath] + items[reskin_name] = image(icon = reskin.new_icon || atom_parent.icon, icon_state = reskin.new_icon_state || atom_parent.icon_state) + + sort_list(items) + + var/pick = show_radial_menu(user, parent, items, custom_check = CALLBACK(src, PROC_REF(check_reskin_menu), user), radius = 38, require_near = TRUE) + if(!pick || !items[pick]) + return + + set_skin_by_name(pick) + to_chat(user, span_info("[parent] is now skinned as '[pick].'")) + + if(!infinite_reskin) + qdel(src) + +/** + * Checks if we are allowed to interact with a radial menu for reskins + * + * Arguments: + * * user The mob interacting with the menu + */ +/datum/component/reskinable_item/proc/check_reskin_menu(mob/user) + if(QDELETED(parent)) + return FALSE + if(user.incapacitated) + return FALSE + return TRUE diff --git a/code/game/machinery/computer/orders/order_items/mining/order_mining.dm b/code/game/machinery/computer/orders/order_items/mining/order_mining.dm index 41c52d9b0a0..f9247d039ce 100644 --- a/code/game/machinery/computer/orders/order_items/mining/order_mining.dm +++ b/code/game/machinery/computer/orders/order_items/mining/order_mining.dm @@ -38,10 +38,6 @@ purchase_path = /obj/item/clothing/gloves/fingerless/punch_mitts cost_per_order = 1000 -/datum/orderable_item/mining/crusher_retool_kit - purchase_path = /obj/item/crusher_trophy/retool_kit - cost_per_order = 150 - /datum/orderable_item/mining/resonator purchase_path = /obj/item/resonator cost_per_order = 710 diff --git a/code/game/machinery/dna_scanner.dm b/code/game/machinery/dna_scanner.dm index ab14767479f..1ad319486c8 100644 --- a/code/game/machinery/dna_scanner.dm +++ b/code/game/machinery/dna_scanner.dm @@ -159,6 +159,41 @@ SIGNAL_HANDLER set_linked_console(null) +// Disk skins +/datum/atom_skin/dna_disk + abstract_type = /datum/atom_skin/dna_disk + +/datum/atom_skin/dna_disk/red + preview_name = "Red" + new_icon_state = "datadisk0" + +/datum/atom_skin/dna_disk/dark_blue + preview_name = "Dark Blue" + new_icon_state = "datadisk1" + +/datum/atom_skin/dna_disk/yellow + preview_name = "Yellow" + new_icon_state = "datadisk2" + +/datum/atom_skin/dna_disk/black + preview_name = "Black" + new_icon_state = "datadisk3" + +/datum/atom_skin/dna_disk/green + preview_name = "Green" + new_icon_state = "datadisk4" + +/datum/atom_skin/dna_disk/purple + preview_name = "Purple" + new_icon_state = "datadisk5" + +/datum/atom_skin/dna_disk/grey + preview_name = "Grey" + new_icon_state = "datadisk6" + +/datum/atom_skin/dna_disk/light_blue + preview_name = "Light Blue" + new_icon_state = "datadisk7" //Just for transferring between genetics machines. /obj/item/disk/data @@ -168,20 +203,10 @@ var/list/mutations = list() var/max_mutations = 6 var/read_only = FALSE //Well,it's still a floppy disk - obj_flags = parent_type::obj_flags | INFINITE_RESKIN - unique_reskin = list( - "Red" = "datadisk0", - "Dark Blue" = "datadisk1", - "Yellow" = "datadisk2", - "Black" = "datadisk3", - "Green" = "datadisk4", - "Purple" = "datadisk5", - "Grey" = "datadisk6", - "Light Blue" = "datadisk7", - ) /obj/item/disk/data/Initialize(mapload) . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/dna_disk, infinite = TRUE) icon_state = "datadisk[rand(0,7)]" add_overlay("datadisk_gene") if(length(genetic_makeup_buffer)) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 508b1b67880..ac2928f5ac1 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -240,14 +240,6 @@ /// A lazylist used for applying fantasy values, contains the actual modification applied to a variable. var/list/fantasy_modifications = null - /// Has the item been reskinned? - var/current_skin - /// List of options to reskin. - var/list/unique_reskin - /// If reskins change base icon state as well - var/unique_reskin_changes_base_icon_state = FALSE - /// If reskins change inhands as well - var/unique_reskin_changes_inhand = FALSE /// Do we apply a click cooldown when resisting this object if it is restraining them? var/resist_cooldown = CLICK_CD_BREAKOUT @@ -283,9 +275,6 @@ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_ITEM, src) - setup_reskinning() - - /obj/item/Destroy(force) // This var exists as a weird proxy "owner" ref // It's used in a few places. Stop using it, and optimially replace all uses please @@ -300,19 +289,6 @@ return ..() - -/obj/item/add_context(atom/source, list/context, obj/item/held_item, mob/user) - . = ..() - - if(!unique_reskin) - return - - if(current_skin && !(obj_flags & INFINITE_RESKIN)) - return - - context[SCREENTIP_CONTEXT_ALT_LMB] = "Reskin" - return CONTEXTUAL_SCREENTIP_SET - /obj/item/click_ctrl(mob/user) SHOULD_NOT_OVERRIDE(TRUE) @@ -458,9 +434,6 @@ else if (siemens_coefficient <= 0.5) .["partially insulated"] = "It is made from a poor insulator that will dampen (but not fully block) electric shocks passing through it." - if(LAZYLEN(unique_reskin) && !current_skin) - .["reskinnable"] = "This item is able to be reskinned! Alt-Click to do so!" - /obj/item/examine_descriptor(mob/user) return "item" diff --git a/code/game/objects/items/melee/baton.dm b/code/game/objects/items/melee/baton.dm index ba75f0b7a3e..b7aa2f91eee 100644 --- a/code/game/objects/items/melee/baton.dm +++ b/code/game/objects/items/melee/baton.dm @@ -764,6 +764,20 @@ /obj/item/melee/baton/security/loaded/hos preload_cell_type = /obj/item/stock_parts/power_store/cell/super +// Stunsword Skins +/datum/atom_skin/stunsword + abstract_type = /datum/atom_skin/stunsword + change_inhand_icon_state = TRUE + change_base_icon_state = TRUE + +/datum/atom_skin/stunsword/default + preview_name = "Default" + new_icon_state = "stunsword" + +/datum/atom_skin/stunsword/energy + preview_name = "Energy" + new_icon_state = "stunsword_energy" + ///Stun Sword /obj/item/melee/baton/security/stunsword name = "\improper NT-20 'Excalibur' Stunsword" @@ -789,14 +803,12 @@ convertible = FALSE obj_flags = UNIQUE_RENAME - unique_reskin = list( - "Default" = "stunsword", - "Energy Stunsword" = "stunsword_energy", - ) - unique_reskin_changes_inhand = TRUE - unique_reskin_changes_base_icon_state = TRUE -/obj/item/melee/baton/security/stunsword/loaded/add_deep_lore() +/obj/item/melee/baton/security/stunsword/Initialize(mapload) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/stunsword) + +/obj/item/melee/baton/security/stunsword/add_deep_lore() return /obj/item/melee/baton/security/stunsword/loaded diff --git a/code/game/objects/items_reskin.dm b/code/game/objects/items_reskin.dm deleted file mode 100644 index 98a3165b5e2..00000000000 --- a/code/game/objects/items_reskin.dm +++ /dev/null @@ -1,95 +0,0 @@ -/// Called when alt clicked and the item has unique reskin options -/obj/item/proc/on_click_alt_reskin(datum/source, mob/user) - SIGNAL_HANDLER - - if(!user.can_perform_action(src, NEED_DEXTERITY)) - return NONE - - if(!(obj_flags & INFINITE_RESKIN) && current_skin) - return NONE - - INVOKE_ASYNC(src, PROC_REF(reskin_obj), user) - return CLICK_ACTION_SUCCESS - -/** - * Checks if we should set up reskinning, - * by default if unique_reskin is set. - * - * Called on setup_reskinning(). - * Inheritors should override this to add their own checks. - */ -/obj/item/proc/check_setup_reskinning() - SHOULD_CALL_PARENT(TRUE) - if(unique_reskin) - return TRUE - - return FALSE - -/** - * Registers signals and context for reskinning, - * if check_setup_reskinning() passes. - * - * Called on Initialize(...). - * Inheritors should override this to add their own setup steps, - * or to avoid double calling register_context(). - */ -/obj/item/proc/setup_reskinning() - SHOULD_CALL_PARENT(FALSE) - if(!check_setup_reskinning()) - return - - RegisterSignal(src, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin)) - register_context() - -/** - * Reskins object based on a user's choice - * - * Arguments: - * * user The mob choosing a reskin option - */ -/obj/item/proc/reskin_obj(mob/user) - if(!LAZYLEN(unique_reskin)) - return - - var/list/items = list() - for(var/reskin_option in unique_reskin) - var/image/item_image = image(icon = src.icon, icon_state = unique_reskin[reskin_option]) - items += list("[reskin_option]" = item_image) - sort_list(items) - - var/pick = show_radial_menu(user, src, items, custom_check = CALLBACK(src, PROC_REF(check_reskin_menu), user), radius = 38, require_near = TRUE) - if(!pick) - return - if(!unique_reskin[pick]) - return - current_skin = pick - icon_state = unique_reskin[pick] - - if (unique_reskin_changes_base_icon_state) - base_icon_state = icon_state - - if (unique_reskin_changes_inhand) - inhand_icon_state = icon_state - - update_appearance() - - to_chat(user, "[src] is now skinned as '[pick].'") - SEND_SIGNAL(src, COMSIG_OBJ_RESKIN, user, pick) - - -/** - * Checks if we are allowed to interact with a radial menu for reskins - * - * Arguments: - * * user The mob interacting with the menu - */ -/obj/item/proc/check_reskin_menu(mob/user) - if(QDELETED(src)) - return FALSE - if(!(obj_flags & INFINITE_RESKIN) && current_skin) - return FALSE - if(!istype(user)) - return FALSE - if(user.incapacitated) - return FALSE - return TRUE diff --git a/code/modules/clothing/masks/_masks.dm b/code/modules/clothing/masks/_masks.dm index 8682dc4a927..672b2eeef1c 100644 --- a/code/modules/clothing/masks/_masks.dm +++ b/code/modules/clothing/masks/_masks.dm @@ -9,7 +9,6 @@ strip_delay = 4 SECONDS equip_delay_other = 4 SECONDS visor_vars_to_toggle = NONE - unique_reskin_changes_base_icon_state = TRUE var/adjusted_flags = null ///Did we install a filtering cloth? diff --git a/code/modules/clothing/masks/costume.dm b/code/modules/clothing/masks/costume.dm index 8ddfcf5520f..253380adc49 100644 --- a/code/modules/clothing/masks/costume.dm +++ b/code/modules/clothing/masks/costume.dm @@ -1,3 +1,28 @@ +// Mask skins +/datum/atom_skin/joy_mask + abstract_type = /datum/atom_skin/joy_mask + change_base_icon_state = TRUE + +/datum/atom_skin/joy_mask/joy + preview_name = "Joy" + new_icon_state = "joy" + +/datum/atom_skin/joy_mask/flushed + preview_name = "Flushed" + new_icon_state = "flushed" + +/datum/atom_skin/joy_mask/pensive + preview_name = "Pensive" + new_icon_state = "pensive" + +/datum/atom_skin/joy_mask/angry + preview_name = "Angry" + new_icon_state = "angry" + +/datum/atom_skin/joy_mask/pleading + preview_name = "Pleading" + new_icon_state = "pleading" + /obj/item/clothing/mask/joy name = "emotion mask" desc = "Express your happiness or hide your sorrows with this cultured cutout." @@ -5,19 +30,10 @@ base_icon_state = "joy" clothing_flags = MASKINTERNALS flags_inv = HIDESNOUT - obj_flags = parent_type::obj_flags | INFINITE_RESKIN - unique_reskin = list( - "Joy" = "joy", - "Flushed" = "flushed", - "Pensive" = "pensive", - "Angry" = "angry", - "Pleading" = "pleading" - ) - -/obj/item/clothing/mask/joy/reskin_obj(mob/user) +/obj/item/clothing/mask/joy/Initialize(mapload) . = ..() - user.update_worn_mask() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/joy_mask, infinite = TRUE) /obj/item/clothing/mask/mummy name = "mummy mask" diff --git a/code/modules/clothing/under/_under.dm b/code/modules/clothing/under/_under.dm index dfff6b566f1..f437e1378ac 100644 --- a/code/modules/clothing/under/_under.dm +++ b/code/modules/clothing/under/_under.dm @@ -73,13 +73,6 @@ . = ..() dump_attachments() -/obj/item/clothing/under/setup_reskinning() - if(!check_setup_reskinning()) - return - - // We already register context in Initialize. - RegisterSignal(src, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin)) - /obj/item/clothing/under/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) . = ..() diff --git a/code/modules/clothing/under/accessories/_accessories.dm b/code/modules/clothing/under/accessories/_accessories.dm index 0e4e2f98190..993771e4888 100644 --- a/code/modules/clothing/under/accessories/_accessories.dm +++ b/code/modules/clothing/under/accessories/_accessories.dm @@ -31,13 +31,6 @@ . = ..() register_context() -/obj/item/clothing/accessory/setup_reskinning() - if(!check_setup_reskinning()) - return - - // We already register context regardless in Initialize. - RegisterSignal(src, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin)) - /** * Can we be attached to the passed clothing article? */ diff --git a/code/modules/clothing/under/accessories/badges.dm b/code/modules/clothing/under/accessories/badges.dm index 9d7d87a0846..9f871c71015 100644 --- a/code/modules/clothing/under/accessories/badges.dm +++ b/code/modules/clothing/under/accessories/badges.dm @@ -177,28 +177,51 @@ name = "Pre-Approved Cyborg Candidate dogtag" display = "This employee has been screened for negative mental traits to an acceptable level of accuracy, and is approved for the NT Cyborg program as an alternative to medical resuscitation." +// Pride pin skins +/datum/atom_skin/pride_pin + abstract_type = /datum/atom_skin/pride_pin + +/datum/atom_skin/pride_pin/gay + preview_name = "Rainbow Pride" + new_icon_state = "pride" + +/datum/atom_skin/pride_pin/bi + preview_name = "Bisexual Pride" + new_icon_state = "pride_bi" + +/datum/atom_skin/pride_pin/pan + preview_name = "Pansexual Pride" + new_icon_state = "pride_pan" + +/datum/atom_skin/pride_pin/ace + preview_name = "Asexual Pride" + new_icon_state = "pride_ace" + +/datum/atom_skin/pride_pin/enby + preview_name = "Non-binary Pride" + new_icon_state = "pride_enby" + +/datum/atom_skin/pride_pin/trans + preview_name = "Transgender Pride" + new_icon_state = "pride_trans" + +/datum/atom_skin/pride_pin/intersex + preview_name = "Intersex Pride" + new_icon_state = "pride_intersex" + +/datum/atom_skin/pride_pin/lesbian + preview_name = "Lesbian Pride" + new_icon_state = "pride_lesbian" + /obj/item/clothing/accessory/pride name = "pride pin" desc = "A Nanotrasen Diversity & Inclusion Center-sponsored holographic pin to show off your pride, reminding the crew of their unwavering commitment to equity, diversity, and inclusion!" icon_state = "pride" - obj_flags = UNIQUE_RENAME | INFINITE_RESKIN - unique_reskin = list( - "Rainbow Pride" = "pride", - "Bisexual Pride" = "pride_bi", - "Pansexual Pride" = "pride_pan", - "Asexual Pride" = "pride_ace", - "Non-binary Pride" = "pride_enby", - "Transgender Pride" = "pride_trans", - "Intersex Pride" = "pride_intersex", - "Lesbian Pride" = "pride_lesbian", - ) + obj_flags = UNIQUE_RENAME -/obj/item/clothing/accessory/pride/setup_reskinning() - if(!check_setup_reskinning()) - return - - // We already register context regardless in Initialize. - RegisterSignal(src, COMSIG_CLICK_ALT, PROC_REF(on_click_alt_reskin)) +/obj/item/clothing/accessory/pride/Initialize(mapload) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/pride_pin, infinite = TRUE) /obj/item/clothing/accessory/deaf_pin name = "deaf personnel pin" diff --git a/code/modules/clothing/under/costume.dm b/code/modules/clothing/under/costume.dm index fa7390b794b..a3bd9c01f4e 100644 --- a/code/modules/clothing/under/costume.dm +++ b/code/modules/clothing/under/costume.dm @@ -275,6 +275,26 @@ can_adjust = FALSE resistance_flags = NONE +// Mech suit skins +/datum/atom_skin/mech_suit + abstract_type = /datum/atom_skin/mech_suit + +/datum/atom_skin/mech_suit/red + preview_name = "Red" + new_icon_state = "red_mech_suit" + +/datum/atom_skin/mech_suit/white + preview_name = "White" + new_icon_state = "white_mech_suit" + +/datum/atom_skin/mech_suit/blue + preview_name = "Blue" + new_icon_state = "blue_mech_suit" + +/datum/atom_skin/mech_suit/black + preview_name = "Black" + new_icon_state = "black_mech_suit" + /obj/item/clothing/under/costume/mech_suit name = "mech pilot's suit" desc = "A mech pilot's suit. Might make your butt look big." @@ -286,13 +306,9 @@ alternate_worn_layer = GLOVES_LAYER //covers hands but gloves can go over it. This is how these things work in my head. can_adjust = FALSE - unique_reskin = list( - "Red" = "red_mech_suit", - "White" = "white_mech_suit", - "Blue" = "blue_mech_suit", - "Black" = "black_mech_suit", - ) - +/obj/item/clothing/under/costume/mech_suit/Initialize(mapload) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/mech_suit) /obj/item/clothing/under/costume/russian_officer name = "\improper Russian officer's uniform" diff --git a/code/modules/loadout/categories/accessories.dm b/code/modules/loadout/categories/accessories.dm index 2c90385b113..7f6a5658aa9 100644 --- a/code/modules/loadout/categories/accessories.dm +++ b/code/modules/loadout/categories/accessories.dm @@ -82,4 +82,4 @@ /datum/loadout_item/accessory/pride name = "Pride Pin" item_path = /obj/item/clothing/accessory/pride - loadout_flags = LOADOUT_FLAG_ALLOW_RESKIN + reskin_datum = /datum/atom_skin/pride_pin diff --git a/code/modules/loadout/loadout_items.dm b/code/modules/loadout/loadout_items.dm index 78561a9401c..a401b426078 100644 --- a/code/modules/loadout/loadout_items.dm +++ b/code/modules/loadout/loadout_items.dm @@ -50,8 +50,9 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) /// Icon state for the UI to use for preview icons. /// Set automatically if null var/ui_icon_state - /// Reskin options of this item if it can be reskinned. - VAR_FINAL/list/cached_reskin_options + /// Base typepath to what reskin datum this item can use to reskin into + /// Doesn't verify that the item_path actually has these reskins + var/reskin_datum /// A list of greyscale colors that are used for items that have greyscale support, but don't allow full customization. /// This is an assoc list of /datum/job_department -> colors, or /datum/job -> colors, allowing for preset colors based on player chosen job. /// Jobs are prioritized over departments. @@ -81,15 +82,6 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) ui_icon = item_path::icon_preview || item_path::icon ui_icon_state = item_path::icon_state_preview || item_path::icon_state - if(loadout_flags & LOADOUT_FLAG_ALLOW_RESKIN) - var/obj/item/dummy_item = new item_path() - if(!length(dummy_item.unique_reskin)) - loadout_flags &= ~LOADOUT_FLAG_ALLOW_RESKIN - stack_trace("Loadout item [item_path] has LOADOUT_FLAG_ALLOW_RESKIN but has no unique reskins.") - else - cached_reskin_options = dummy_item.unique_reskin.Copy() - qdel(dummy_item) - /datum/loadout_item/Destroy(force, ...) if(!force) stack_trace("QDEL called on loadout item [type]. This shouldn't ever happen. (Use FORCE if necessary.)") @@ -126,7 +118,7 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) return set_name(manager, user) if("set_skin") - if(loadout_flags & LOADOUT_FLAG_ALLOW_RESKIN) + if(reskin_datum) return set_skin(manager, user, params) return TRUE @@ -205,10 +197,7 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) /// Used for reskinning an item to an alt skin. /datum/loadout_item/proc/set_skin(datum/preference_middleware/loadout/manager, mob/user, params) - var/reskin_to = params["skin"] - if(!cached_reskin_options[reskin_to]) - return FALSE - + var/reskin_to = params["skin"] // sanity checking isn't necessary because it's all checked when equipped anyways var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) if(!loadout?[item_path]) return FALSE @@ -283,11 +272,13 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) equipped_item.name = trim(item_details[INFO_NAMED], PREVENT_CHARACTER_TRIM_LOSS(MAX_NAME_LEN)) ADD_TRAIT(equipped_item, TRAIT_WAS_RENAMED, "Loadout") - if((loadout_flags & LOADOUT_FLAG_ALLOW_RESKIN) && item_details?[INFO_RESKIN]) + if(reskin_datum && item_details?[INFO_RESKIN]) var/skin_chosen = item_details[INFO_RESKIN] - if(skin_chosen in equipped_item.unique_reskin) - equipped_item.current_skin = skin_chosen - equipped_item.icon_state = equipped_item.unique_reskin[skin_chosen] + for(var/datum/atom_skin/skin_path as anything in valid_subtypesof(reskin_datum)) + if(skin_path::preview_name != skin_chosen) + continue + var/datum/atom_skin/skin_instance = GLOB.atom_skins[skin_path] + skin_instance.apply(equipped_item) if(istype(equipped_item, /obj/item/clothing/accessory)) // Snowflake handing for accessories, because we need to update the thing it's attached to instead if(isclothing(equipped_item.loc)) @@ -296,6 +287,7 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) update_flag |= (ITEM_SLOT_OCLOTHING|ITEM_SLOT_ICLOTHING) else update_flag |= equipped_item.slot_flags + break return update_flag @@ -336,7 +328,7 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) if((loadout_flags & LOADOUT_FLAG_GREYSCALING_ALLOWED) && !(loadout_flags & LOADOUT_FLAG_JOB_GREYSCALING)) displayed_text[FA_ICON_PALETTE] = "Recolorable" - if(loadout_flags & LOADOUT_FLAG_ALLOW_RESKIN) + if(reskin_datum) displayed_text[FA_ICON_SWATCHBOOK] = "Reskinnable" return displayed_text @@ -381,16 +373,17 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) * Returns a list of options this item can be reskinned into. */ /datum/loadout_item/proc/get_reskin_options() as /list - if(!(loadout_flags & LOADOUT_FLAG_ALLOW_RESKIN)) + if(!reskin_datum) return null var/list/reskins = list() - for(var/skin in cached_reskin_options) + for(var/datum/atom_skin/skin as anything in valid_subtypesof(reskin_datum)) UNTYPED_LIST_ADD(reskins, list( - "name" = skin, - "tooltip" = skin, - "skin_icon_state" = cached_reskin_options[skin], + "name" = skin::new_name || skin::preview_name, + "tooltip" = skin::preview_name, + "skin_icon" = skin::new_icon, + "skin_icon_state" = skin::new_icon_state, )) return reskins diff --git a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm index baf71f5cbe3..55533341d89 100644 --- a/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm +++ b/code/modules/mining/equipment/kinetic_crusher/kinetic_crusher.dm @@ -12,6 +12,7 @@ suicidal miners against local fauna." icon = 'icons/obj/mining.dmi' icon_state = "crusher" + base_icon_state = "crusher" inhand_icon_state = "crusher0" icon_angle = -45 lefthand_file = 'icons/mob/inhands/weapons/hammers_lefthand.dmi' @@ -56,25 +57,43 @@ var/detonation_damage = 50 /// Damage that the mark additionally does when hit by the crusher via backstab var/backstab_bonus = 30 - /// Used by retool kits when changing the crusher's appearance - var/current_inhand_icon_state = "crusher" /// The file in which our projectile icon resides var/projectile_icon = 'icons/obj/weapons/guns/projectiles.dmi' /// Used by retool kits when changing the crusher's projectile sprite var/projectile_icon_state = "pulse1" /// Wielded damage we deal, aka our "real" damage var/force_wielded = 20 + /// Set to TRUE if the last projectile fired was point-blank at a living target + var/last_projectile_pb = FALSE /obj/item/kinetic_crusher/Initialize(mapload) . = ..() - AddComponent(/datum/component/butchering, \ + AddComponent( \ + /datum/component/butchering, \ speed = 6 SECONDS, \ effectiveness = 110, \ ) + update_reskin(null) //technically it's huge and bulky, but this provides an incentive to use it update_wielding() register_context() +/** + * Adds or updates the reskinning component on the crusher. + * + * * default_skin_typepath: The typepath of skin to apply by default. + * Passing null will either not apply a skin or will reset it to default if one is already applied. + * If a supplied skin is blacklisted, it will be un-blacklisted. + */ +/obj/item/kinetic_crusher/proc/update_reskin(datum/atom_skin/crusher_skin/default_skin_typepath) + AddComponent( \ + /datum/component/reskinable_item, \ + /datum/atom_skin/crusher_skin, \ + infinite = TRUE, \ + initial_skin = default_skin_typepath ? default_skin_typepath::preview_name : null, \ + blacklisted_subtypes = subtypesof(/datum/atom_skin/crusher_skin/locked) - default_skin_typepath, \ + ) + /obj/item/kinetic_crusher/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() if(!held_item) @@ -236,6 +255,7 @@ var/turf/proj_turf = user.loc if(!isturf(proj_turf)) return + last_projectile_pb = get_dist(target, user) <= 1 && isliving(target) var/obj/projectile/destabilizer/destabilizer = new(proj_turf) SEND_SIGNAL(src, COMSIG_CRUSHER_FIRED_BLAST, target, user, destabilizer) destabilizer.icon = projectile_icon @@ -281,7 +301,7 @@ return TRUE /obj/item/kinetic_crusher/update_icon_state() - inhand_icon_state = "[current_inhand_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]" // this is not icon_state and not supported by 2hcomponent + inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]" // this is not icon_state and not supported by 2hcomponent return ..() /obj/item/kinetic_crusher/update_overlays() diff --git a/code/modules/mining/equipment/kinetic_crusher/trophies_misc.dm b/code/modules/mining/equipment/kinetic_crusher/trophies_misc.dm index c4ff738b9cb..dca037e37cd 100644 --- a/code/modules/mining/equipment/kinetic_crusher/trophies_misc.dm +++ b/code/modules/mining/equipment/kinetic_crusher/trophies_misc.dm @@ -1,164 +1,95 @@ -/*! - * Contains crusher trophies that are not obtained from fauna - */ -/// Cosmetic items for changing the crusher's look -/obj/item/crusher_trophy/retool_kit - name = "crusher retool kit" - desc = "A toolkit for changing the crusher's appearance without affecting the device's function." - icon = 'icons/obj/mining.dmi' - icon_state = "retool_kit" - denied_type = /obj/item/crusher_trophy/retool_kit - - /// Currently picked crusher reskin - var/datum/crusher_skin/active_skin = /datum/crusher_skin/sword - /// If this kit forces some specific skin, or can pick between subtypes - var/forced_skin - -/obj/item/crusher_trophy/retool_kit/Destroy(force) - if (istype(active_skin)) - QDEL_NULL(active_skin) - return ..() - -/obj/item/crusher_trophy/retool_kit/effect_desc() - return "the crusher to have the appearance of \a [active_skin::name]" - -/obj/item/crusher_trophy/retool_kit/add_to(obj/item/kinetic_crusher/pkc, mob/user) - if (!forced_skin) - var/list/choices = list() - for (var/datum/crusher_skin/skin as anything in subtypesof(/datum/crusher_skin)) - if (skin::normal_skin) - choices[skin] = icon(skin::retool_icon || 'icons/obj/mining.dmi', skin::retool_icon_state) - var/datum/crusher_skin/chosen_skin = show_radial_menu(user, src, choices, tooltips = TRUE, require_near = TRUE) - if (!chosen_skin) - return - active_skin = chosen_skin - else - active_skin = forced_skin - . = ..() - if(!.) - return - active_skin = new active_skin(pkc) - if (active_skin.retool_icon) - pkc.icon = active_skin.retool_icon - pkc.icon_state = active_skin.retool_icon_state - pkc.current_inhand_icon_state = active_skin.retool_inhand_icon - if (active_skin.retool_projectile_icon) - pkc.projectile_icon = active_skin.retool_projectile_icon - if (active_skin.retool_projectile_icon_state) - pkc.projectile_icon_state = active_skin.retool_projectile_icon_state - // Should either have both, or neither - if (active_skin.retool_lefthand_file) - pkc.lefthand_file = active_skin.retool_lefthand_file - pkc.righthand_file = active_skin.retool_righthand_file - if(active_skin.retool_worn_file) - pkc.worn_icon = active_skin.retool_worn_file - pkc.worn_icon_state = active_skin::retool_icon_state - if (active_skin.retool_inhand_x) - pkc.inhand_x_dimension = active_skin.retool_inhand_x - if (active_skin.retool_inhand_y) - pkc.inhand_y_dimension = active_skin.retool_inhand_y - pkc.update_appearance() - pkc.update_slot_icon() - -/obj/item/crusher_trophy/retool_kit/remove_from(obj/item/kinetic_crusher/pkc) - var/skin_type = active_skin.type - qdel(active_skin) - active_skin = skin_type - pkc.icon = initial(pkc.icon) - pkc.icon_state = initial(pkc.icon_state) - pkc.current_inhand_icon_state = initial(pkc.current_inhand_icon_state) - pkc.projectile_icon = initial(pkc.projectile_icon) - pkc.projectile_icon_state = initial(pkc.projectile_icon_state) - pkc.lefthand_file = initial(pkc.lefthand_file) - pkc.righthand_file = initial(pkc.righthand_file) - pkc.worn_icon = initial(pkc.worn_icon) - pkc.worn_icon_state = initial(pkc.worn_icon_state) - pkc.inhand_x_dimension = initial(pkc.inhand_x_dimension) - pkc.inhand_y_dimension = initial(pkc.inhand_y_dimension) - pkc.update_appearance() - pkc.update_slot_icon() - return ..() - -/// Alternate PKC skins -/datum/crusher_skin - /// Name of the modification - var/name = "error that should be reported to coders" - /// Specifies the icon file in which the crusher's new state is stored. - var/retool_icon = 'icons/obj/mining.dmi' - ///Specifies the sprite/icon state which the crusher is changed to as an item. Should appear in the icons/obj/mining.dmi file with accompanying "lit" and "recharging" sprites - var/retool_icon_state = "ipickaxe" - ///Specifies the icon state for the crusher's appearance in hand. Should appear in both retool_lefthand_file and retool_righthand_file. - var/retool_inhand_icon = "ipickaxe" +// Alternate PKC skins +/datum/atom_skin/crusher_skin + abstract_type = /datum/atom_skin/crusher_skin + change_base_icon_state = TRUE + new_icon = 'icons/obj/mining.dmi' + new_icon_state = "ipickaxe" + /// Specifies the icon state for the crusher's appearance in hand. Should appear in both new_lefthand_file and new_righthand_file. + var/new_inhand_icon = "ipickaxe" /// Specifies the icon file in which the crusher's projectile sprite is located. - var/retool_projectile_icon = 'icons/obj/weapons/guns/projectiles.dmi' - ///For if the retool kit changes the projectile's appearance. - var/retool_projectile_icon_state = null + var/new_projectile_icon = 'icons/obj/weapons/guns/projectiles.dmi' + /// For if the retool kit changes the projectile's appearance. + var/new_projectile_icon_state /// Specifies the left hand inhand icon file. Don't forget to set the right hand file as well. - var/retool_lefthand_file = null + var/new_lefthand_file /// Specifies the right hand inhand icon file. Don't forget to set the left hand file as well. - var/retool_righthand_file = null + var/new_righthand_file /// Specifies the worn icon file. - var/retool_worn_file = null + var/new_worn_file /// Specifies the X dimensions of the new inhand, only relevant with different inhand files. - var/retool_inhand_x = null + var/new_inhandx /// Specifies the Y dimensions of the new inhand, only relevant with different inhand files. - var/retool_inhand_y = null - /// Can this skin be normally selected by a generic retool kit? - var/normal_skin = TRUE - /// Crusher this skin is attached to - var/obj/item/kinetic_crusher/crusher + var/new_inhandy -/datum/crusher_skin/New(obj/item/kinetic_crusher/new_crusher) +/datum/atom_skin/crusher_skin/apply(obj/item/kinetic_crusher/apply_to) . = ..() - crusher = new_crusher + APPLY_VAR_OR_RESET_INITIAL(apply_to, inhand_icon_state, new_inhand_icon, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, projectile_icon, new_projectile_icon, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, projectile_icon_state, new_projectile_icon_state, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, lefthand_file, new_lefthand_file, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, righthand_file, new_righthand_file, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, worn_icon, new_worn_file, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, worn_icon_state, new_icon_state, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, inhand_x_dimension, new_inhandx, reset_missing) + APPLY_VAR_OR_RESET_INITIAL(apply_to, inhand_y_dimension, new_inhandy, reset_missing) -/datum/crusher_skin/Destroy(force) - crusher = null - return ..() - -/datum/crusher_skin/sword - name = "sword" - retool_icon_state = "crusher_sword" - retool_inhand_icon = "crusher_sword" - -/datum/crusher_skin/harpoon - name = "harpoon" - retool_icon_state = "crusher_harpoon" - retool_inhand_icon = "crusher_harpoon" - retool_projectile_icon_state = "pulse_harpoon" - -/datum/crusher_skin/harpoon/New(obj/item/kinetic_crusher/new_crusher) +/datum/atom_skin/crusher_skin/clear_skin(obj/item/kinetic_crusher/clear_from) . = ..() - RegisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation)) + RESET_INITIAL_IF_SET(clear_from, inhand_icon_state, new_inhand_icon) + RESET_INITIAL_IF_SET(clear_from, projectile_icon, new_projectile_icon) + RESET_INITIAL_IF_SET(clear_from, projectile_icon_state, new_projectile_icon_state) + RESET_INITIAL_IF_SET(clear_from, lefthand_file, new_lefthand_file) + RESET_INITIAL_IF_SET(clear_from, righthand_file, new_righthand_file) + RESET_INITIAL_IF_SET(clear_from, worn_icon, new_worn_file) + RESET_INITIAL_IF_SET(clear_from, worn_icon_state, new_icon_state) + RESET_INITIAL_IF_SET(clear_from, inhand_x_dimension, new_inhandx) + RESET_INITIAL_IF_SET(clear_from, inhand_y_dimension, new_inhandy) -/datum/crusher_skin/harpoon/Destroy(force) - UnregisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION) - return ..() +/datum/atom_skin/crusher_skin/sword + new_name = "proto-kinetic sword" + preview_name = "Sword" + new_icon_state = "crusher_sword" + new_inhand_icon = "crusher_sword" -/datum/crusher_skin/harpoon/proc/on_attack_animation(obj/item/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override) +/datum/atom_skin/crusher_skin/harpoon + new_name = "proto-kinetic harpoon" + preview_name = "Harpoon" + new_icon_state = "crusher_harpoon" + new_inhand_icon = "crusher_harpoon" + new_projectile_icon_state = "pulse_harpoon" + +/datum/atom_skin/crusher_skin/harpoon/apply(atom/apply_to) + . = ..() + RegisterSignal(apply_to, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation)) + +/datum/atom_skin/crusher_skin/harpoon/clear_skin(atom/clear_from) + . = ..() + UnregisterSignal(clear_from, COMSIG_ITEM_ATTACK_ANIMATION) + +/datum/atom_skin/crusher_skin/harpoon/proc/on_attack_animation(obj/item/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override) SIGNAL_HANDLER // If nothing is forcing an animation type, attack with a piercing animation because we're a harpoon if (!animation_type) animation_override += ATTACK_ANIMATION_PIERCE -/datum/crusher_skin/dagger - name = "dual dagger and blaster" - retool_icon_state = "crusher_dagger" - retool_inhand_icon = "crusher_dagger" - /// Are we doing a blaster animation right now? - var/blaster_strike = FALSE +/datum/atom_skin/crusher_skin/dagger + new_name = "proto-kinetic dual dagger and blaster" + preview_name = "Dagger and Blaster" + new_icon_state = "crusher_dagger" + new_inhand_icon = "crusher_dagger" -/datum/crusher_skin/dagger/New(obj/item/kinetic_crusher/new_crusher) +/datum/atom_skin/crusher_skin/dagger/apply(atom/apply_to) . = ..() - RegisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation)) - RegisterSignal(crusher, COMSIG_CRUSHER_FIRED_BLAST, PROC_REF(on_fired_blast)) + RegisterSignal(apply_to, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation)) + RegisterSignal(apply_to, COMSIG_CRUSHER_FIRED_BLAST, PROC_REF(on_fired_blast)) -/datum/crusher_skin/dagger/Destroy(force) - UnregisterSignal(crusher, list(COMSIG_ITEM_ATTACK_ANIMATION, COMSIG_CRUSHER_FIRED_BLAST)) - return ..() +/datum/atom_skin/crusher_skin/dagger/clear_skin(atom/clear_from) + . = ..() + UnregisterSignal(clear_from, COMSIG_ITEM_ATTACK_ANIMATION) + UnregisterSignal(clear_from, COMSIG_CRUSHER_FIRED_BLAST) -/datum/crusher_skin/dagger/proc/on_attack_animation(obj/item/kinetic_crusher/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override) +/datum/atom_skin/crusher_skin/dagger/proc/on_attack_animation(obj/item/kinetic_crusher/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override) SIGNAL_HANDLER // If we've been forcefully assigned an animation type already, we shouldn't do the custom attack animation logic @@ -167,11 +98,11 @@ if (isliving(attacked_atom)) var/mob/living/target = attacked_atom - if (blaster_strike) + if (source.last_projectile_pb) image_override += image(icon = 'icons/obj/mining.dmi', icon_state = "crusher_dagger_blaster") angle_override += 0 animation_override += ATTACK_ANIMATION_PIERCE - blaster_strike = FALSE + source.last_projectile_pb = FALSE return if (target.has_status_effect(/datum/status_effect/crusher_mark)) @@ -179,33 +110,62 @@ image_override += image(icon = 'icons/obj/mining.dmi', icon_state = "crusher_dagger_melee") -/datum/crusher_skin/dagger/proc/on_fired_blast(obj/item/kinetic_crusher/source, atom/target, mob/living/user, obj/projectile/destabilizer/destabilizer) +/datum/atom_skin/crusher_skin/dagger/proc/on_fired_blast(obj/item/kinetic_crusher/source, atom/target, mob/living/user, obj/projectile/destabilizer/destabilizer) SIGNAL_HANDLER if (isliving(target) && get_dist(target, user) <= 1) - blaster_strike = TRUE user.do_item_attack_animation(target, used_item = source) -/datum/crusher_skin/glaive - name = "glaive" - retool_icon_state = "crusher_glaive" - retool_inhand_icon = "crusher_glaive" - retool_lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi' - retool_righthand_file = 'icons/mob/inhands/64x64_righthand.dmi' - retool_inhand_x = 64 - retool_inhand_y = 64 +/datum/atom_skin/crusher_skin/glaive + new_name = "proto-kinetic glaive" + preview_name = "Glaive" + new_icon_state = "crusher_glaive" + new_inhand_icon = "crusher_glaive" + new_lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi' + new_righthand_file = 'icons/mob/inhands/64x64_righthand.dmi' + new_inhandx = 64 + new_inhandy = 64 + +// Locked skins that cannot be selected normally +/datum/atom_skin/crusher_skin/locked + abstract_type = /datum/atom_skin/crusher_skin/locked + +/datum/atom_skin/crusher_skin/locked/ashen_skull + preview_name = "Skull" + new_icon_state = "crusher_skull" + new_inhand_icon = "crusher_skull" + new_projectile_icon_state = "pulse_skull" + +/// Unlockable (or forced) skins +/obj/item/crusher_trophy/retool_kit + name = "crusher retool kit" + desc = "A toolkit for changing the crusher's appearance without affecting the device's function." + icon = 'icons/obj/mining.dmi' + icon_state = "retool_kit" + denied_type = /obj/item/crusher_trophy/retool_kit + + /// What skin do we apply when attached + var/datum/atom_skin/crusher_skin/forced_skin + +/obj/item/crusher_trophy/retool_kit/effect_desc() + return "the crusher to have the appearance of \a [forced_skin::preview_name]" + +/obj/item/crusher_trophy/retool_kit/add_to(obj/item/kinetic_crusher/pkc, mob/user) + . = ..() + if(!.) + return + + pkc.update_reskin(forced_skin) + +/obj/item/crusher_trophy/retool_kit/remove_from(obj/item/kinetic_crusher/pkc) + pkc.update_reskin(null) // resets reskin component + return ..() /obj/item/crusher_trophy/retool_kit/ashenskull name = "ashen skull" desc = "It burns with the flame of the necropolis, whispering in your ear. It demands to be bound to a suitable weapon." icon_state = "retool_kit_skull" - forced_skin = /datum/crusher_skin/ashen_skull + forced_skin = /datum/atom_skin/crusher_skin/locked/ashen_skull /obj/item/crusher_trophy/retool_kit/ashenskull/effect_desc() return "the crusher to appear corrupted by infernal powers" - -/datum/crusher_skin/ashen_skull - retool_icon_state = "crusher_skull" - retool_inhand_icon = "crusher_skull" - retool_projectile_icon_state = "pulse_skull" - normal_skin = FALSE diff --git a/code/modules/paperwork/clipboard.dm b/code/modules/paperwork/clipboard.dm index 46c27082c75..2856dddc296 100644 --- a/code/modules/paperwork/clipboard.dm +++ b/code/modules/paperwork/clipboard.dm @@ -1,3 +1,20 @@ +// Clipboard skins +/datum/atom_skin/clipboard + abstract_type = /datum/atom_skin/clipboard + change_inhand_icon_state = TRUE + +/datum/atom_skin/clipboard/brown + preview_name = "Brown" + new_icon_state = "clipboard" + +/datum/atom_skin/clipboard/black + preview_name = "Black" + new_icon_state = "clipboard_black" + +/datum/atom_skin/clipboard/white + preview_name = "White" + new_icon_state = "clipboard_white" + /** * Clipboard */ @@ -14,13 +31,6 @@ slot_flags = ITEM_SLOT_BELT resistance_flags = FLAMMABLE - unique_reskin = list( - "Brown" = "clipboard", - "Black" = "clipboard_black", - "White" = "clipboard_white", - ) - unique_reskin_changes_inhand = TRUE - /// The stored pen var/obj/item/pen/pen /// Is the pen integrated? @@ -39,6 +49,7 @@ /obj/item/clipboard/Initialize(mapload) update_appearance() . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/clipboard) /obj/item/clipboard/Destroy() QDEL_NULL(pen) diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm index 9bd4324a583..e92bfe47b47 100644 --- a/code/modules/paperwork/pen.dm +++ b/code/modules/paperwork/pen.dm @@ -177,6 +177,38 @@ time = 3 SECONDS category = CAT_TOOLS +// Skins for captain's fountain pen +/datum/atom_skin/cap_pen + abstract_type = /datum/atom_skin/cap_pen + +/datum/atom_skin/cap_pen/apply(atom/apply_to) + . = ..() + apply_to.desc = "It's an expensive [preview_name] fountain pen. The nib is quite sharp." + +/datum/atom_skin/cap_pen/clear_skin(atom/clear_from) + . = ..() + clear_from.desc = initial(clear_from.desc) + +/datum/atom_skin/cap_pen/oak + preview_name = "Oak" + new_icon_state = "pen-fountain-o" + +/datum/atom_skin/cap_pen/gold + preview_name = "Gold" + new_icon_state = "pen-fountain-g" + +/datum/atom_skin/cap_pen/rosewood + preview_name = "Rosewood" + new_icon_state = "pen-fountain-r" + +/datum/atom_skin/cap_pen/black_silver + preview_name = "Black and Silver" + new_icon_state = "pen-fountain-b" + +/datum/atom_skin/cap_pen/command_blue + preview_name = "Command Blue" + new_icon_state = "pen-fountain-cb" + /obj/item/pen/fountain/captain name = "captain's fountain pen" desc = "It's an expensive Oak fountain pen. The nib is quite sharp." @@ -187,22 +219,15 @@ custom_materials = list(/datum/material/gold = SMALL_MATERIAL_AMOUNT*7.5) sharpness = SHARP_EDGED resistance_flags = FIRE_PROOF - unique_reskin = list( - "Oak" = "pen-fountain-o", - "Gold" = "pen-fountain-g", - "Rosewood" = "pen-fountain-r", - "Black and Silver" = "pen-fountain-b", - "Command Blue" = "pen-fountain-cb" - ) embed_type = /datum/embedding/pen/captain dart_insert_casing_icon_state = "overlay_fountainpen_gold" dart_insert_projectile_icon_state = "overlay_fountainpen_gold_proj" var/list/overlay_reskin = list( - "Oak" = "overlay_fountainpen_gold", - "Gold" = "overlay_fountainpen_gold", - "Rosewood" = "overlay_fountainpen_gold", - "Black and Silver" = "overlay_fountainpen", - "Command Blue" = "overlay_fountainpen_gold" + /datum/atom_skin/cap_pen/black_silver::preview_name = "overlay_fountainpen", + /datum/atom_skin/cap_pen/command_blue::preview_name = "overlay_fountainpen_gold", + /datum/atom_skin/cap_pen/gold::preview_name = "overlay_fountainpen_gold", + /datum/atom_skin/cap_pen/oak::preview_name = "overlay_fountainpen_gold", + /datum/atom_skin/cap_pen/rosewood::preview_name = "overlay_fountainpen_gold", ) /datum/embedding/pen/captain @@ -210,24 +235,21 @@ /obj/item/pen/fountain/captain/Initialize(mapload) . = ..() - AddComponent(/datum/component/butchering, \ - speed = 20 SECONDS, \ - effectiveness = 115, \ + AddComponent( \ + /datum/component/butchering, \ + speed = 20 SECONDS, \ + effectiveness = 115, \ ) + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/cap_pen) //the pen is mightier than the sword RegisterSignal(src, COMSIG_DART_INSERT_PARENT_RESKINNED, PROC_REF(reskin_dart_insert)) -/obj/item/pen/fountain/captain/reskin_obj(mob/M) - ..() - if(current_skin) - desc = "It's an expensive [current_skin] fountain pen. The nib is quite sharp." - - -/obj/item/pen/fountain/captain/proc/reskin_dart_insert(datum/component/dart_insert/insert_comp) +/obj/item/pen/fountain/captain/proc/reskin_dart_insert(datum/component/dart_insert/insert_comp, skin) + SIGNAL_HANDLER if(!istype(insert_comp)) //You really shouldn't be sending this signal from anything other than a dart_insert component return - insert_comp.casing_overlay_icon_state = overlay_reskin[current_skin] - insert_comp.projectile_overlay_icon_state = "[overlay_reskin[current_skin]]_proj" + insert_comp.casing_overlay_icon_state = overlay_reskin[skin] + insert_comp.projectile_overlay_icon_state = "[overlay_reskin[skin]]_proj" /obj/item/pen/item_ctrl_click(mob/living/carbon/user) if(loc != user) diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm index 6bc9022e258..c3a82c96b72 100644 --- a/code/modules/projectiles/guns/ballistic.dm +++ b/code/modules/projectiles/guns/ballistic.dm @@ -9,7 +9,6 @@ pickup_sound = 'sound/items/handling/gun/gun_pick_up.ogg' drop_sound = 'sound/items/handling/gun/gun_drop.ogg' sound_vary = TRUE - unique_reskin_changes_base_icon_state = TRUE ///sound when inserting magazine var/load_sound = 'sound/items/weapons/gun/general/magazine_insert_full.ogg' diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm index 06a8b50c566..6f4b9be9330 100644 --- a/code/modules/projectiles/guns/ballistic/revolver.dm +++ b/code/modules/projectiles/guns/ballistic/revolver.dm @@ -97,8 +97,7 @@ . = ..() var/live_ammo = get_ammo(FALSE, FALSE) . += "[live_ammo ? live_ammo : "None"] of those are live rounds." - if (current_skin) - . += span_notice("It can be spun with [EXAMINE_HINT("alt-click")].") + . += span_notice("It can be spun with [EXAMINE_HINT("alt-click")].") /obj/item/gun/ballistic/revolver/ignition_effect(atom/A, mob/user) if(last_fire && last_fire + 15 SECONDS > world.time) @@ -112,6 +111,47 @@ base_icon_state = "c38" fire_sound = 'sound/items/weapons/gun/revolver/shot.ogg' +// 38 special skins +/datum/atom_skin/det_revolver + abstract_type = /datum/atom_skin/det_revolver + change_base_icon_state = TRUE + +/datum/atom_skin/det_revolver/default + preview_name = "Default" + new_icon_state = "c38" + +/datum/atom_skin/det_revolver/fitz_special + preview_name = "Fitz Special" + new_icon_state = "c38_fitz" + +/datum/atom_skin/det_revolver/police_positive_special + preview_name = "Police Positive Special" + new_icon_state = "c38_police" + +/datum/atom_skin/det_revolver/blued_steel + preview_name = "Blued Steel" + new_icon_state = "c38_blued" + +/datum/atom_skin/det_revolver/stainless_steel + preview_name = "Stainless Steel" + new_icon_state = "c38_stainless" + +/datum/atom_skin/det_revolver/gold_trim + preview_name = "Gold Trim" + new_icon_state = "c38_trim" + +/datum/atom_skin/det_revolver/golden + preview_name = "Golden" + new_icon_state = "c38_gold" + +/datum/atom_skin/det_revolver/peacemaker + preview_name = "The Peacemaker" + new_icon_state = "c38_peacemaker" + +/datum/atom_skin/det_revolver/black_panther + preview_name = "Black Panther" + new_icon_state = "c38_panther" + /obj/item/gun/ballistic/revolver/c38/detective name = "\improper Colt Detective Special" desc = "A classic, if not outdated, law enforcement firearm. Uses .38 Special rounds. \nSome spread rumors that if you loosen the barrel with a wrench, you can \"improve\" it." @@ -126,17 +166,10 @@ misfire_percentage_increment = 25 //about 1 in 4 rounds, which increases rapidly every shot obj_flags = UNIQUE_RENAME - unique_reskin = list( - "Default" = "c38", - "Fitz Special" = "c38_fitz", - "Police Positive Special" = "c38_police", - "Blued Steel" = "c38_blued", - "Stainless Steel" = "c38_stainless", - "Gold Trim" = "c38_trim", - "Golden" = "c38_gold", - "The Peacemaker" = "c38_peacemaker", - "Black Panther" = "c38_panther" - ) + +/obj/item/gun/ballistic/revolver/c38/detective/Initialize(mapload) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/det_revolver) /obj/item/gun/ballistic/revolver/badass name = "\improper Badass Revolver" diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm index e3b631b0e79..124284f3c99 100644 --- a/code/modules/projectiles/guns/ballistic/shotgun.dm +++ b/code/modules/projectiles/guns/ballistic/shotgun.dm @@ -285,6 +285,35 @@ /obj/item/gun/ballistic/shotgun/bulldog/unrestricted pin = /obj/item/firing_pin +// Double barrel shotgun skins +/datum/atom_skin/bar_shotgun + abstract_type = /datum/atom_skin/bar_shotgun + change_base_icon_state = TRUE + +/datum/atom_skin/bar_shotgun/default + preview_name = "Standard Finish" + new_icon_state = "dshotgun" + +/datum/atom_skin/bar_shotgun/dark_red + preview_name = "Dark Red Finish" + new_icon_state = "dshotgun_d" + +/datum/atom_skin/bar_shotgun/ash + preview_name = "Ash" + new_icon_state = "dshotgun_f" + +/datum/atom_skin/bar_shotgun/faded_grey + preview_name = "Faded Grey" + new_icon_state = "dshotgun_g" + +/datum/atom_skin/bar_shotgun/maple + preview_name = "Maple" + new_icon_state = "dshotgun_l" + +/datum/atom_skin/bar_shotgun/rosewood + preview_name = "Rosewood" + new_icon_state = "dshotgun_p" + ///////////////////////////// // DOUBLE BARRELED SHOTGUN // ///////////////////////////// @@ -304,18 +333,15 @@ sawn_desc = "Omar's coming!" obj_flags = UNIQUE_RENAME rack_sound_volume = 0 - unique_reskin = list("Default" = "dshotgun", - "Dark Red Finish" = "dshotgun_d", - "Ash" = "dshotgun_f", - "Faded Grey" = "dshotgun_g", - "Maple" = "dshotgun_l", - "Rosewood" = "dshotgun_p" - ) semi_auto = TRUE bolt_type = BOLT_TYPE_NO_BOLT can_be_sawn_off = TRUE pb_knockback = 3 // it's a super shotgun! +/obj/item/gun/ballistic/shotgun/doublebarrel/Initialize(mapload) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/bar_shotgun) + /obj/item/gun/ballistic/shotgun/doublebarrel/sawoff(mob/user) . = ..() if(.) diff --git a/code/modules/reagents/reagent_containers/medigel.dm b/code/modules/reagents/reagent_containers/medigel.dm index f21c08cdb0c..d88a427277c 100644 --- a/code/modules/reagents/reagent_containers/medigel.dm +++ b/code/modules/reagents/reagent_containers/medigel.dm @@ -1,3 +1,31 @@ +// Gel skins +/datum/atom_skin/med_gel + abstract_type = /datum/atom_skin/med_gel + +/datum/atom_skin/med_gel/blue + preview_name = "Blue" + new_icon_state = "medigel_blue" + +/datum/atom_skin/med_gel/cyan + preview_name = "Cyan" + new_icon_state = "medigel_cyan" + +/datum/atom_skin/med_gel/green + preview_name = "Green" + new_icon_state = "medigel_green" + +/datum/atom_skin/med_gel/red + preview_name = "Red" + new_icon_state = "medigel_red" + +/datum/atom_skin/med_gel/orange + preview_name = "Orange" + new_icon_state = "medigel_orange" + +/datum/atom_skin/med_gel/purple + preview_name = "Purple" + new_icon_state = "medigel_purple" + /obj/item/reagent_containers/medigel name = "medical gel" desc = "A medical gel applicator bottle, designed for precision application, with an unscrewable cap." @@ -23,15 +51,11 @@ var/apply_method = "spray" //the thick gel is sprayed and then dries into patch like film. var/self_delay = 30 custom_price = PAYCHECK_CREW * 2 - unique_reskin = list( - "Blue" = "medigel_blue", - "Cyan" = "medigel_cyan", - "Green" = "medigel_green", - "Red" = "medigel_red", - "Orange" = "medigel_orange", - "Purple" = "medigel_purple" - ) +/obj/item/reagent_containers/medigel/Initialize(mapload) + . = ..() + if(icon_state == "medigel") // oh yeah baby raw icon state check to make sure we can't reskin preset gels + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/med_gel) /obj/item/reagent_containers/medigel/mode_change_message(mob/user) var/squirt_mode = amount_per_transfer_from_this == initial(amount_per_transfer_from_this) @@ -78,21 +102,18 @@ name = "medical gel (libital)" desc = "A medical gel applicator bottle, designed for precision application, with an unscrewable cap. This one contains libital, for treating cuts and bruises. Libital does minor liver damage. Diluted with granibitaluri." icon_state = "brutegel" - current_skin = "brutegel" list_reagents = list(/datum/reagent/medicine/c2/libital = 24, /datum/reagent/medicine/granibitaluri = 36) /obj/item/reagent_containers/medigel/aiuri name = "medical gel (aiuri)" desc = "A medical gel applicator bottle, designed for precision application, with an unscrewable cap. This one contains aiuri, useful for treating burns. Aiuri does minor eye damage. Diluted with granibitaluri." icon_state = "burngel" - current_skin = "burngel" list_reagents = list(/datum/reagent/medicine/c2/aiuri = 24, /datum/reagent/medicine/granibitaluri = 36) /obj/item/reagent_containers/medigel/synthflesh name = "medical gel (synthflesh)" desc = "A medical gel applicator bottle, designed for precision application, with an unscrewable cap. This one contains synthflesh, a slightly toxic medicine capable of healing bruises, burns, and husks." icon_state = "synthgel" - current_skin = "synthgel" list_reagents = list(/datum/reagent/medicine/c2/synthflesh = 60) list_reagents_purity = 1 amount_per_transfer_from_this = 60 @@ -117,6 +138,5 @@ name = "sterilizer gel" desc = "gel bottle loaded with non-toxic sterilizer. Useful in preparation for surgery." icon_state = "medigel_blue" - current_skin = "medigel_blue" list_reagents = list(/datum/reagent/space_cleaner/sterilizine = 60) custom_price = PAYCHECK_CREW * 2 diff --git a/code/modules/reagents/reagent_containers/spray.dm b/code/modules/reagents/reagent_containers/spray.dm index 869131dd5e8..4c4f0dbcbf9 100644 --- a/code/modules/reagents/reagent_containers/spray.dm +++ b/code/modules/reagents/reagent_containers/spray.dm @@ -420,6 +420,23 @@ . = ..() icon_state = pick("sprayer_sus_1", "sprayer_sus_2", "sprayer_sus_3", "sprayer_sus_4", "sprayer_sus_5","sprayer_sus_6", "sprayer_sus_7", "sprayer_sus_8") +// Spray bottle skins +/datum/atom_skin/med_spray + abstract_type = /datum/atom_skin/med_spray + change_inhand_icon_state = TRUE + +/datum/atom_skin/med_spray/red + preview_name = "Red" + new_icon_state = "sprayer_med_red" + +/datum/atom_skin/med_spray/yellow + preview_name = "Yellow" + new_icon_state = "sprayer_med_yellow" + +/datum/atom_skin/med_spray/blue + preview_name = "Blue" + new_icon_state = "sprayer_med_blue" + /obj/item/reagent_containers/spray/medical name = "medical spray bottle" icon = 'icons/obj/medical/chemical.dmi' @@ -428,20 +445,10 @@ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' volume = 100 - unique_reskin = list("Red" = "sprayer_med_red", - "Yellow" = "sprayer_med_yellow", - "Blue" = "sprayer_med_blue") -/obj/item/reagent_containers/spray/medical/reskin_obj(mob/M) - ..() - switch(icon_state) - if("sprayer_med_red") - inhand_icon_state = "sprayer_med_red" - if("sprayer_med_yellow") - inhand_icon_state = "sprayer_med_yellow" - if("sprayer_med_blue") - inhand_icon_state = "sprayer_med_blue" - M.update_held_items() +/obj/item/reagent_containers/spray/medical/Initialize(mapload, vol) + . = ..() + AddComponent(/datum/component/reskinable_item, /datum/atom_skin/med_spray) /obj/item/reagent_containers/spray/hercuri name = "medical spray (hercuri)" diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index e4f6debfe68..f71cb0f33cf 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -266,6 +266,7 @@ #include "recycle_recycling.dm" #include "required_map_items.dm" #include "resist.dm" +#include "reskin_validation.dm" #include "reta_system.dm" #include "say.dm" #include "screenshot_airlocks.dm" diff --git a/code/modules/unit_tests/reskin_validation.dm b/code/modules/unit_tests/reskin_validation.dm new file mode 100644 index 00000000000..85e454058ce --- /dev/null +++ b/code/modules/unit_tests/reskin_validation.dm @@ -0,0 +1,15 @@ +/datum/unit_test/reskin_validation + +/datum/unit_test/reskin_validation/Run() + var/list/known_names = list() + for(var/datum/atom_skin/skin as anything in valid_subtypesof(/datum/atom_skin)) + if(isnull(skin::preview_name)) + TEST_FAIL("Reskin [skin] is missing a preview_name.") + // preview names are bundled by abstract types + else if(known_names["[skin::preview_name]-[skin::abstract_type]"]) + TEST_FAIL("Reskin [skin] has a duplicate preview_name [skin::preview_name].") + else + known_names["[skin::preview_name]-[skin::abstract_type]"] = TRUE + + if(skin::new_icon && skin::new_icon_state && !icon_exists(skin::new_icon, skin::new_icon_state)) + TEST_FAIL("Reskin [skin] has a new_icon_state [skin::new_icon_state] that does not exist in file [skin::new_icon].") diff --git a/tgstation.dme b/tgstation.dme index b0e04c9b7f6..5dbe1562184 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -209,6 +209,7 @@ #include "code\__DEFINES\regex.dm" #include "code\__DEFINES\religion.dm" #include "code\__DEFINES\research.dm" +#include "code\__DEFINES\reskin.dm" #include "code\__DEFINES\resonator.dm" #include "code\__DEFINES\revolution.dm" #include "code\__DEFINES\robots.dm" @@ -1271,6 +1272,7 @@ #include "code\datums\components\regenerator.dm" #include "code\datums\components\religious_tool.dm" #include "code\datums\components\rename.dm" +#include "code\datums\components\reskinnable_atom.dm" #include "code\datums\components\revenge_ability.dm" #include "code\datums\components\rot.dm" #include "code\datums\components\rotation.dm" @@ -2371,7 +2373,6 @@ #include "code\game\objects\buckling.dm" #include "code\game\objects\empulse.dm" #include "code\game\objects\items.dm" -#include "code\game\objects\items_reskin.dm" #include "code\game\objects\obj_defense.dm" #include "code\game\objects\objs.dm" #include "code\game\objects\structures.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/loadout/ModifyPanel.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/loadout/ModifyPanel.tsx index 256b9650192..c2ca21aca6b 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/loadout/ModifyPanel.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/loadout/ModifyPanel.tsx @@ -104,7 +104,7 @@ function LoadoutModifyButtons(props: ButtonsProps) { {!!modifyItemDimmer.reskins && ( - + {modifyItemDimmer.reskins.map((reskin) => (