mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 18:22:39 +00:00
[MIRROR] Customizable dragon plushie! (#11717)
Co-authored-by: MeepleMuncher <76881946+MeepleMuncher@users.noreply.github.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
349ac807c4
commit
3fd22e8106
206
code/game/objects/items/toys/toy_customizable.dm
Normal file
206
code/game/objects/items/toys/toy_customizable.dm
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/obj/item/toy/plushie/customizable
|
||||||
|
name = "You shouldn't be seeing this..."
|
||||||
|
desc = "Allan, please add details!"
|
||||||
|
icon = 'icons/obj/customizable_toys/durg.dmi'
|
||||||
|
icon_state = "blankdurg"
|
||||||
|
pokephrase = "Squeaky!"
|
||||||
|
|
||||||
|
var/base_color = "#FFFFFF"
|
||||||
|
var/list/possible_overlays
|
||||||
|
var/list/added_overlays
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/update_icon()
|
||||||
|
cut_overlays()
|
||||||
|
var/mutable_appearance/B = mutable_appearance(icon, icon_state)
|
||||||
|
B.color = base_color
|
||||||
|
add_overlay(B)
|
||||||
|
if(added_overlays)
|
||||||
|
for(var/key, value in added_overlays)
|
||||||
|
var/mutable_appearance/our_image = mutable_appearance(icon, key)
|
||||||
|
our_image.color = value["color"]
|
||||||
|
our_image.alpha = value["alpha"]
|
||||||
|
add_overlay(our_image)
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/Initialize(mapload)
|
||||||
|
. = ..()
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/tgui_state(mob/user)
|
||||||
|
return GLOB.tgui_conscious_state
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/tgui_interact(mob/user, datum/tgui/ui = null)
|
||||||
|
ui = SStgui.try_update_ui(user, src, ui)
|
||||||
|
if(!ui)
|
||||||
|
ui = new(user, src, "PlushieEditor")
|
||||||
|
ui.set_autoupdate(FALSE) //This might be a bit intensive, better to not update it every few ticks
|
||||||
|
ui.open()
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/tgui_data(mob/user)
|
||||||
|
|
||||||
|
var/list/possible_overlay_data = list()
|
||||||
|
if(possible_overlays)
|
||||||
|
for(var/state,name in possible_overlays)
|
||||||
|
UNTYPED_LIST_ADD(possible_overlay_data, list(
|
||||||
|
"name" = name,
|
||||||
|
"icon_state" = state
|
||||||
|
))
|
||||||
|
|
||||||
|
var/list/our_overlays = list()
|
||||||
|
if(added_overlays)
|
||||||
|
for(var/state,overlay in added_overlays)
|
||||||
|
UNTYPED_LIST_ADD(our_overlays, list(
|
||||||
|
"icon_state" = state,
|
||||||
|
"name" = possible_overlays[state],
|
||||||
|
"color" = overlay["color"],
|
||||||
|
"alpha" = overlay["alpha"]
|
||||||
|
))
|
||||||
|
|
||||||
|
var/list/data = list(
|
||||||
|
"base_color" = base_color,
|
||||||
|
"name" = name,
|
||||||
|
"icon" = icon,
|
||||||
|
"preview" = icon2base64(get_flat_icon(src)),
|
||||||
|
"possible_overlays" = possible_overlay_data,
|
||||||
|
"overlays" = our_overlays
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/tgui_act(action, params, datum/tgui/ui)
|
||||||
|
if(..())
|
||||||
|
return TRUE
|
||||||
|
add_fingerprint(ui.user)
|
||||||
|
|
||||||
|
if(!added_overlays)
|
||||||
|
added_overlays = list()
|
||||||
|
|
||||||
|
switch(action)
|
||||||
|
if("add_overlay")
|
||||||
|
if(!possible_overlays)
|
||||||
|
return FALSE
|
||||||
|
var/new_overlay = params["new_overlay"]
|
||||||
|
if(!(new_overlay in possible_overlays))
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
added_overlays[new_overlay] = list(color = "#FFFFFF", alpha = 255)
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("remove_overlay")
|
||||||
|
if(!added_overlays)
|
||||||
|
return FALSE
|
||||||
|
var/removed_overlay = params["removed_overlay"]
|
||||||
|
if(!(removed_overlay in added_overlays))
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
added_overlays.Remove(removed_overlay)
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("change_overlay_color")
|
||||||
|
if(!possible_overlays)
|
||||||
|
return FALSE
|
||||||
|
var/selected_icon_state = params["icon_state"]
|
||||||
|
if(!(selected_icon_state in possible_overlays))
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
var/target = added_overlays[selected_icon_state]
|
||||||
|
var/mob/our_user = ui.user
|
||||||
|
var/new_color = tgui_color_picker(our_user, "Choose a color:", possible_overlays[selected_icon_state], base_color)
|
||||||
|
if(!new_color || our_user.stat || !Adjacent(our_user))
|
||||||
|
return FALSE
|
||||||
|
target["color"] = new_color
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("move_overlay_up")
|
||||||
|
var/target = params["icon"]
|
||||||
|
var/idx = added_overlays.Find(target)
|
||||||
|
if(!idx)
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
if (idx < added_overlays.len)
|
||||||
|
added_overlays.Swap(idx, idx + 1)
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("move_overlay_down")
|
||||||
|
var/target = params["icon"]
|
||||||
|
var/idx = added_overlays.Find(target)
|
||||||
|
if(!idx)
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
if (idx > 1)
|
||||||
|
added_overlays.Swap(idx, idx - 1)
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("change_base_color")
|
||||||
|
. = TRUE
|
||||||
|
var/mob/our_user = ui.user
|
||||||
|
var/new_color = tgui_color_picker(our_user, "Choose a color:", "Plushie base color", base_color)
|
||||||
|
if(!new_color || our_user.stat || !Adjacent(our_user))
|
||||||
|
return FALSE
|
||||||
|
base_color = new_color
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("set_overlay_alpha")
|
||||||
|
var/target = added_overlays[params["icon_state"]]
|
||||||
|
if(!target)
|
||||||
|
return FALSE
|
||||||
|
. = TRUE
|
||||||
|
var/new_alpha = params["alpha"]
|
||||||
|
target["alpha"] = new_alpha
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("import_config")
|
||||||
|
. = TRUE
|
||||||
|
var/our_data = params["config"]
|
||||||
|
var/imported_color = sanitize_hexcolor(our_data["base_color"])
|
||||||
|
if(imported_color)
|
||||||
|
base_color = imported_color
|
||||||
|
set_new_name(our_data["name"])
|
||||||
|
added_overlays.Cut()
|
||||||
|
if(!possible_overlays)
|
||||||
|
return
|
||||||
|
for(var/overlay in our_data["overlays"])
|
||||||
|
if(possible_overlays.Find(overlay["icon_state"]))
|
||||||
|
added_overlays[overlay["icon_state"]] = list( color = overlay["color"], alpha = overlay["alpha"] )
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("clear")
|
||||||
|
. = TRUE
|
||||||
|
added_overlays.Cut()
|
||||||
|
base_color = "#FFFFFF"
|
||||||
|
update_icon()
|
||||||
|
|
||||||
|
if("rename")
|
||||||
|
return set_new_name(params["name"])
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/proc/set_new_name(new_name)
|
||||||
|
var/sane_name = sanitize_name(new_name)
|
||||||
|
if(!sane_name)
|
||||||
|
return FALSE
|
||||||
|
name = sane_name
|
||||||
|
adjusted_name = sane_name
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/AltClick(mob/user)
|
||||||
|
tgui_interact(user)
|
||||||
|
|
||||||
|
/obj/item/toy/plushie/customizable/dragon
|
||||||
|
name = "custom dragon plushie"
|
||||||
|
desc = "A customizable, modular plushie in the shape of a dragon. How cute!"
|
||||||
|
pokephrase = "Gawr!"
|
||||||
|
possible_overlays = list(
|
||||||
|
"durg_underbelly" = "Underbelly",
|
||||||
|
"durg_fur" = "Fur",
|
||||||
|
"durg_spines" = "Spines",
|
||||||
|
"classic_w_1" = "Wings, Western, L",
|
||||||
|
"classic_w_2" = "Wings, Western, R",
|
||||||
|
"classic_w_misc" = "Wings, Western, Underside",
|
||||||
|
"fairy_w_1" = "Wings, Fairy, L",
|
||||||
|
"fairy_w_2" = "Wings, Fairy, R",
|
||||||
|
"angular_w_1" = "Wings, Angular, L",
|
||||||
|
"angular_w_2" = "Wings, Angular, R",
|
||||||
|
"double_h_1" = "Horns, Double, L",
|
||||||
|
"double_h_2" = "Horns, Double, R",
|
||||||
|
"classic_h_1" = "Horns, Classic, L",
|
||||||
|
"classic_h_2" = "Horns, Classic, R",
|
||||||
|
"thick_h_1" = "Horns, Thick, L",
|
||||||
|
"thick_h_2" = "Horns, Thick, R"
|
||||||
|
)
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
blacklisted_types += subtypesof(/obj/item/toy/plushie/fluff)
|
blacklisted_types += subtypesof(/obj/item/toy/plushie/fluff)
|
||||||
blacklisted_types += /obj/item/toy/plushie/borgplushie/drake //VOREStation addition
|
blacklisted_types += /obj/item/toy/plushie/borgplushie/drake //VOREStation addition
|
||||||
blacklisted_types += /obj/item/toy/plushie/dragon/gold_east
|
blacklisted_types += /obj/item/toy/plushie/dragon/gold_east
|
||||||
|
blacklisted_types += /obj/item/toy/plushie/customizable
|
||||||
for(var/obj/item/toy/plushie/plushie_type as anything in subtypesof(/obj/item/toy/plushie) - blacklisted_types)
|
for(var/obj/item/toy/plushie/plushie_type as anything in subtypesof(/obj/item/toy/plushie) - blacklisted_types)
|
||||||
plushies[initial(plushie_type.name)] = plushie_type
|
plushies[initial(plushie_type.name)] = plushie_type
|
||||||
gear_tweaks += new/datum/gear_tweak/path(sortAssoc(plushies))
|
gear_tweaks += new/datum/gear_tweak/path(sortAssoc(plushies))
|
||||||
|
|||||||
BIN
icons/obj/customizable_toys/durg.dmi
Normal file
BIN
icons/obj/customizable_toys/durg.dmi
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { ImageButton, Stack } from 'tgui-core/components';
|
||||||
|
import type { Overlay } from '../types';
|
||||||
|
|
||||||
|
type ActiveOverlaysGridProps = {
|
||||||
|
icon: string;
|
||||||
|
overlays: Overlay[];
|
||||||
|
selectedOverlay: string | null;
|
||||||
|
onSelect: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActiveOverlaysGrid = memo<ActiveOverlaysGridProps>(
|
||||||
|
({ icon, overlays, selectedOverlay, onSelect }) => (
|
||||||
|
<Stack wrap="wrap" justify="center">
|
||||||
|
{overlays.map((ov) => (
|
||||||
|
<Stack.Item key={ov.icon_state}>
|
||||||
|
<ImageButton
|
||||||
|
dmIcon={icon}
|
||||||
|
dmIconState={ov.icon_state}
|
||||||
|
onClick={() => onSelect(ov.icon_state)}
|
||||||
|
color={selectedOverlay === ov.icon_state ? 'green' : undefined}
|
||||||
|
>
|
||||||
|
{ov.name}
|
||||||
|
</ImageButton>
|
||||||
|
</Stack.Item>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useBackend } from 'tgui/backend';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Floating,
|
||||||
|
ImageButton,
|
||||||
|
Input,
|
||||||
|
Section,
|
||||||
|
Stack,
|
||||||
|
} from 'tgui-core/components';
|
||||||
|
import { createSearch } from 'tgui-core/string';
|
||||||
|
import { useOverlayMap } from '../function';
|
||||||
|
import type { Data } from '../types';
|
||||||
|
|
||||||
|
type OverlayModalProps = {
|
||||||
|
toggleOverlay: (icon_state: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OverlaySelector: React.FC<OverlayModalProps> = ({
|
||||||
|
toggleOverlay,
|
||||||
|
}) => {
|
||||||
|
const { data } = useBackend<Data>();
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const { possible_overlays, overlays, icon } = data;
|
||||||
|
|
||||||
|
const searcher = createSearch(
|
||||||
|
filter,
|
||||||
|
(ov: { name: string; icon_state: string }) => ov.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const overlayMap = useOverlayMap(overlays);
|
||||||
|
const filteredPossible = possible_overlays.filter(searcher);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Floating
|
||||||
|
placement="bottom-end"
|
||||||
|
contentClasses="PlushEditor__Floating"
|
||||||
|
content={
|
||||||
|
<Section title="Overlays" scrollable>
|
||||||
|
<Stack vertical fill>
|
||||||
|
<Stack.Item>
|
||||||
|
<Input
|
||||||
|
placeholder="Search overlays..."
|
||||||
|
value={filter}
|
||||||
|
onChange={setFilter}
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Box>
|
||||||
|
{filteredPossible.map(({ name, icon_state }) => (
|
||||||
|
<ImageButton
|
||||||
|
key={icon_state}
|
||||||
|
dmIcon={icon}
|
||||||
|
dmIconState={icon_state}
|
||||||
|
onClick={() => toggleOverlay(icon_state)}
|
||||||
|
color={overlayMap[icon_state] ? 'green' : 'red'}
|
||||||
|
fluid
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</ImageButton>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box className="VorePanel__floatingButton">+/-</Box>
|
||||||
|
</Floating>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { useBackend } from 'tgui/backend';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ColorBox,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
LabeledList,
|
||||||
|
Section,
|
||||||
|
Stack,
|
||||||
|
} from 'tgui-core/components';
|
||||||
|
import type { Data } from '../types';
|
||||||
|
|
||||||
|
type PreviewPanelProps = {
|
||||||
|
setSelectedOverlay: Dispatch<SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
export const PreviewPanel: React.FC<PreviewPanelProps> = ({
|
||||||
|
setSelectedOverlay,
|
||||||
|
}) => {
|
||||||
|
const { act, data } = useBackend<Data>();
|
||||||
|
const { base_color, preview, name } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
fill
|
||||||
|
title="Preview"
|
||||||
|
buttons={
|
||||||
|
<Button.Confirm
|
||||||
|
icon="trash"
|
||||||
|
color="red"
|
||||||
|
tooltip="Reset the edits"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOverlay(null);
|
||||||
|
act('clear');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button.Confirm>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack vertical fill>
|
||||||
|
<Stack.Item>
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
align="center"
|
||||||
|
width="100%"
|
||||||
|
height="320px"
|
||||||
|
style={{
|
||||||
|
borderRadius: 16,
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box m="auto" minWidth="256px" minHeight="256px">
|
||||||
|
<Image
|
||||||
|
src={`data:image/jpeg;base64,${preview}`}
|
||||||
|
style={{ width: '256px', height: '256px' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Divider />
|
||||||
|
<Stack.Item>
|
||||||
|
<LabeledList>
|
||||||
|
<LabeledList.Item label="Name">
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
value={name}
|
||||||
|
onChange={(value) => act('rename', { name: value })}
|
||||||
|
/>
|
||||||
|
</LabeledList.Item>
|
||||||
|
<LabeledList.Item label="Base color">
|
||||||
|
<Stack align="center" justify="space-between">
|
||||||
|
<ColorBox color={base_color} mr={2} />
|
||||||
|
<Button icon="brush" onClick={() => act('change_base_color')}>
|
||||||
|
Recolor
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</LabeledList.Item>
|
||||||
|
</LabeledList>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useBackend } from 'tgui/backend';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ColorBox,
|
||||||
|
LabeledList,
|
||||||
|
NoticeBox,
|
||||||
|
Section,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
} from 'tgui-core/components';
|
||||||
|
import { downloadJson, handleImportData, useOverlayMap } from '../function';
|
||||||
|
import type { Data } from '../types';
|
||||||
|
import { ActiveOverlaysGrid } from './ActiveOverlayGrid';
|
||||||
|
import { OverlaySelector } from './OverlaySelector';
|
||||||
|
|
||||||
|
type SidebarPanelProps = {
|
||||||
|
selectedOverlay: string | null;
|
||||||
|
setSelectedOverlay: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarPanel: React.FC<SidebarPanelProps> = ({
|
||||||
|
selectedOverlay,
|
||||||
|
setSelectedOverlay,
|
||||||
|
}) => {
|
||||||
|
const { act, data } = useBackend<Data>();
|
||||||
|
const { base_color, icon, name, overlays } = data;
|
||||||
|
const overlayMap = useOverlayMap(overlays);
|
||||||
|
|
||||||
|
function toggleOverlay(icon_state: string) {
|
||||||
|
if (overlayMap[icon_state]) {
|
||||||
|
act('remove_overlay', { removed_overlay: icon_state });
|
||||||
|
if (selectedOverlay === icon_state) {
|
||||||
|
setSelectedOverlay(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
act('add_overlay', { new_overlay: icon_state });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title="Editor"
|
||||||
|
fill
|
||||||
|
buttons={
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button.File
|
||||||
|
accept=".json"
|
||||||
|
tooltip="Import plushie data"
|
||||||
|
icon="upload"
|
||||||
|
onSelectFiles={(files) => handleImportData(files)}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button.File>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
icon="download"
|
||||||
|
onClick={() =>
|
||||||
|
downloadJson(`plushie-${name}-config.json`, {
|
||||||
|
name: name,
|
||||||
|
base_color: base_color,
|
||||||
|
icon: icon,
|
||||||
|
overlays: overlays,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack vertical fill>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Section
|
||||||
|
title={`Overlays (${overlays.length} active)`}
|
||||||
|
fill
|
||||||
|
scrollable
|
||||||
|
buttons={<OverlaySelector toggleOverlay={toggleOverlay} />}
|
||||||
|
>
|
||||||
|
<ActiveOverlaysGrid
|
||||||
|
icon={icon}
|
||||||
|
overlays={overlays}
|
||||||
|
selectedOverlay={selectedOverlay}
|
||||||
|
onSelect={(id) =>
|
||||||
|
setSelectedOverlay(selectedOverlay === id ? null : id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item basis="35%">
|
||||||
|
{!selectedOverlay ? (
|
||||||
|
<Section fill title="Overlay details">
|
||||||
|
<NoticeBox>
|
||||||
|
Select an active overlay to edit its settings.
|
||||||
|
</NoticeBox>
|
||||||
|
</Section>
|
||||||
|
) : (
|
||||||
|
<Section
|
||||||
|
title="Overlay details"
|
||||||
|
fill
|
||||||
|
buttons={
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
icon="arrow-up"
|
||||||
|
onClick={() =>
|
||||||
|
act('move_overlay_up', {
|
||||||
|
icon: overlayMap[selectedOverlay].icon_state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
icon="arrow-down"
|
||||||
|
onClick={() =>
|
||||||
|
act('move_overlay_down', {
|
||||||
|
icon: overlayMap[selectedOverlay].icon_state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
icon="trash"
|
||||||
|
backgroundColor="#d63939ff"
|
||||||
|
onClick={() =>
|
||||||
|
toggleOverlay(overlayMap[selectedOverlay].icon_state)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LabeledList>
|
||||||
|
<LabeledList.Item label="Overlay">
|
||||||
|
{overlayMap[selectedOverlay].icon_state}
|
||||||
|
</LabeledList.Item>
|
||||||
|
<LabeledList.Item label="Color">
|
||||||
|
<Stack align="center" justify="space-between">
|
||||||
|
<ColorBox
|
||||||
|
color={overlayMap[selectedOverlay].color}
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="brush"
|
||||||
|
onClick={() =>
|
||||||
|
act('change_overlay_color', {
|
||||||
|
icon_state: overlayMap[selectedOverlay].icon_state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit Overlay Color
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</LabeledList.Item>
|
||||||
|
<LabeledList.Item label="Opacity">
|
||||||
|
<Stack align="center">
|
||||||
|
<Box width={12} mr={1} textAlign="right">
|
||||||
|
{Math.round(
|
||||||
|
((overlayMap[selectedOverlay].alpha ?? 255) / 255) *
|
||||||
|
100,
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</Box>
|
||||||
|
<Slider
|
||||||
|
value={overlayMap[selectedOverlay].alpha ?? 255}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={255}
|
||||||
|
step={5}
|
||||||
|
onChange={(e, val) =>
|
||||||
|
act('set_overlay_alpha', {
|
||||||
|
icon_state: overlayMap[selectedOverlay].icon_state,
|
||||||
|
alpha: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</LabeledList.Item>
|
||||||
|
</LabeledList>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
tgui/packages/tgui/interfaces/PlushieEditor/function.ts
Normal file
31
tgui/packages/tgui/interfaces/PlushieEditor/function.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useBackend } from 'tgui/backend';
|
||||||
|
import type { Overlay, PlushieConfig } from './types';
|
||||||
|
|
||||||
|
export function downloadJson(filename: string, data: PlushieConfig) {
|
||||||
|
const blob = new Blob([JSON.stringify(data)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
Byond.saveBlob(blob, filename, '.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleImportData(
|
||||||
|
importString: string | string[],
|
||||||
|
): PlushieConfig | null {
|
||||||
|
const { act } = useBackend();
|
||||||
|
const ourInput = Array.isArray(importString) ? importString[0] : importString;
|
||||||
|
try {
|
||||||
|
const parsedData: PlushieConfig = JSON.parse(ourInput);
|
||||||
|
act('import_config', { config: parsedData });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to parse JSON:', err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOverlayMap(overlays: Overlay[]) {
|
||||||
|
return useMemo(
|
||||||
|
() => Object.fromEntries(overlays.map((o) => [o.icon_state, o])),
|
||||||
|
[overlays],
|
||||||
|
);
|
||||||
|
}
|
||||||
28
tgui/packages/tgui/interfaces/PlushieEditor/index.tsx
Normal file
28
tgui/packages/tgui/interfaces/PlushieEditor/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Window } from 'tgui/layouts';
|
||||||
|
import { Stack } from 'tgui-core/components';
|
||||||
|
import { PreviewPanel } from './EditorElements/PreviewPanel';
|
||||||
|
import { SidebarPanel } from './EditorElements/SidePanel';
|
||||||
|
|
||||||
|
export const PlushieEditor: React.FC = () => {
|
||||||
|
const [selectedOverlay, setSelectedOverlay] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Window width={780} height={560}>
|
||||||
|
<Window.Content scrollable>
|
||||||
|
<Stack fill>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<PreviewPanel setSelectedOverlay={setSelectedOverlay} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<SidebarPanel
|
||||||
|
selectedOverlay={selectedOverlay}
|
||||||
|
setSelectedOverlay={setSelectedOverlay}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
tgui/packages/tgui/interfaces/PlushieEditor/types.tsx
Normal file
22
tgui/packages/tgui/interfaces/PlushieEditor/types.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export type Data = {
|
||||||
|
name: string;
|
||||||
|
base_color: string;
|
||||||
|
icon: string;
|
||||||
|
preview: string;
|
||||||
|
possible_overlays: { name: string; icon_state: string }[];
|
||||||
|
overlays: Overlay[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Overlay = {
|
||||||
|
icon_state: string;
|
||||||
|
name?: string;
|
||||||
|
color?: string;
|
||||||
|
alpha?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlushieConfig = {
|
||||||
|
name: string;
|
||||||
|
base_color: string;
|
||||||
|
icon: string;
|
||||||
|
overlays: Overlay[];
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ export const VorePanelEditCheckboxes = (props: {
|
|||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<Floating
|
<Floating
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
contentClasses="VorePanel__fLoating"
|
contentClasses="VorePanel__Floating"
|
||||||
content={
|
content={
|
||||||
<Stack vertical fill>
|
<Stack vertical fill>
|
||||||
{options.map((value) => (
|
{options.map((value) => (
|
||||||
|
|||||||
13
tgui/packages/tgui/styles/interfaces/PlushEditor.scss
Normal file
13
tgui/packages/tgui/styles/interfaces/PlushEditor.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.PlushEditor__Floating {
|
||||||
|
height: 450px;
|
||||||
|
background-color: var(--color-section);
|
||||||
|
backdrop-filter: var(--blur-medium);
|
||||||
|
border: var(--border-thickness-tiny) solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
box-shadow: var(--shadow-glow-medium) hsla(0, 0%, 0%, 0.5);
|
||||||
|
margin: var(--space-m);
|
||||||
|
color: var(--color-text);
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: var(--space-m);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.VorePanel__fLoating {
|
.VorePanel__Floating {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background-color: var(--color-section);
|
background-color: var(--color-section);
|
||||||
backdrop-filter: var(--blur-medium);
|
backdrop-filter: var(--blur-medium);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
@include meta.load-css('./interfaces/TicketPanel.scss');
|
@include meta.load-css('./interfaces/TicketPanel.scss');
|
||||||
@include meta.load-css('./interfaces/VorePanel.scss');
|
@include meta.load-css('./interfaces/VorePanel.scss');
|
||||||
@include meta.load-css('./interfaces/Wires.scss');
|
@include meta.load-css('./interfaces/Wires.scss');
|
||||||
|
@include meta.load-css('./interfaces/PlushEditor.scss');
|
||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
@include meta.load-css('./layouts/Layout.scss');
|
@include meta.load-css('./layouts/Layout.scss');
|
||||||
|
|||||||
@@ -1662,6 +1662,7 @@
|
|||||||
#include "code\game\objects\items\toys\godfigures.dm"
|
#include "code\game\objects\items\toys\godfigures.dm"
|
||||||
#include "code\game\objects\items\toys\mech_toys.dm"
|
#include "code\game\objects\items\toys\mech_toys.dm"
|
||||||
#include "code\game\objects\items\toys\target_toy.dm"
|
#include "code\game\objects\items\toys\target_toy.dm"
|
||||||
|
#include "code\game\objects\items\toys\toy_customizable.dm"
|
||||||
#include "code\game\objects\items\toys\toys.dm"
|
#include "code\game\objects\items\toys\toys.dm"
|
||||||
#include "code\game\objects\items\toys\toys_ch.dm"
|
#include "code\game\objects\items\toys\toys_ch.dm"
|
||||||
#include "code\game\objects\items\toys\toys_vr.dm"
|
#include "code\game\objects\items\toys\toys_vr.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user