mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +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 += /obj/item/toy/plushie/borgplushie/drake //VOREStation addition
|
||||
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)
|
||||
plushies[initial(plushie_type.name)] = plushie_type
|
||||
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>
|
||||
<Floating
|
||||
placement="bottom-end"
|
||||
contentClasses="VorePanel__fLoating"
|
||||
contentClasses="VorePanel__Floating"
|
||||
content={
|
||||
<Stack vertical fill>
|
||||
{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;
|
||||
background-color: var(--color-section);
|
||||
backdrop-filter: var(--blur-medium);
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
@include meta.load-css('./interfaces/TicketPanel.scss');
|
||||
@include meta.load-css('./interfaces/VorePanel.scss');
|
||||
@include meta.load-css('./interfaces/Wires.scss');
|
||||
@include meta.load-css('./interfaces/PlushEditor.scss');
|
||||
|
||||
// Layouts
|
||||
@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\mech_toys.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_ch.dm"
|
||||
#include "code\game\objects\items\toys\toys_vr.dm"
|
||||
|
||||
Reference in New Issue
Block a user