diff --git a/code/modules/antagonists/clown_ops/outfits.dm b/code/modules/antagonists/clown_ops/outfits.dm
index 59e11989ff3..8536ffd056e 100644
--- a/code/modules/antagonists/clown_ops/outfits.dm
+++ b/code/modules/antagonists/clown_ops/outfits.dm
@@ -13,7 +13,9 @@
/obj/item/knife/combat/survival,
/obj/item/dnainjector/clumsymut, //in case you want to be clumsy for the memes
/obj/item/storage/box/syndie_kit/clownpins, //for any guns that you get your grubby little clown op mitts on
- /obj/item/reagent_containers/spray/waterflower/lube)
+ /obj/item/reagent_containers/spray/waterflower/lube,
+ /obj/item/mod/skin_applier/honkerative,
+ )
implants = list(/obj/item/implant/sad_trombone)
uplink_type = /obj/item/uplink/clownop
diff --git a/code/modules/mod/mod_construction.dm b/code/modules/mod/mod_construction.dm
index 55d56875188..0b26f567af9 100644
--- a/code/modules/mod/mod_construction.dm
+++ b/code/modules/mod/mod_construction.dm
@@ -79,12 +79,6 @@
/obj/item/mod/construction/plating/cosmohonk
theme = /datum/mod_theme/cosmohonk
-/obj/item/mod/paint
- name = "MOD paint kit"
- desc = "This kit will repaint your MODsuit to something unique."
- icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'
- icon_state = "paintkit"
-
#define START_STEP "start"
#define CORE_STEP "core"
#define SCREWED_CORE_STEP "screwed_core"
diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm
index 05930871872..59b415d8e4f 100644
--- a/code/modules/mod/mod_control.dm
+++ b/code/modules/mod/mod_control.dm
@@ -371,14 +371,6 @@
else if(is_wire_tool(attacking_item) && open)
wires.interact(user)
return TRUE
- else if(istype(attacking_item, /obj/item/mod/paint))
- if(active || activating)
- balloon_alert(user, "suit is active!")
- else if(paint(user, attacking_item))
- balloon_alert(user, "suit painted")
- else
- balloon_alert(user, "not painted!")
- return TRUE
else if(open && attacking_item.GetID())
update_access(user, attacking_item.GetID())
return TRUE
@@ -515,23 +507,6 @@
return
picked_module.on_select()
-/obj/item/mod/control/proc/paint(mob/user, obj/item/paint)
- if(length(theme.skins) <= 1)
- return FALSE
- var/list/skins = list()
- for(var/mod_skin in theme.skins)
- skins[mod_skin] = image(icon = icon, icon_state = "[mod_skin]-control")
- var/pick = show_radial_menu(user, src, skins, custom_check = FALSE, require_near = TRUE)
- if(!pick || !user.is_holding(paint))
- return FALSE
- skin = pick
- var/list/skin_updating = mod_parts.Copy() + src
- for(var/obj/item/piece as anything in skin_updating)
- piece.icon_state = "[skin]-[initial(piece.icon_state)]"
- update_flags()
- wearer?.regenerate_icons()
- return TRUE
-
/obj/item/mod/control/proc/shock(mob/living/user)
if(!istype(user) || get_charge() < 1)
return FALSE
@@ -633,6 +608,21 @@
balloon_alert(wearer, "no power!")
toggle_activate(wearer, force_deactivate = TRUE)
+/obj/item/mod/control/proc/set_mod_color(new_color)
+ var/list/all_parts = mod_parts.Copy() + src
+ for(var/obj/item/part as anything in all_parts)
+ part.remove_atom_colour(WASHABLE_COLOUR_PRIORITY)
+ part.add_atom_colour(new_color, FIXED_COLOUR_PRIORITY)
+ wearer?.regenerate_icons()
+
+/obj/item/mod/control/proc/set_mod_skin(new_skin)
+ skin = new_skin
+ var/list/skin_updating = mod_parts.Copy() + src
+ for(var/obj/item/piece as anything in skin_updating)
+ piece.icon_state = "[skin]-[initial(piece.icon_state)]"
+ update_flags()
+ wearer?.regenerate_icons()
+
/obj/item/mod/control/proc/on_exit(datum/source, atom/movable/part, direction)
SIGNAL_HANDLER
@@ -644,10 +634,10 @@
return
if(part.loc == wearer)
return
- if(modules.Find(part))
+ if(part in modules)
uninstall(part)
return
- if(mod_parts.Find(part))
+ if(part in mod_parts)
conceal(wearer, part)
if(active)
INVOKE_ASYNC(src, .proc/toggle_activate, wearer, TRUE)
@@ -659,14 +649,11 @@
if(slowdown_inactive <= 0)
to_chat(user, span_warning("[src] has already been coated with red, that's as fast as it'll go!"))
return SPEED_POTION_STOP
- if(wearer)
- to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's on someone!"))
+ if(active)
+ to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's active!"))
return SPEED_POTION_STOP
to_chat(user, span_notice("You slather the red gunk over [src], making it faster."))
- var/list/all_parts = mod_parts.Copy() + src
- for(var/obj/item/part as anything in all_parts)
- part.remove_atom_colour(WASHABLE_COLOUR_PRIORITY)
- part.add_atom_colour("#FF0000", FIXED_COLOUR_PRIORITY)
+ set_mod_color("#FF0000")
slowdown_inactive = 0
slowdown_active = 0
update_speed()
diff --git a/code/modules/mod/mod_paint.dm b/code/modules/mod/mod_paint.dm
new file mode 100644
index 00000000000..0b00f6fa723
--- /dev/null
+++ b/code/modules/mod/mod_paint.dm
@@ -0,0 +1,194 @@
+#define MODPAINT_MAX_COLOR_VALUE 1.25
+#define MODPAINT_MIN_COLOR_VALUE 0
+#define MODPAINT_MAX_SECTION_COLORS 2
+#define MODPAINT_MIN_SECTION_COLORS 0.25
+#define MODPAINT_MAX_OVERALL_COLORS 4
+#define MODPAINT_MIN_OVERALL_COLORS 1.5
+
+/obj/item/mod/paint
+ name = "MOD paint kit"
+ desc = "This kit will repaint your MODsuit to something unique."
+ icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'
+ icon_state = "paintkit"
+ var/obj/item/mod/control/editing_mod
+ var/atom/movable/screen/color_matrix_proxy_view/proxy_view
+ var/list/current_color
+
+/obj/item/mod/paint/Initialize(mapload)
+ . = ..()
+ current_color = color_matrix_identity()
+
+/obj/item/mod/paint/examine(mob/user)
+ . = ..()
+ . += span_notice("Left-click a MODsuit to change skin.")
+ . += span_notice("Right-click a MODsuit to recolor.")
+
+/obj/item/mod/paint/pre_attack(atom/attacked_atom, mob/living/user, params)
+ if(!istype(attacked_atom, /obj/item/mod/control))
+ return ..()
+ var/obj/item/mod/control/mod = attacked_atom
+ if(mod.active || mod.activating)
+ balloon_alert(user, "suit is active!")
+ return TRUE
+ paint_skin(mod, user)
+
+/obj/item/mod/paint/pre_attack_secondary(atom/attacked_atom, mob/living/user, params)
+ if(!istype(attacked_atom, /obj/item/mod/control))
+ return ..()
+ var/obj/item/mod/control/mod = attacked_atom
+ if(mod.active || mod.activating)
+ balloon_alert(user, "suit is active!")
+ return TRUE
+ if(editing_mod)
+ return TRUE
+ editing_mod = mod
+ proxy_view = new()
+ proxy_view.appearance = editing_mod.appearance
+ proxy_view.color = null
+ proxy_view.register_to_client(user.client)
+ ui_interact(user)
+ return TRUE
+
+/obj/item/mod/paint/ui_interact(mob/user, datum/tgui/ui)
+ if(!editing_mod)
+ return
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "MODpaint", name)
+ ui.open()
+
+/obj/item/mod/paint/ui_host()
+ return editing_mod
+
+/obj/item/mod/paint/ui_close(mob/user)
+ . = ..()
+ editing_mod = null
+ QDEL_NULL(proxy_view)
+ current_color = color_matrix_identity()
+
+/obj/item/mod/paint/ui_status(mob/user)
+ if(check_menu(editing_mod, user))
+ return ..()
+ return UI_CLOSE
+
+/obj/item/mod/paint/ui_static_data(mob/user)
+ var/list/data = list()
+ data["mapRef"] = proxy_view.assigned_map
+ return data
+
+/obj/item/mod/paint/ui_data(mob/user)
+ var/list/data = list()
+ data["currentColor"] = current_color
+ return data
+
+/obj/item/mod/paint/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("transition_color")
+ current_color = params["color"]
+ animate(proxy_view, time = 0.5 SECONDS, color = current_color)
+ if("confirm")
+ if(length(current_color) != 20) //20 is the length of a matrix identity list
+ return
+ for(var/color_value in current_color)
+ if(isnum(color_value))
+ continue
+ return
+ var/total_color_value = 0
+ var/list/total_colors = current_color.Copy()
+ total_colors.Cut(13, length(total_colors)) // 13 to 20 are just a and c, dont want to count them
+ var/red_value = current_color[1] + current_color[5] + current_color[9] //rr + gr + br
+ var/green_value = current_color[2] + current_color[6] + current_color[10] //rg + gg + bg
+ var/blue_value = current_color[3] + current_color[7] + current_color[11] //rb + gb + bb
+ if(red_value > MODPAINT_MAX_SECTION_COLORS)
+ balloon_alert(usr, "total red too high! ([red_value*100]%/[MODPAINT_MAX_SECTION_COLORS*100]%)")
+ return
+ else if(red_value < MODPAINT_MIN_SECTION_COLORS)
+ balloon_alert(usr, "total red too low! ([red_value*100]%/[MODPAINT_MIN_SECTION_COLORS*100]%)")
+ return
+ if(green_value > MODPAINT_MAX_SECTION_COLORS)
+ balloon_alert(usr, "total green too high! ([green_value*100]%/[MODPAINT_MAX_SECTION_COLORS*100]%)")
+ return
+ else if(green_value < MODPAINT_MIN_SECTION_COLORS)
+ balloon_alert(usr, "total green too low! ([green_value*100]%/[MODPAINT_MIN_SECTION_COLORS*100]%)")
+ return
+ if(blue_value > MODPAINT_MAX_SECTION_COLORS)
+ balloon_alert(usr, "total blue too high! ([blue_value*100]%/[MODPAINT_MAX_SECTION_COLORS*100]%)")
+ return
+ else if(blue_value < MODPAINT_MIN_SECTION_COLORS)
+ balloon_alert(usr, "total blue too low! ([blue_value*100]%/[MODPAINT_MIN_SECTION_COLORS*100]%)")
+ return
+ for(var/color_value in total_colors)
+ total_color_value += color_value
+ if(color_value > MODPAINT_MAX_COLOR_VALUE)
+ balloon_alert(usr, "one of colors too high! ([color_value*100]%/[MODPAINT_MAX_COLOR_VALUE*100]%")
+ return
+ else if(color_value < MODPAINT_MIN_COLOR_VALUE)
+ balloon_alert(usr, "one of colors too low! ([color_value*100]%/[MODPAINT_MIN_COLOR_VALUE*100]%")
+ return
+ if(total_color_value > MODPAINT_MAX_OVERALL_COLORS)
+ balloon_alert(usr, "total colors too high! ([total_color_value*100]%/[MODPAINT_MAX_OVERALL_COLORS*100]%)")
+ return
+ else if(total_color_value < MODPAINT_MIN_OVERALL_COLORS)
+ balloon_alert(usr, "total colors too low! ([total_color_value*100]%/[MODPAINT_MIN_OVERALL_COLORS*100]%)")
+ return
+ editing_mod.set_mod_color(current_color)
+ SStgui.close_uis(src)
+
+/obj/item/mod/paint/proc/paint_skin(obj/item/mod/control/mod, mob/user)
+ if(length(mod.theme.skins) <= 1)
+ balloon_alert(user, "no alternate skins!")
+ return
+ var/list/skins = list()
+ for(var/mod_skin in mod.theme.skins)
+ skins[mod_skin] = image(icon = mod.icon, icon_state = "[mod_skin]-control")
+ var/pick = show_radial_menu(user, mod, skins, custom_check = CALLBACK(src, .proc/check_menu, mod, user), require_near = TRUE)
+ if(!pick)
+ balloon_alert(user, "no skin picked!")
+ return
+ mod.set_mod_skin(pick)
+
+/obj/item/mod/paint/proc/check_menu(obj/item/mod/control/mod, mob/user)
+ if(user.incapacitated() || !user.is_holding(src) || !mod || mod.active || mod.activating)
+ return FALSE
+ return TRUE
+
+#undef MODPAINT_MAX_COLOR_VALUE
+#undef MODPAINT_MIN_COLOR_VALUE
+#undef MODPAINT_MAX_SECTION_COLORS
+#undef MODPAINT_MIN_SECTION_COLORS
+#undef MODPAINT_MAX_OVERALL_COLORS
+#undef MODPAINT_MIN_OVERALL_COLORS
+
+/obj/item/mod/skin_applier
+ name = "MOD skin applier"
+ desc = "This one-use skin applier will add a skin to MODsuits of a specific type."
+ icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'
+ icon_state = "skinapplier"
+ var/skin = "civilian"
+ var/compatible_theme = /datum/mod_theme
+
+/obj/item/mod/skin_applier/Initialize(mapload)
+ . = ..()
+ name = "MOD [skin] skin applier"
+
+/obj/item/mod/skin_applier/pre_attack(atom/attacked_atom, mob/living/user, params)
+ if(!istype(attacked_atom, /obj/item/mod/control))
+ return ..()
+ var/obj/item/mod/control/mod = attacked_atom
+ if(mod.active || mod.activating)
+ balloon_alert(user, "suit is active!")
+ return TRUE
+ if(!istype(mod.theme, compatible_theme))
+ balloon_alert(user, "incompatible theme!")
+ return TRUE
+ mod.set_mod_skin(skin)
+ balloon_alert(user, "skin applied")
+ qdel(src)
+ return TRUE
+
+/obj/item/mod/skin_applier/honkerative
+ skin = "honkerative"
+ compatible_theme = /datum/mod_theme/syndicate
diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm
index eba6ecf9a99..4f1774e247e 100644
--- a/code/modules/mod/mod_theme.dm
+++ b/code/modules/mod/mod_theme.dm
@@ -59,8 +59,7 @@
HELMET_FLAGS = list(
UNSEALED_CLOTHING = SNUG_FIT,
SEALED_CLOTHING = THICKMATERIAL|STOPSPRESSUREDAMAGE,
- UNSEALED_INVISIBILITY = HIDEFACIALHAIR,
- SEALED_INVISIBILITY = HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
+ SEALED_INVISIBILITY = HIDEFACIALHAIR|HIDEMASK|HIDEEARS|HIDEEYES|HIDEFACE|HIDEHAIR|HIDESNOUT,
SEALED_COVER = HEADCOVERSMOUTH|HEADCOVERSEYES|PEPPERPROOF,
),
CHESTPLATE_FLAGS = list(
diff --git a/code/modules/mod/modules/_module.dm b/code/modules/mod/modules/_module.dm
index d00f0cb148c..4089ea68c58 100644
--- a/code/modules/mod/modules/_module.dm
+++ b/code/modules/mod/modules/_module.dm
@@ -274,6 +274,7 @@
else
return
var/mutable_appearance/module_icon = mutable_appearance('icons/mob/clothing/mod.dmi', used_overlay, layer = standing.layer + 0.1)
+ module_icon.appearance_flags |= RESET_COLOR
. += module_icon
/// Updates the signal used by active modules to be activated
diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm
index 1df19058451..98ae8343d5a 100644
--- a/code/modules/uplink/uplink_items/nukeops.dm
+++ b/code/modules/uplink/uplink_items/nukeops.dm
@@ -626,15 +626,6 @@
cost = 15
purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
-/datum/uplink_item/suits/paint
- name = "MODsuit Funny Paint Kit"
- desc = "A highly advanced piece of technology that will change the theme \
- of your MODsuit. Highly recommended by clowns."
- item = /obj/item/mod/paint
- cost = 0
- limited_stock = 1 // they're infinite usage so we don't need millions floating around
- purchasable_from = UPLINK_CLOWN_OPS // right now, only intended to let clown ops use honkerative modsuit, but could be made available to all uplinks if we get different suit variations.
-
/datum/uplink_item/device_tools/magboots
name = "Blood-Red Magboots"
desc = "A pair of magnetic boots with a Syndicate paintjob that assist with freer movement in space or on-station \
diff --git a/icons/obj/clothing/modsuit/mod_construction.dmi b/icons/obj/clothing/modsuit/mod_construction.dmi
index 37818382b42..a6be94284af 100644
Binary files a/icons/obj/clothing/modsuit/mod_construction.dmi and b/icons/obj/clothing/modsuit/mod_construction.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 7ba17df4ad2..e299ac439c1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3445,6 +3445,7 @@
#include "code\modules\mod\mod_construction.dm"
#include "code\modules\mod\mod_control.dm"
#include "code\modules\mod\mod_core.dm"
+#include "code\modules\mod\mod_paint.dm"
#include "code\modules\mod\mod_theme.dm"
#include "code\modules\mod\mod_types.dm"
#include "code\modules\mod\mod_ui.dm"
diff --git a/tgui/packages/tgui/interfaces/MODpaint.js b/tgui/packages/tgui/interfaces/MODpaint.js
new file mode 100644
index 00000000000..0efe9d45d2c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/MODpaint.js
@@ -0,0 +1,171 @@
+import { useBackend } from '../backend';
+import { Box, Stack, Section, ByondUi, Slider, Flex, Button } from '../components';
+import { Window } from '../layouts';
+import { capitalize } from 'common/string';
+
+const colorToMatrix = param => {
+ switch (param) {
+ case 'red':
+ return [
+ 1, 0, 0, 0,
+ 0.25, 0.5, 0, 0,
+ 0.25, 0, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'yellow':
+ return [
+ 0.5, 0.5, 0, 0,
+ 0.5, 0.5, 0, 0,
+ 0.25, 0.25, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'green':
+ return [
+ 0.5, 0.25, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0.25, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'teal':
+ return [
+ 0.25, 0.25, 0.25, 0,
+ 0, 0.5, 0.5, 0,
+ 0, 0.5, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'blue':
+ return [
+ 0.25, 0, 0.25, 0,
+ 0, 0.5, 0.25, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ case 'purple':
+ return [
+ 0.5, 0, 0.5, 0,
+ 0.25, 0.5, 0.25, 0,
+ 0.5, 0, 0.5, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ ];
+ }
+};
+
+const displayText = param => {
+ switch (param) {
+ case 'r':
+ return "Red";
+ case 'g':
+ return "Green";
+ case 'b':
+ return "Blue";
+ }
+};
+
+export const MODpaint = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { mapRef, currentColor } = data;
+ const [
+ [rr, rg, rb, ra],
+ [gr, gg, gb, ga],
+ [br, bg, bb, ba],
+ [ar, ag, ab, aa],
+ [cr, cg, cb, ca],
+ ] = currentColor;
+ const presets = ['red', 'yellow', 'green', 'teal', 'blue', 'purple'];
+ const prefixes = ['r', 'g', 'b'];
+ return (
+
+
+
+
+ {[0, 1, 2].map((row) => (
+
+ {[0, 1, 2].map((col, key) => (
+
+
+
+ {`${displayText(prefixes[col])}:`}
+
+
+
+ `${value}%`}
+ onDrag={(e, value) => {
+ let retColor = currentColor;
+ retColor[row*4+col] = value/100;
+ act("transition_color", { color: retColor });
+ }} />
+
+
+ ))}
+
+ ))}
+
+
+
+
+ {presets.map(preset => (
+
+
+
+
+
+
+
+
+
+
+ );
+};